feat(core): Turn the skills hub on with an environment variable instead of a PostHog flag (no-changelog) (#41143)

This commit is contained in:
Ricardo Espinoza 2026-10-15 08:59:25 +00:00 committed by GitHub
parent 315e0a5377
commit 0c0b38e3cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 284 additions and 82 deletions

View File

@ -5,12 +5,6 @@ import type { AgentSkill } from './types';
import { paginationSchema } from '../dto/pagination/pagination.dto';
import { Z } from '../zod-class';
/**
* PostHog flag for the rollout of the skills hub, read for the instance once at startup.
* Off: agents keep their inline skills and the skills routes answer 404.
*/
export const SKILLS_FLAG = '125_context_skills';
export const SKILLS_MAX_PAGE_SIZE = 100;
/** Optional on purpose: without `take` the caller gets every visible skill. */

View File

@ -463,7 +463,7 @@ export type FrontendModuleSettings = {
knowledgeBaseEnabled: boolean;
/** Whether the AI Assistant proxy is available to the agents module. */
proxyEnabled: boolean;
/** Whether agents use the skills hub (PostHog flag `125_context_skills`, read at startup). */
/** Whether agents use the skills hub (`N8N_AGENTS_SKILLS_HUB_ENABLED`, read at startup). */
skillsHubEnabled?: boolean;
};
};

View File

