Download packages/node-sdk/test/session-skills.test.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 12.9 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/node-sdk/test/session-skills.test.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/node-sdk/test/session-skills.test.ts
-
curl -L -o session-skills.test.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/node-sdk/test/session-skills.test.ts
12.9 kB
| import { mkdir, writeFile } from 'node:fs/promises'; | |
| import { join } from 'node:path'; | |
| import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest'; | |
| import { | |
| createKimiHarness, | |
| type Event, | |
| type KimiError, | |
| type SkillActivatedEvent, | |
| type SkillSummary, | |
| } from '#/index'; | |
| import type { SDKRpcClientBase } from '#/rpc'; | |
| import { | |
| makeTempDir, | |
| removeTempDirs, | |
| waitForSDKEvent, | |
| } from './session-runtime-helpers'; | |
| import { TEST_IDENTITY } from './test-identity'; | |
| const { Session } = await import('#/index'); | |
| const tempDirs: string[] = []; | |
| const CONFIG_ENV_PATTERN = | |
| /^(KIMI_MODEL_|KIMI_LOOP_|KIMI_MCP_|KIMI_WEB_|KIMI_IMAGE_|KIMI_CODE_BACKGROUND_|KIMI_CODE_MODEL_CATALOG_)/; | |
| /** Keep ambient env from injecting providers/models into the v2 engine. */ | |
| function scrubConfigEnv(): () => void { | |
| const saved: Record<string, string> = {}; | |
| for (const [key, value] of Object.entries(process.env)) { | |
| if (value !== undefined && CONFIG_ENV_PATTERN.test(key)) { | |
| saved[key] = value; | |
| delete process.env[key]; | |
| } | |
| } | |
| return () => { | |
| for (const [key, value] of Object.entries(saved)) { | |
| process.env[key] = value; | |
| } | |
| }; | |
| } | |
| afterEach(async () => { | |
| await removeTempDirs(tempDirs); | |
| vi.unstubAllEnvs(); | |
| }); | |
| describe('Session skills', () => { | |
| it('submits multiple skills with a prompt as one grouped turn', async () => { | |
| const restoreEnv = scrubConfigEnv(); | |
| const homeDir = await makeTempDir(tempDirs, 'kimi-sdk-skills-home-'); | |
| const workDir = await makeTempDir(tempDirs, 'kimi-sdk-skills-work-'); | |
| await writeSkill(workDir, 'review', [ | |
| '---', | |
| 'name: review', | |
| 'description: Review code', | |
| '---', | |
| '', | |
| 'Review the requested file.', | |
| ]); | |
| await writeSkill(workDir, 'security', [ | |
| '---', | |
| 'name: security', | |
| 'description: Check security', | |
| '---', | |
| '', | |
| 'Check the requested file for security issues.', | |
| ]); | |
| const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY }); | |
| try { | |
| const session = await harness.createSession({ id: 'ses_sdk_multi_skill', workDir }); | |
| const events: Event[] = []; | |
| const unsubscribe = session.onEvent((event) => { | |
| events.push(event); | |
| }); | |
| const ended = waitForSDKEvent(session, (event) => event.type === 'turn.ended'); | |
| await session.promptWithSkills( | |
| 'Review this change.', | |
| [{ name: 'review' }, { name: 'security' }], | |
| ); | |
| await ended; | |
| unsubscribe(); | |
| const activations = events.filter( | |
| (event): event is Extract<Event, { type: 'skill.activated' }> => | |
| event.type === 'skill.activated', | |
| ); | |
| expect(activations.map((event) => event.skillName)).toEqual(['review', 'security']); | |
| expect(events.filter((event) => event.type === 'turn.started')).toHaveLength(1); | |
| } finally { | |
| await harness.close(); | |
| restoreEnv(); | |
| } | |
| }); | |
| it('lists session skills without exposing content', async () => { | |
| const homeDir = await makeTempDir(tempDirs, 'kimi-sdk-skills-home-'); | |
| const workDir = await makeTempDir(tempDirs, 'kimi-sdk-skills-work-'); | |
| await writeSkill(workDir, 'review', [ | |
| '---', | |
| 'name: review', | |
| 'description: Review code', | |
| 'disable_model_invocation: true', | |
| '---', | |
| '', | |
| 'Review the requested file.', | |
| ]); | |
| const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY }); | |
| try { | |
| const session = await harness.createSession({ id: 'ses_sdk_skill_list', workDir }); | |
| const skills = await session.listSkills(); | |
| const listed = skills.find((skill) => skill.name === 'review'); | |
| expect(listed).toMatchObject({ | |
| name: 'review', | |
| description: 'Review code', | |
| source: 'project', | |
| disableModelInvocation: true, | |
| }); | |
| expect(listed?.path.endsWith('/.kimi-code/skills/review/SKILL.md')).toBe(true); | |
| expect(JSON.stringify(skills)).not.toContain('Review the requested file.'); | |
| } finally { | |
| await harness.close(); | |
| } | |
| }); | |
| it('activates a skill through core and emits the public skill event', async () => { | |
| const homeDir = await makeTempDir(tempDirs, 'kimi-sdk-skills-home-'); | |
| const workDir = await makeTempDir(tempDirs, 'kimi-sdk-skills-work-'); | |
| await writeSkill(workDir, 'review', [ | |
| '---', | |
| 'name: review', | |
| 'description: Review code', | |
| '---', | |
| '', | |
| 'Review the requested file.', | |
| ]); | |
| const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY }); | |
| try { | |
| const session = await harness.createSession({ id: 'ses_sdk_skill_activate', workDir }); | |
| const events: Event[] = []; | |
| const unsubscribe = session.onEvent((event) => { | |
| events.push(event); | |
| }); | |
| const activated = waitForSDKEvent(session, (event) => event.type === 'skill.activated'); | |
| const metaUpdated = waitForSDKEvent( | |
| session, | |
| (event) => event.type === 'session.meta.updated', | |
| ); | |
| const ended = waitForSDKEvent(session, (event) => event.type === 'turn.ended'); | |
| await session.activateSkill(' review ', ' src/app.ts '); | |
| const activatedEvent = await activated; | |
| const metaEvent = await metaUpdated; | |
| await ended; | |
| unsubscribe(); | |
| expect(activatedEvent).toMatchObject({ | |
| type: 'skill.activated', | |
| sessionId: session.id, | |
| agentId: 'main', | |
| skillName: 'review', | |
| skillArgs: 'src/app.ts', | |
| trigger: 'user-slash', | |
| skillSource: 'project', | |
| }); | |
| expect(JSON.stringify(activatedEvent)).not.toContain('Review the requested file.'); | |
| expect(events.findIndex((event) => event.type === 'skill.activated')).toBeGreaterThanOrEqual( | |
| 0, | |
| ); | |
| expect(events.findIndex((event) => event.type === 'turn.started')).toBeGreaterThan( | |
| events.findIndex((event) => event.type === 'skill.activated'), | |
| ); | |
| expect(metaEvent).toMatchObject({ | |
| type: 'session.meta.updated', | |
| sessionId: session.id, | |
| agentId: 'main', | |
| title: '/review src/app.ts', | |
| patch: { | |
| title: '/review src/app.ts', | |
| isCustomTitle: false, | |
| lastPrompt: '/review src/app.ts', | |
| }, | |
| }); | |
| } finally { | |
| await harness.close(); | |
| } | |
| }); | |
| it('resolves user brand skills from KIMI_CODE_HOME, not the OS home', async () => { | |
| const homeDir = await makeTempDir(tempDirs, 'kimi-sdk-skills-home-'); | |
| const processHome = await makeTempDir(tempDirs, 'kimi-sdk-skills-process-home-'); | |
| const workDir = await makeTempDir(tempDirs, 'kimi-sdk-skills-work-'); | |
| vi.stubEnv('HOME', processHome); | |
| vi.stubEnv('KIMI_CODE_HOME', homeDir); | |
| await writeLegacyUserSkill(processHome, 'sdk-real-home-only', 'SDK real home skill'); | |
| await writeBrandUserSkill(homeDir, 'sdk-sandbox-only', 'SDK sandbox skill'); | |
| const harness = createKimiHarness({ identity: TEST_IDENTITY }); | |
| try { | |
| const session = await harness.createSession({ id: 'ses_sdk_skill_env_home', workDir }); | |
| const names = new Set((await session.listSkills()).map((skill) => skill.name)); | |
| expect(names.has('sdk-real-home-only')).toBe(false); | |
| expect(names.has('sdk-sandbox-only')).toBe(true); | |
| } finally { | |
| await harness.close(); | |
| } | |
| }); | |
| it('rejects empty names before calling RPC and rejects after close', async () => { | |
| const activateSkill = vi.fn(async () => {}); | |
| const closeSession = vi.fn(async (_input: { readonly sessionId: string }) => {}); | |
| const clearSessionHandlers = vi.fn(); | |
| const listSkills = vi.fn(async () => []); | |
| const session = new Session({ | |
| id: 'ses_skill_validation', | |
| workDir: '/tmp/work', | |
| rpc: { | |
| activateSkill, | |
| closeSession, | |
| clearSessionHandlers, | |
| listSkills, | |
| } as unknown as SDKRpcClientBase, | |
| }); | |
| await expect(session.activateSkill(' ')).rejects.toMatchObject({ | |
| name: 'KimiError', | |
| code: 'skill.name_empty', | |
| } satisfies Partial<KimiError>); | |
| expect(activateSkill).not.toHaveBeenCalled(); | |
| await session.close(); | |
| expect(closeSession).toHaveBeenCalledWith({ sessionId: session.id }); | |
| expect(clearSessionHandlers).toHaveBeenCalledWith(session.id); | |
| await expect(session.listSkills()).rejects.toMatchObject({ | |
| name: 'KimiError', | |
| code: 'session.closed', | |
| } satisfies Partial<KimiError>); | |
| await expect(session.activateSkill('review')).rejects.toMatchObject({ | |
| name: 'KimiError', | |
| code: 'session.closed', | |
| } satisfies Partial<KimiError>); | |
| }); | |
| it('finalizes local close state when the core close RPC fails', async () => { | |
| const closeSession = vi.fn(async (_input: { readonly sessionId: string }) => { | |
| throw new Error('flush failed'); | |
| }); | |
| const clearSessionHandlers = vi.fn(); | |
| const listSkills = vi.fn(async () => []); | |
| const activateSkill = vi.fn(async () => {}); | |
| const session = new Session({ | |
| id: 'ses_close_failed', | |
| workDir: '/tmp/work', | |
| rpc: { | |
| activateSkill, | |
| closeSession, | |
| clearSessionHandlers, | |
| listSkills, | |
| } as unknown as SDKRpcClientBase, | |
| }); | |
| await expect(session.close()).rejects.toThrow('flush failed'); | |
| await expect(session.close()).resolves.toBeUndefined(); | |
| expect(closeSession).toHaveBeenCalledTimes(1); | |
| expect(clearSessionHandlers).toHaveBeenCalledWith(session.id); | |
| await expect(session.listSkills()).rejects.toMatchObject({ | |
| name: 'KimiError', | |
| code: 'session.closed', | |
| } satisfies Partial<KimiError>); | |
| }); | |
| it('exposes public skill event and summary types', () => { | |
| expectTypeOf<SkillSummary['name']>().toEqualTypeOf<string>(); | |
| expectTypeOf<SkillActivatedEvent['skillName']>().toEqualTypeOf<string>(); | |
| }); | |
| }); | |
| describe('KimiHarness workspace skills', () => { | |
| it('returns project skills when no session exists', async () => { | |
| const homeDir = await makeTempDir(tempDirs, 'kimi-sdk-workspace-skills-home-'); | |
| const workDir = await makeTempDir(tempDirs, 'kimi-sdk-workspace-skills-work-'); | |
| await writeSkill(workDir, 'workspace-review', [ | |
| '---', | |
| 'name: workspace-review', | |
| 'description: Review workspace changes', | |
| '---', | |
| '', | |
| 'Inspect every changed file.', | |
| ]); | |
| const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY }); | |
| try { | |
| const skills = await harness.listWorkspaceSkills(workDir); | |
| expect(skills.find((skill) => skill.name === 'workspace-review')).toMatchObject({ | |
| name: 'workspace-review', | |
| description: 'Review workspace changes', | |
| source: 'project', | |
| }); | |
| expect(harness.sessions.size).toBe(0); | |
| } finally { | |
| await harness.close(); | |
| } | |
| }); | |
| it('preserves the core error when workDir is empty', async () => { | |
| const homeDir = await makeTempDir(tempDirs, 'kimi-sdk-workspace-skills-home-'); | |
| const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY }); | |
| try { | |
| await expect(harness.listWorkspaceSkills(' ')).rejects.toMatchObject({ | |
| name: 'KimiError', | |
| code: 'request.work_dir_required', | |
| message: 'listWorkspaceSkills requires workDir', | |
| } satisfies Partial<KimiError>); | |
| } finally { | |
| await harness.close(); | |
| } | |
| }); | |
| it('preserves the core error when workDir is not a string', async () => { | |
| const homeDir = await makeTempDir(tempDirs, 'kimi-sdk-workspace-skills-home-'); | |
| const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY }); | |
| try { | |
| await expect(harness.listWorkspaceSkills(null as never)).rejects.toMatchObject({ | |
| name: 'KimiError', | |
| code: 'request.work_dir_required', | |
| message: 'listWorkspaceSkills requires workDir', | |
| } satisfies Partial<KimiError>); | |
| } finally { | |
| await harness.close(); | |
| } | |
| }); | |
| }); | |
| async function writeSkill(workDir: string, name: string, lines: readonly string[]): Promise<void> { | |
| const dir = join(workDir, '.kimi-code', 'skills', name); | |
| await mkdir(dir, { recursive: true }); | |
| await writeFile(join(dir, 'SKILL.md'), lines.join('\n')); | |
| } | |
| async function writeLegacyUserSkill( | |
| userHomeDir: string, | |
| name: string, | |
| description: string, | |
| ): Promise<void> { | |
| await writeSkillFile(join(userHomeDir, '.kimi-code', 'skills', name), name, description); | |
| } | |
| async function writeBrandUserSkill( | |
| brandHomeDir: string, | |
| name: string, | |
| description: string, | |
| ): Promise<void> { | |
| await writeSkillFile(join(brandHomeDir, 'skills', name), name, description); | |
| } | |
| async function writeSkillFile(dir: string, name: string, description: string): Promise<void> { | |
| await mkdir(dir, { recursive: true }); | |
| await writeFile( | |
| join(dir, 'SKILL.md'), | |
| ['---', `name: ${name}`, `description: ${description}`, '---', '', `${description}.`].join( | |
| '\n', | |
| ), | |
| ); | |
| } | |