Download packages/agent-core-v2/test/features/skill/skill.test.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/agent-core-v2/test/features/skill/skill.test.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/agent-core-v2/test/features/skill/skill.test.ts
-
curl -L -o skill.test.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/agent-core-v2/test/features/skill/skill.test.ts
10.7 kB
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | |
| import { createControlledPromise } from '@antfu/utils'; | |
| import { InMemorySkillCatalog } from '#/features/skill/catalog/registry'; | |
| import { summarizeSkill } from '#/features/skill/catalog/types'; | |
| import { IAgentSkillService } from '#/features/skill/skillService'; | |
| import type { LlmRequester } from '#human/llm/requester/requester'; | |
| import { | |
| ISkillTool, | |
| MAX_SKILL_QUERY_DEPTH, | |
| NestedSkillTooDeepError, | |
| SkillToolInputSchema, | |
| } from '#/features/skill/tools/skill'; | |
| import { SkillTool } from '#/features/skill/tools/skillTool'; | |
| import { executeTool } from '../../tools/fixtures/execute-tool'; | |
| import { stubSkill } from './catalog/stubs'; | |
| import { | |
| createTestAgent, | |
| InMemoryWireRecordPersistence, | |
| skillServices, | |
| type TestAgentContext, | |
| } from '../../harness'; | |
| type GenerateFn = LlmRequester; | |
| const COMMIT_SKILL = stubSkill('commit', { | |
| description: 'commit changes', | |
| path: '/skills/commit/SKILL.md', | |
| dir: '/skills/commit', | |
| content: '# Commit', | |
| metadata: {}, | |
| source: 'user', | |
| }); | |
| function catalogWithCommit(): InMemorySkillCatalog { | |
| const skills = new InMemorySkillCatalog(); | |
| skills.register(COMMIT_SKILL); | |
| return skills; | |
| } | |
| describe('AgentSkillService', () => { | |
| let ctx: TestAgentContext; | |
| afterEach(async () => { | |
| await ctx.dispose(); | |
| }); | |
| it('activate prompts with the rendered skill for a known skill', async () => { | |
| ctx = createTestAgent(skillServices(catalogWithCommit())); | |
| ctx.mockNextResponse({ type: 'text', text: 'done' }); | |
| const launched = await ctx.get(IAgentSkillService).activate({ name: 'commit' }); | |
| expect(launched.turn_id).toBe(0); | |
| await ctx.untilTurnEnd(); | |
| const activation = ctx.context.get().find((m) => m.origin?.kind === 'skill_activation'); | |
| expect(activation?.role).toBe('user'); | |
| expect(activation?.origin).toMatchObject({ | |
| kind: 'skill_activation', | |
| skillName: 'commit', | |
| }); | |
| }); | |
| it('activate throws for an unknown skill', async () => { | |
| ctx = createTestAgent(skillServices(catalogWithCommit())); | |
| await expect(ctx.get(IAgentSkillService).activate({ name: 'missing' })).rejects.toThrow( | |
| /not found/i, | |
| ); | |
| }); | |
| it('activate waits for the catalog to be ready before resolving', async () => { | |
| let resolveReady!: () => void; | |
| const ready = new Promise<void>((resolve) => { | |
| resolveReady = resolve; | |
| }); | |
| const skills = catalogWithCommit(); | |
| ctx = createTestAgent( | |
| skillServices({ | |
| _serviceBrand: undefined, | |
| catalog: skills, | |
| ready, | |
| onDidChange: () => ({ dispose: () => {} }), | |
| load: async () => {}, | |
| reload: async () => {}, | |
| list: async () => skills.listSkills().map(summarizeSkill), | |
| }), | |
| ); | |
| ctx.mockNextResponse({ type: 'text', text: 'done' }); | |
| let finished = false; | |
| const activation = ctx | |
| .get(IAgentSkillService) | |
| .activate({ name: 'commit' }) | |
| .then(() => { | |
| finished = true; | |
| }); | |
| await Promise.resolve(); | |
| expect(finished).toBe(false); | |
| resolveReady(); | |
| await activation; | |
| expect(finished).toBe(true); | |
| await ctx.untilTurnEnd(); | |
| expect(ctx.context.get().some((m) => m.origin?.kind === 'skill_activation')).toBe(true); | |
| }); | |
| }); | |
| describe('SkillTool', () => { | |
| let ctx: TestAgentContext; | |
| let skills: InMemorySkillCatalog; | |
| beforeEach(() => { | |
| skills = catalogWithCommit(); | |
| ctx = createTestAgent(skillServices(skills)); | |
| }); | |
| afterEach(async () => { | |
| await ctx.dispose(); | |
| }); | |
| function toolContext(args: { readonly skill: string; readonly args?: string }) { | |
| return { | |
| turnId: 0, | |
| toolCallId: 'call_skill', | |
| args, | |
| signal: new AbortController().signal, | |
| }; | |
| } | |
| function makeTool(depth?: number): SkillTool { | |
| const tool = ctx.get(ISkillTool) as SkillTool; | |
| return depth === undefined ? tool : tool.withInitialQueryDepth(depth); | |
| } | |
| it('exposes metadata and schema for model-invoked skills', () => { | |
| const tool = makeTool(); | |
| expect(tool.name).toBe('Skill'); | |
| expect(tool.parameters).toMatchObject({ | |
| type: 'object', | |
| required: ['skill'], | |
| additionalProperties: false, | |
| properties: { | |
| skill: { type: 'string' }, | |
| args: { type: 'string' }, | |
| }, | |
| }); | |
| expect(SkillToolInputSchema.safeParse({ skill: 'commit' }).success).toBe(true); | |
| expect(SkillToolInputSchema.safeParse({ skill: 'commit', args: '-m fix' }).success).toBe(true); | |
| expect(SkillToolInputSchema.safeParse({}).success).toBe(false); | |
| }); | |
| it('returns a tool error when the skill is unknown', async () => { | |
| const result = await executeTool( | |
| makeTool(), | |
| toolContext({ skill: 'missing' }), | |
| ); | |
| expect(result).toMatchObject({ | |
| isError: true, | |
| output: 'Skill "missing" not found in the current skill listing.', | |
| }); | |
| }); | |
| it('rejects skills that disable model invocation', async () => { | |
| skills.register(stubSkill('private', { metadata: { disableModelInvocation: true } })); | |
| const result = await executeTool( | |
| makeTool(), | |
| toolContext({ skill: 'private' }), | |
| ); | |
| expect(result).toMatchObject({ | |
| isError: true, | |
| output: 'Skill "private" can only be triggered by the user (model invocation is disabled).', | |
| }); | |
| }); | |
| it('rejects non-inline skill types in the current v1 runtime', async () => { | |
| skills.register(stubSkill('flow-only', { metadata: { type: 'flow' } })); | |
| const result = await executeTool( | |
| makeTool(), | |
| toolContext({ skill: 'flow-only' }), | |
| ); | |
| expect(result).toMatchObject({ | |
| isError: true, | |
| output: 'Skill "flow-only" is not an inline skill and cannot be invoked by the model in v1.', | |
| }); | |
| }); | |
| it('loads inline skills through the model-tool wrapper without exposing the body in output', async () => { | |
| const result = await executeTool( | |
| makeTool(), | |
| toolContext({ skill: 'commit', args: 'src/app.ts' }), | |
| ); | |
| expect(result).toMatchObject({ | |
| output: 'Skill "commit" loaded inline. Follow its instructions.', | |
| }); | |
| expect(result.output).not.toContain('# Commit'); | |
| expect(ctx.context.get()).toHaveLength(0); | |
| expect(result.delivery?.kind).toBe('steer'); | |
| expect(result.delivery?.message.origin).toMatchObject({ | |
| kind: 'skill_activation', | |
| skillName: 'commit', | |
| trigger: 'model-tool', | |
| }); | |
| expect(result.delivery?.message.content[0]).toMatchObject({ | |
| type: 'text', | |
| text: expect.stringContaining( | |
| '<skill-loaded name="commit" trigger="model-tool" source="user" dir="/skills/commit" args="src/app.ts">', | |
| ), | |
| }); | |
| expect(result.delivery?.message.content[0]).toMatchObject({ | |
| type: 'text', | |
| text: expect.stringContaining('ARGUMENTS: src/app.ts'), | |
| }); | |
| }); | |
| it('honors initialQueryDepth as an alias for queryDepth', async () => { | |
| const nested = await executeTool( | |
| makeTool(2), | |
| toolContext({ skill: 'commit' }), | |
| ); | |
| const root = await executeTool( | |
| makeTool(0), | |
| toolContext({ skill: 'commit' }), | |
| ); | |
| expect(ctx.context.get()).toHaveLength(0); | |
| expect(nested.delivery?.message.origin).toMatchObject({ | |
| kind: 'skill_activation', | |
| trigger: 'nested-skill', | |
| }); | |
| expect(root.delivery?.message.origin).toMatchObject({ | |
| kind: 'skill_activation', | |
| trigger: 'model-tool', | |
| }); | |
| }); | |
| it('throws a structured recursion error when nested skill invocation is too deep', async () => { | |
| await expect( | |
| executeTool( | |
| makeTool(MAX_SKILL_QUERY_DEPTH), | |
| toolContext({ skill: 'commit' }), | |
| ), | |
| ).rejects.toBeInstanceOf(NestedSkillTooDeepError); | |
| expect(ctx.context.get()).toHaveLength(0); | |
| }); | |
| }); | |
| describe('AgentSkillService busy delivery (harness)', () => { | |
| let ctx: TestAgentContext; | |
| afterEach(async () => { | |
| await ctx.dispose(); | |
| }); | |
| it('steers the activation into the running turn and launches a new one when idle', async () => { | |
| const catalog = new InMemorySkillCatalog(); | |
| catalog.register( | |
| stubSkill('tower', { | |
| content: 'Tower mission: $ARGUMENTS', | |
| metadata: {}, | |
| }), | |
| ); | |
| const gate = createControlledPromise<void>(); | |
| let generateCalls = 0; | |
| const generate: GenerateFn = { | |
| generate: async (_config, _content, control) => { | |
| generateCalls += 1; | |
| const n = generateCalls; | |
| control.onEvent?.({ type: 'llm.sent' }); | |
| if (n === 1) await gate; | |
| control.signal.throwIfAborted(); | |
| const text = `response-${String(n)}`; | |
| control.onEvent?.({ type: 'llm.streaming.part', part: { type: 'text', text } }); | |
| control.onEvent?.({ | |
| type: 'llm.streaming.usage', | |
| usage: { inputOther: 1, output: 1, inputCacheRead: 0, inputCacheCreation: 0 }, | |
| }); | |
| control.onEvent?.({ | |
| type: 'llm.streaming.finish', | |
| finish: { finishReason: 'completed', rawFinishReason: 'stop' }, | |
| }); | |
| control.onEvent?.({ type: 'llm.streaming.message_id', messageId: `mock-${String(n)}` }); | |
| control.onEvent?.({ type: 'llm.done' }); | |
| }, | |
| }; | |
| const persistence = new InMemoryWireRecordPersistence(); | |
| ctx = createTestAgent(skillServices(catalog), { generate, persistence }); | |
| const promptPromise = ctx.rpc.prompt({ input: [{ type: 'text', text: 'start' }] }); | |
| await vi.waitFor(() => { | |
| expect(generateCalls).toBe(1); | |
| }); | |
| const busyResult = await ctx.get(IAgentSkillService).activate({ name: 'tower', args: 'mission-1' }); | |
| expect(busyResult.turn_id).toBe(0); | |
| expect(generateCalls).toBe(1); | |
| gate.resolve(); | |
| await promptPromise; | |
| await ctx.untilTurnEnd(); | |
| const idleResult = await ctx.get(IAgentSkillService).activate({ name: 'tower', args: 'mission-2' }); | |
| expect(idleResult.turn_id).toBe(2); | |
| await ctx.untilTurnEnd(); | |
| expect(generateCalls).toBe(3); | |
| const activations = ctx | |
| .contextData() | |
| .history.filter((m) => m.role === 'user' && m.origin?.kind === 'skill_activation'); | |
| expect(activations.map((m) => (m.origin?.kind === 'skill_activation' ? m.origin.skillArgs : ''))).toEqual([ | |
| 'mission-1', | |
| 'mission-2', | |
| ]); | |
| const types = persistence.records.map((record) => record.type); | |
| expect(types.filter((type) => type === 'turn.prompt')).toHaveLength(2); | |
| expect(types.filter((type) => type === 'turn.steer')).toHaveLength(1); | |
| const steer = persistence.records.find((record) => record.type === 'turn.steer'); | |
| expect(steer).toMatchObject({ origin: { kind: 'skill_activation', skillArgs: 'mission-1' } }); | |
| }); | |
| }); | |