@ -66,6 +66,15 @@ export class AgentsConfig {
@Env('N8N_AGENTS_TRACING_RECORD_OUTPUTS')
tracingRecordOutputs: boolean = true;
/**
* Whether agents keep their skills in the skills hub instead of inline on the agent.
* Read once when the agents module starts; a change needs a restart. Set the same value
* on every main and worker. With it on, each main copies inline skills into the hub at
* startup, once per agent.
*/
@Env('N8N_AGENTS_SKILLS_HUB_ENABLED')
skillsHubEnabled: boolean = false;
/**
* Comma-separated list of agent sub-feature modules to enable. Each entry
* gates a specific frontend/runtime capability inside the agents module.

View File

@ -743,6 +743,7 @@ describe('GlobalConfig', () => {
tracingEnabled: true,
tracingRecordInputs: true,
tracingRecordOutputs: true,
skillsHubEnabled: false,
modules: [],
backgroundTasksEnabled: false,
sandboxEnabled: false,
@ -801,6 +802,15 @@ describe('GlobalConfig', () => {
expect(config.agents.tracingRecordOutputs).toBe(false);
});
it('should parse N8N_AGENTS_SKILLS_HUB_ENABLED from env variables', () => {
process.env = {
N8N_AGENTS_SKILLS_HUB_ENABLED: 'true',
};
const config = Container.get(GlobalConfig);
expect(config.agents.skillsHubEnabled).toBe(true);
});
it('should parse N8N_AGENTS_AI_SANDBOX_SNAPSHOT from env variables', () => {
process.env = {
N8N_AGENTS_AI_SANDBOX_SNAPSHOT: 'n8n/agent-knowledge:1.2.3',

View File

@ -9,7 +9,7 @@ Guidance for the agents module (`packages/cli/src/modules/agents`). See the [cli
## Skill refs
- The PostHog flag `125_context_skills` (`SKILLS_FLAG`) selects where skills live. `SkillsHubRollout` reads it for the instance once, when the agents module starts, and keeps the value for the life of the process; a change needs a restart. `AgentSkillRefsService.usesSkillsHub` and the `/skills` routes read `SkillsHubRollout`. Turn it on locally with `N8N_FEATURE_FLAG_OVERRIDES`. There are no double writes: data written with one setting is not visible with the other.
- The environment variable `N8N_AGENTS_SKILLS_HUB_ENABLED` (`AgentsConfig.skillsHubEnabled`, default `false`) selects where skills live. `SkillsHubRollout` reads it once, when the agents module starts, and keeps the value for the life of the process; a change needs a restart. Set the same value on every main and worker. `AgentSkillRefsService.usesSkillsHub` and the `/skills` routes read `SkillsHubRollout`. There are no double writes: data written with one setting is not visible with the other.
- Hub on: skills live in the skills hub (`skill`, `skill_version`, `skill_file`). A draft keeps its skill refs in `agent_skill_dependency`, in attach order (`createdAt`). A published version keeps its pins in `agent_version_skill_dependency`, in the same order.
- Hub on: a skill that an agent page creates is available to the project, to that agent only (`skill.agentId`), or to the instance (`availableTo` on the create request, default `project`). A duplicate of an agent gets its own copy of each agent-only skill.
- Hub off: the refs are the `skills` key of `agents.schema` and `agent_history.schema`, and the bodies are the `agents.skills` and `agent_history.skills` columns, as before the skills hub. A ref with `enabled: false` is hidden, and its body stays in the column.

View File

@ -1,45 +1,37 @@
import { SKILLS_FLAG } from '@n8n/api-types';
import type { Logger } from '@n8n/backend-common';
import { AgentsConfig } from '@n8n/config';
import { mock } from 'vitest-mock-extended';
import type { PostHogClient } from '@/posthog';
import { SkillsHubRollout } from '../skills/skills-hub-rollout';
describe('SkillsHubRollout', () => {
const postHog = mock<PostHogClient>();
let config: AgentsConfig;
let rollout: SkillsHubRollout;
beforeEach(() => {
vi.resetAllMocks();
rollout = new SkillsHubRollout(postHog, mock<Logger>());
config = new AgentsConfig();
rollout = new SkillsHubRollout(config, mock<Logger>());
});
it('is off until the flag is read', () => {
it('is off by default', () => {
rollout.init();
expect(config.skillsHubEnabled).toBe(false);
expect(rollout.isEnabled).toBe(false);
});
it('reads the flag for the instance once and keeps the value', async () => {
postHog.getFeatureFlagForInstance.mockResolvedValue(true);
it('is off until the setting is read', () => {
config.skillsHubEnabled = true;
await rollout.init();
postHog.getFeatureFlagForInstance.mockResolvedValue(false);
expect(rollout.isEnabled).toBe(false);
});
it('reads N8N_AGENTS_SKILLS_HUB_ENABLED once and keeps the value until a restart', () => {
config.skillsHubEnabled = true;
rollout.init();
config.skillsHubEnabled = false;
expect(rollout.isEnabled).toBe(true);
expect(postHog.getFeatureFlagForInstance).toHaveBeenCalledTimes(1);
expect(postHog.getFeatureFlagForInstance).toHaveBeenCalledWith(SKILLS_FLAG);
expect(postHog.getFeatureFlags).not.toHaveBeenCalled();
});
it.each([
['missing', undefined],
['false', false],
['a variant string', 'test'],
])('stays off when the flag is %s', async (_label, value) => {
postHog.getFeatureFlagForInstance.mockResolvedValue(value);
await rollout.init();
expect(rollout.isEnabled).toBe(false);
});
});

View File

@ -1,14 +1,13 @@
import { AgentsConfig } from '@n8n/config';
import { Container } from '@n8n/di';
import { PostHogClient } from '@/posthog';
import { SkillsHubRollout } from '../../skills/skills-hub-rollout';
/**
* Sets the `125_context_skills` flag for the instance and reads it again, as a restart
* does. Tests that need the skills hub on or off call it instead of a config value.
* Sets `N8N_AGENTS_SKILLS_HUB_ENABLED` and reads it again, as a restart does. Tests that
* need the skills hub on or off call it instead of changing the config directly.
*/
export async function setSkillsHubFlag(enabled: boolean): Promise<void> {
vi.spyOn(Container.get(PostHogClient), 'getFeatureFlagForInstance').mockResolvedValue(enabled);
await Container.get(SkillsHubRollout).init();
export function setSkillsHubFlag(enabled: boolean): void {
Container.get(AgentsConfig).skillsHubEnabled = enabled;
Container.get(SkillsHubRollout).init();
}

View File

@ -38,7 +38,7 @@ export interface AgentSkillsSnapshot {
* version, and for the skill bodies they resolve to. All other code reads and
* writes skill refs through this service.
*
* With the skills hub on (the `125_context_skills` flag), the refs of a draft are the
* With the skills hub on (`N8N_AGENTS_SKILLS_HUB_ENABLED`), the refs of a draft are the
* `agent_skill_dependency` rows and the refs of a published version are the
* `agent_version_skill_dependency` pins, in attach order. The skill bodies come
* from the skills hub.

View File

@ -11,10 +11,10 @@ export class AgentsModule implements ModuleInterface {
const { SandboxSettingsService } = await import('@/services/sandbox-settings.service.js');
Container.get(SandboxSettingsService).registerCredentialUses();
// Read the skills hub flag once, before anything reads or writes agent skills. With
// the flag on, each main copies inline agent skills into the hub before agents run.
// Read the skills hub setting once, before anything reads or writes agent skills. With
// it on, each main copies inline agent skills into the hub before agents run.
const { SkillsHubRollout } = await import('./skills/skills-hub-rollout.js');
await Container.get(SkillsHubRollout).init();
Container.get(SkillsHubRollout).init();
const { SkillsHubStartupCopy } = await import('./skills/skills-hub-startup-copy.js');
await Container.get(SkillsHubStartupCopy).runOnStartup();

View File

@ -1,27 +1,24 @@
import { SKILLS_FLAG } from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
import { AgentsConfig } from '@n8n/config';
import { Service } from '@n8n/di';
import { PostHogClient } from '@/posthog';
/**
* Whether agents use the skills hub, from the PostHog flag `125_context_skills` for the
* instance. `init` reads the flag once at startup and the value holds for the life of the
* process, so the startup copy and every read agree. A change needs a restart. PostHog
* down or diagnostics off means off, unless `N8N_FEATURE_FLAG_OVERRIDES` turns it on.
* Whether agents use the skills hub, from `N8N_AGENTS_SKILLS_HUB_ENABLED`. `init` reads it
* once at startup and the value holds for the life of the process, so the startup copy and
* every read agree. A change needs a restart.
*/
@Service()
export class SkillsHubRollout {
private enabled = false;
constructor(
private readonly postHogClient: PostHogClient,
private readonly agentsConfig: AgentsConfig,
private readonly logger: Logger,
) {}
async init(): Promise<void> {
this.enabled = (await this.postHogClient.getFeatureFlagForInstance(SKILLS_FLAG)) === true;
this.logger.debug('Read the skills hub rollout flag', { enabled: this.enabled });
init(): void {
this.enabled = this.agentsConfig.skillsHubEnabled;
this.logger.debug('Read the skills hub setting', { enabled: this.enabled });
}
get isEnabled(): boolean {

View File

@ -22,6 +22,10 @@ const BATCH_SIZE = 100;
const WARN_AGENT_IDS = 20;
type CopyCounts = {
/** Agents checked, whether or not they needed a copy. */
scanned: number;
/** Agents with nothing left to copy. */
skipped: number;
agents: number;
skills: number;
versions: number;
@ -32,6 +36,9 @@ type CopyCounts = {
/** What one agent's copy wrote. */
type AgentCounts = Pick<CopyCounts, 'skills' | 'versions' | 'pins'>;
/** What one agent's copy wrote, and what it copied from. */
type AgentCopyResult = AgentCounts & { publishedVersions: number; draft: boolean };
type AgentWork = {
agent: Pick<Agent, 'id' | 'schema' | 'skills'>;
versions: Array<Pick<AgentHistory, 'versionId' | 'schema' | 'skills'>>;
@ -89,14 +96,32 @@ export class SkillsHubStartupCopy {
}
async copyAll(): Promise<CopyCounts> {
const counts: CopyCounts = { agents: 0, skills: 0, versions: 0, pins: 0, failed: 0 };
const counts: CopyCounts = {
scanned: 0,
skipped: 0,
agents: 0,
skills: 0,
versions: 0,
pins: 0,
failed: 0,
};
// A start line without a summary line shows that the copy stopped halfway.
this.logger.info('Started copying inline agent skills into the skills hub');
const startedAt = Date.now();
let after: Pick<Agent, 'createdAt' | 'id'> | null = null;
for (;;) {
const agents = await this.agentRepository.findPageForSkillCopy(after, BATCH_SIZE);
for (const agent of agents) {
counts.scanned++;
try {
const copied = await this.copyAgentWithRetry(agent.id);
if (copied) {
if (!copied) counts.skipped++;
else {
// Logged after the commit, so a rolled-back copy never shows up here.
this.logger.debug('Copied the inline skills of an agent into the skills hub', {
agentId: agent.id,
...copied,
});
counts.agents++;
counts.skills += copied.skills;
counts.versions += copied.versions;
@ -108,14 +133,24 @@ export class SkillsHubStartupCopy {
counts.failed++;
this.logger.error('Failed to copy the skills of an agent into the skills hub', {
agentId: agent.id,
errorName: error instanceof Error ? error.name : undefined,
error: error instanceof Error ? error.message : String(error),
});
if (error instanceof Error) {
this.logger.debug('Stack of the failed skills hub copy', {
agentId: agent.id,
stack: error.stack,
});
}
}
}
if (agents.length < BATCH_SIZE) break;
after = agents[agents.length - 1];
}
this.logger.info('Copied inline agent skills into the skills hub', counts);
this.logger.info('Copied inline agent skills into the skills hub', {
...counts,
durationMs: Date.now() - startedAt,
});
return counts;
}
@ -152,13 +187,17 @@ export class SkillsHubStartupCopy {
* time, so one of them fails on the primary key. A second try picks another id. A
* failed try rolls back, so only the counts of the try that commits are kept.
*/
private async copyAgentWithRetry(agentId: string): Promise<AgentCounts | null> {
private async copyAgentWithRetry(agentId: string): Promise<AgentCopyResult | null> {
// Most agents have nothing left to copy after the first start. Check that without
// a transaction and a row lock, and check again under the lock.
if (!(await this.findWork(agentId, {}))) return null;
try {
return await this.copyAgent(agentId);
} catch {
} catch (error) {
this.logger.warn('Retrying the skills hub copy of an agent after a failed try', {
agentId,
error: error instanceof Error ? error.message : String(error),
});
return await this.copyAgent(agentId);
}
}
@ -180,7 +219,7 @@ export class SkillsHubStartupCopy {
}
/** Copies one agent in one transaction. Returns null when it wrote nothing. */
private async copyAgent(agentId: string): Promise<AgentCounts | null> {
private async copyAgent(agentId: string): Promise<AgentCopyResult | null> {
return await this.txRunner.run({}, async (ctx) => {
// A draft write to this agent waits until the copy commits. The agent row and the
// hub-row check are read under the lock, so the copy never uses stale inline
@ -216,7 +255,7 @@ export class SkillsHubStartupCopy {
}
await this.skillRepository.replaceDependencies(agent.id, refs, ctx);
}
return counts;
return { ...counts, publishedVersions: versions.length, draft: copyDraft };
});
}

View File

@ -70,7 +70,7 @@ async function readSourceRecords() {
}
beforeAll(async () => {
await setSkillsHubFlag(true);
setSkillsHubFlag(true);
await testModules.loadModules(['agents']);
await testDb.init();
agentRepository = Container.get(AgentRepository);

View File

@ -58,7 +58,7 @@ describe('Agent skills in the skills hub', () => {
const broadcaster = mockInstance(AgentUpdateBroadcaster);
beforeAll(async () => {
await setSkillsHubFlag(true);
setSkillsHubFlag(true);
mockInstance(Telemetry);
mockInstance(AgentRuntimeCacheService);
mockInstance(AgentTaskService).requestReconcile.mockResolvedValue(undefined);
@ -291,19 +291,19 @@ describe('Agent skills in the skills hub', () => {
await saveInstructions(skill, 'Newer text.');
const version = published.activeVersion!;
await setSkillsHubFlag(false);
setSkillsHubFlag(false);
try {
// With the flag off, the legacy reader uses the columns that publish wrote.
const { skillRefs: refs, skills } = await skillRefs.readVersion(version, {});
expect(refs).toEqual([{ type: 'skill', id: skill }]);
expect(await instructionsOf(skills)).toEqual({ [skill]: 'Published text.' });
} finally {
await setSkillsHubFlag(true);
setSkillsHubFlag(true);
}
});
it('keeps skills on the agent row when the skills hub is off', async () => {
await setSkillsHubFlag(false);
setSkillsHubFlag(false);
try {
const agent = await createAgent([]);
const context = { user: editor, modifiedBy: 'builder' as const };
@ -335,7 +335,7 @@ describe('Agent skills in the skills hub', () => {
expect(await Container.get(SkillRepository).findByIds([created.id])).toEqual([]);
expect(await skillRefs.refsForDraft(row, {})).toEqual([{ type: 'skill', id: created.id }]);
} finally {
await setSkillsHubFlag(true);
setSkillsHubFlag(true);
}
});

View File

@ -1,7 +1,9 @@
import type { AgentSkill } from '@n8n/api-types';
import { createTeamProject, mockInstance, testDb, testModules } from '@n8n/backend-test-utils';
import { Logger } from '@n8n/backend-common';
import type { Project } from '@n8n/db';
import { AgentsConfig } from '@n8n/config';
import { TransactionRunner, type Project } from '@n8n/db';
import { DataSource } from '@n8n/typeorm';
import { Container } from '@n8n/di';
import { InstanceSettings } from 'n8n-core';
import { randomUUID } from 'node:crypto';
@ -11,9 +13,15 @@ import { AgentRuntimeCacheService } from '@/modules/agents/agent-runtime-cache.s
import { AgentSkillRefsService } from '@/modules/agents/agent-skill-refs.service';
import { AgentUpdateBroadcaster } from '@/modules/agents/agent-update-broadcaster';
import type { Agent } from '@/modules/agents/entities/agent.entity';
import { AgentSkillDependency } from '@/modules/agents/entities/agent-skill-dependency.entity';
import { AgentVersionSkillDependency } from '@/modules/agents/entities/agent-version-skill-dependency.entity';
import { SkillFile } from '@/modules/agents/entities/skill-file.entity';
import { SkillVersion } from '@/modules/agents/entities/skill-version.entity';
import { Skill } from '@/modules/agents/entities/skill.entity';
import { AgentHistoryRepository } from '@/modules/agents/repositories/agent-history.repository';
import { AgentRepository } from '@/modules/agents/repositories/agent.repository';
import { SkillRepository } from '@/modules/agents/repositories/skill.repository';
import { SkillsHubRollout } from '@/modules/agents/skills/skills-hub-rollout';
import { SkillsHubStartupCopy } from '@/modules/agents/skills/skills-hub-startup-copy';
import { Telemetry } from '@/telemetry';
@ -48,7 +56,7 @@ describe('SkillsHubStartupCopy', () => {
beforeEach(async () => {
project = await createTeamProject();
await setSkillsHubFlag(true);
setSkillsHubFlag(true);
});
afterEach(async () => {
@ -169,6 +177,44 @@ describe('SkillsHubStartupCopy', () => {
expect(await skills.findAgentSkillVersions(agent.id)).toEqual(before);
});
it('logs the start, each copied agent and a summary with scanned and skipped agents', async () => {
const logger = Container.get(Logger);
const info = vi.spyOn(logger, 'info');
const debug = vi.spyOn(logger, 'debug');
const agent = await legacyAgent([{ type: 'skill', id: 'tone' }], { tone: body('Tone') });
await legacyVersion(agent, [{ type: 'skill', id: 'tone' }], { tone: body('Tone') });
await copy.copyAll();
await copy.copyAll();
expect(info).toHaveBeenCalledWith('Started copying inline agent skills into the skills hub');
const copiedLogs = debug.mock.calls.filter(
([message]) => message === 'Copied the inline skills of an agent into the skills hub',
);
expect(copiedLogs).toEqual([
[
expect.any(String),
expect.objectContaining({ agentId: agent.id, publishedVersions: 1, draft: true }),
],
]);
const summaries = info.mock.calls.filter(
([message]) => message === 'Copied inline agent skills into the skills hub',
);
expect(summaries).toEqual([
[
expect.any(String),
expect.objectContaining({
scanned: 1,
skipped: 0,
agents: 1,
durationMs: expect.any(Number),
}),
],
[expect.any(String), expect.objectContaining({ scanned: 1, skipped: 1, agents: 0 })],
]);
vi.restoreAllMocks();
});
it('does not copy a ref that was switched off', async () => {
const agent = await legacyAgent(
[
@ -260,6 +306,122 @@ describe('SkillsHubStartupCopy', () => {
);
}
describe('restarts', () => {
/**
* Boots the agents module again on the same database, as a restart of the process
* does: the setting is read into a new rollout and a new startup copy runs. Only the
* database carries over, so nothing in memory can stop a second copy.
*/
async function restart(skillsHubEnabled: boolean) {
const config = Container.get(AgentsConfig);
config.skillsHubEnabled = skillsHubEnabled;
const rollout = new SkillsHubRollout(config, Container.get(Logger));
rollout.init();
// The services that read agent skills follow the same setting after the restart.
setSkillsHubFlag(skillsHubEnabled);
const freshCopy = new SkillsHubStartupCopy(
Container.get(Logger),
rollout,
Container.get(InstanceSettings),
agents,
skills,
Container.get(TransactionRunner),
);
await freshCopy.runOnStartup();
return freshCopy;
}
/** Row counts of every table the copy writes. */
async function hubState() {
const manager = Container.get(DataSource).manager;
return {
skills: await manager.count(Skill),
versions: await manager.count(SkillVersion),
files: await manager.count(SkillFile),
draftRefs: await manager.count(AgentSkillDependency),
pins: await manager.count(AgentVersionSkillDependency),
};
}
async function agentWithPublishedSkills() {
const refs: InlineRef[] = [
{ type: 'skill', id: 'tone' },
{ type: 'skill', id: 'pricing' },
];
const agent = await legacyAgent(refs, {
tone: body('Tone', 'Draft tone.'),
pricing: { ...body('Pricing'), references: [{ path: 'references/a.md', content: 'A' }] },
});
await legacyVersion(agent, refs, {
tone: body('Tone', 'Published tone.'),
pricing: body('Pricing'),
});
return agent;
}
it('copies on the first start and writes nothing on later restarts', async () => {
const agent = await agentWithPublishedSkills();
await restart(true);
const afterFirst = await hubState();
await restart(true);
await restart(true);
expect(afterFirst).toMatchObject({ skills: 2, draftRefs: 2, pins: 2 });
expect(await hubState()).toEqual(afterFirst);
expect(instructionsOf((await skillRefs.readDraft(agent, {})).skills)).toEqual({
tone: 'Draft tone.',
pricing: 'Pricing instructions.',
});
});
it('keeps hub edits and detached skills over a restart', async () => {
const agent = await agentWithPublishedSkills();
await restart(true);
await editInHub('tone', 'Edited in the hub.');
await skillRefs.replaceDraftRefs(agent, [{ type: 'skill', id: 'tone' }], {});
const beforeRestart = await hubState();
await restart(true);
expect(await hubState()).toEqual(beforeRestart);
expect(await skillRefs.refsForDraft(agent, {})).toEqual([{ type: 'skill', id: 'tone' }]);
expect(instructionsOf((await skillRefs.readDraft(agent, {})).skills)).toEqual({
tone: 'Edited in the hub.',
});
});
it('does not copy again when the setting goes off and back on', async () => {
const agent = await agentWithPublishedSkills();
await restart(true);
const afterFirst = await hubState();
await restart(false);
// With the setting off, agents write their inline columns again.
await agents.update(
{ id: agent.id },
{ skills: { tone: body('Tone', 'Inline edit.') } as never },
);
await restart(true);
expect(await hubState()).toEqual(afterFirst);
// The agent has hub rows, so the copy leaves it alone: inline edits are not merged.
expect(instructionsOf((await skillRefs.readDraft(agent, {})).skills)).toEqual({
tone: 'Draft tone.',
pricing: 'Pricing instructions.',
});
});
it('copies each agent once when two mains start at the same time', async () => {
await agentWithPublishedSkills();
await agentWithPublishedSkills();
await Promise.all([restart(true), restart(true)]);
expect(await hubState()).toMatchObject({ skills: 4, draftRefs: 4, pins: 4 });
});
});
describe('with the flag off', () => {
beforeEach(() => {
vi.spyOn(Container.get(Logger), 'warn');
@ -276,7 +438,7 @@ describe('SkillsHubStartupCopy', () => {
it('copies nothing and reads only the draft refs and their content', async () => {
await legacyAgent([{ type: 'skill', id: 'tone' }], { tone: body('Tone') });
await setSkillsHubFlag(false);
setSkillsHubFlag(false);
const copyReads = [
vi.spyOn(agents, 'findPageForSkillCopy'),
vi.spyOn(skills, 'findUnpinnedVersions'),
@ -298,7 +460,7 @@ describe('SkillsHubStartupCopy', () => {
await legacyAgent([{ type: 'skill', id: 'tone' }], { tone: body('Tone') });
await copy.copyAll();
await editInHub('tone', 'Edited in the hub.');
await setSkillsHubFlag(false);
setSkillsHubFlag(false);
const findRefs = vi.spyOn(skills, 'findAgentIdsWithDraftRefs');
const settings = Container.get(InstanceSettings);
settings.markAsFollower();
@ -315,7 +477,7 @@ describe('SkillsHubStartupCopy', () => {
it('names no agent when the hub runs the same content as the inline skills', async () => {
await legacyAgent([{ type: 'skill', id: 'tone' }], { tone: body('Tone') });
await copy.copyAll();
await setSkillsHubFlag(false);
setSkillsHubFlag(false);
await copy.runOnStartup();
@ -334,7 +496,7 @@ describe('SkillsHubStartupCopy', () => {
await copy.copyAll();
await editInHub('tone', 'Edited in the hub.');
await editInHub('pricing', 'Edited in the hub.');
await setSkillsHubFlag(false);
setSkillsHubFlag(false);
await copy.runOnStartup();
@ -354,7 +516,7 @@ describe('SkillsHubStartupCopy', () => {
{ name: 'Added', description: 'd', instructions: 'New.', frontmatter: null, files: [] },
);
await skills.replaceDependencies(agent.id, [{ skillId: 'tone' }, { skillId: 'added' }]);
await setSkillsHubFlag(false);
setSkillsHubFlag(false);
await copy.runOnStartup();
@ -826,7 +988,7 @@ describe('SkillsHubStartupCopy', () => {
}
await copy.copyAll();
for (let index = 0; index < 22; index++) await editInHub(`s${index}`, 'Edited in the hub.');
await setSkillsHubFlag(false);
setSkillsHubFlag(false);
const warn = vi.spyOn(Container.get(Logger), 'warn');
await copy.runOnStartup();

View File

@ -45,11 +45,11 @@ describe('SkillsController', () => {
});
beforeEach(async () => {
await setSkillsHubFlag(true);
setSkillsHubFlag(true);
});
it('hides the skills API when the skills hub is off', async () => {
await setSkillsHubFlag(false);
setSkillsHubFlag(false);
await owner.get('/skills').expect(404);
await owner.post('/skills').send({ scope: 'instance', skill: body }).expect(404);

View File

@ -83,7 +83,7 @@ describe('AgentRepository', () => {
afterEach(async () => {
// The rollout keeps the flag it read, so set it back off for the next test.
await setSkillsHubFlag(false);
setSkillsHubFlag(false);
vi.restoreAllMocks();
await agentHistoryRepo.delete({});
await agentRepo.delete({});
@ -221,7 +221,7 @@ describe('AgentRepository', () => {
});
it('replaces the complete draft and preserves existing task creation times', async () => {
await setSkillsHubFlag(true);
setSkillsHubFlag(true);
const agent = await createAgent();
await Container.get(SkillRepository).createSkill(
{

View File

@ -51,7 +51,7 @@ let workflow: WorkflowEntity;
let repository: AgentRepository;
beforeAll(async () => {
await setSkillsHubFlag(true);
setSkillsHubFlag(true);
repository = Container.get(AgentRepository);
Container.get(InstanceSettings).markAsLeader();
});