diff options
Diffstat (limited to 'packages/skills/src/skills.test.ts')
| -rw-r--r-- | packages/skills/src/skills.test.ts | 478 |
1 files changed, 239 insertions, 239 deletions
diff --git a/packages/skills/src/skills.test.ts b/packages/skills/src/skills.test.ts index fe0b437..57e5d63 100644 --- a/packages/skills/src/skills.test.ts +++ b/packages/skills/src/skills.test.ts @@ -8,261 +8,261 @@ import { createLoadSkillTool, scanSkillsDir } from "./load-skill.js"; import { makeSkillsToolFilter } from "./tools-filter.js"; function stubCtx(overrides?: Partial<ToolExecuteContext>): ToolExecuteContext { - return { - toolCallId: "test-call-1", - onOutput: () => {}, - signal: AbortSignal.timeout(5000), - log: createLogger( - { extensionId: "test" }, - { emit: () => {} }, - { now: () => 0, newId: () => "id" }, - ), - ...overrides, - }; + return { + toolCallId: "test-call-1", + onOutput: () => {}, + signal: AbortSignal.timeout(5000), + log: createLogger( + { extensionId: "test" }, + { emit: () => {} }, + { now: () => 0, newId: () => "id" }, + ), + ...overrides, + }; } let homeDir: string; let workdir: string; beforeEach(async () => { - homeDir = await mkdtemp(join(tmpdir(), "skills-home-test-")); - workdir = await mkdtemp(join(tmpdir(), "skills-workdir-test-")); + homeDir = await mkdtemp(join(tmpdir(), "skills-home-test-")); + workdir = await mkdtemp(join(tmpdir(), "skills-workdir-test-")); }); afterEach(async () => { - await rm(homeDir, { recursive: true, force: true }); - await rm(workdir, { recursive: true, force: true }); + await rm(homeDir, { recursive: true, force: true }); + await rm(workdir, { recursive: true, force: true }); }); describe("load_skill tool", () => { - it("loads a skill body (strips first two lines) from cwd .skills", async () => { - const skillsDir = join(workdir, ".skills"); - await mkdir(skillsDir); - await writeFile( - join(skillsDir, "web-search.md"), - "Use for web searches\n---\n# Web Search Skill\nDo a web search.", - "utf8", - ); - - const tool = createLoadSkillTool({ homeDir, workdir }); - const result = await tool.execute({ name: "web-search" }, stubCtx()); - - expect(result.isError).toBeUndefined(); - expect(result.content).toBe("# Web Search Skill\nDo a web search."); - }); - - it("falls back to home .skills when not in cwd", async () => { - const homeSkillsDir = join(homeDir, ".skills"); - await mkdir(homeSkillsDir); - await writeFile( - join(homeSkillsDir, "global-skill.md"), - "Global skill summary\n---\nGlobal body content", - "utf8", - ); - - const tool = createLoadSkillTool({ homeDir, workdir }); - const result = await tool.execute({ name: "global-skill" }, stubCtx()); - - expect(result.isError).toBeUndefined(); - expect(result.content).toBe("Global body content"); - }); - - it("returns isError for an unknown skill", async () => { - const tool = createLoadSkillTool({ homeDir, workdir }); - const result = await tool.execute({ name: "nonexistent" }, stubCtx()); - - expect(result.isError).toBe(true); - expect(result.content).toContain("unknown skill"); - }); - - it("rejects a name containing a path separator", async () => { - const tool = createLoadSkillTool({ homeDir, workdir }); - const result = await tool.execute({ name: "../escape" }, stubCtx()); - - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid skill name"); - }); - - it("rejects a name containing ..", async () => { - const tool = createLoadSkillTool({ homeDir, workdir }); - const result = await tool.execute({ name: "skill..evil" }, stubCtx()); - - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid skill name"); - }); - - it("rejects a name containing backslash", async () => { - const tool = createLoadSkillTool({ homeDir, workdir }); - const result = await tool.execute({ name: "path\\name" }, stubCtx()); - - expect(result.isError).toBe(true); - expect(result.content).toContain("Invalid skill name"); - }); - - it("returns the whole file when malformed (no --- on line 2)", async () => { - const skillsDir = join(workdir, ".skills"); - await mkdir(skillsDir); - await writeFile( - join(skillsDir, "malformed.md"), - "Just some content\nNo separator here\nMore content", - "utf8", - ); - - const tool = createLoadSkillTool({ homeDir, workdir }); - const result = await tool.execute({ name: "malformed" }, stubCtx()); - - expect(result.isError).toBeUndefined(); - expect(result.content).toBe("Just some content\nNo separator here\nMore content"); - }); - - it("cwd skill shadows home skill of the same name", async () => { - const homeSkillsDir = join(homeDir, ".skills"); - await mkdir(homeSkillsDir); - await writeFile(join(homeSkillsDir, "shared.md"), "Home summary\n---\nHome body", "utf8"); - - const cwdSkillsDir = join(workdir, ".skills"); - await mkdir(cwdSkillsDir); - await writeFile(join(cwdSkillsDir, "shared.md"), "Cwd summary\n---\nCwd body", "utf8"); - - const tool = createLoadSkillTool({ homeDir, workdir }); - const result = await tool.execute({ name: "shared" }, stubCtx()); - - expect(result.isError).toBeUndefined(); - expect(result.content).toBe("Cwd body"); - }); - - it("reads from ctx.cwd when set", async () => { - const ctxDir = await mkdtemp(join(tmpdir(), "skills-ctx-test-")); - try { - const ctxSkillsDir = join(ctxDir, ".skills"); - await mkdir(ctxSkillsDir); - await writeFile(join(ctxSkillsDir, "ctx-skill.md"), "Ctx summary\n---\nFrom ctx cwd", "utf8"); - - const tool = createLoadSkillTool({ homeDir, workdir }); - const result = await tool.execute({ name: "ctx-skill" }, stubCtx({ cwd: ctxDir })); - - expect(result.isError).toBeUndefined(); - expect(result.content).toBe("From ctx cwd"); - } finally { - await rm(ctxDir, { recursive: true, force: true }); - } - }); - - it("concurrencySafe is true", () => { - const tool = createLoadSkillTool({ homeDir, workdir }); - expect(tool.concurrencySafe).toBe(true); - }); + it("loads a skill body (strips first two lines) from cwd .skills", async () => { + const skillsDir = join(workdir, ".skills"); + await mkdir(skillsDir); + await writeFile( + join(skillsDir, "web-search.md"), + "Use for web searches\n---\n# Web Search Skill\nDo a web search.", + "utf8", + ); + + const tool = createLoadSkillTool({ homeDir, workdir }); + const result = await tool.execute({ name: "web-search" }, stubCtx()); + + expect(result.isError).toBeUndefined(); + expect(result.content).toBe("# Web Search Skill\nDo a web search."); + }); + + it("falls back to home .skills when not in cwd", async () => { + const homeSkillsDir = join(homeDir, ".skills"); + await mkdir(homeSkillsDir); + await writeFile( + join(homeSkillsDir, "global-skill.md"), + "Global skill summary\n---\nGlobal body content", + "utf8", + ); + + const tool = createLoadSkillTool({ homeDir, workdir }); + const result = await tool.execute({ name: "global-skill" }, stubCtx()); + + expect(result.isError).toBeUndefined(); + expect(result.content).toBe("Global body content"); + }); + + it("returns isError for an unknown skill", async () => { + const tool = createLoadSkillTool({ homeDir, workdir }); + const result = await tool.execute({ name: "nonexistent" }, stubCtx()); + + expect(result.isError).toBe(true); + expect(result.content).toContain("unknown skill"); + }); + + it("rejects a name containing a path separator", async () => { + const tool = createLoadSkillTool({ homeDir, workdir }); + const result = await tool.execute({ name: "../escape" }, stubCtx()); + + expect(result.isError).toBe(true); + expect(result.content).toContain("Invalid skill name"); + }); + + it("rejects a name containing ..", async () => { + const tool = createLoadSkillTool({ homeDir, workdir }); + const result = await tool.execute({ name: "skill..evil" }, stubCtx()); + + expect(result.isError).toBe(true); + expect(result.content).toContain("Invalid skill name"); + }); + + it("rejects a name containing backslash", async () => { + const tool = createLoadSkillTool({ homeDir, workdir }); + const result = await tool.execute({ name: "path\\name" }, stubCtx()); + + expect(result.isError).toBe(true); + expect(result.content).toContain("Invalid skill name"); + }); + + it("returns the whole file when malformed (no --- on line 2)", async () => { + const skillsDir = join(workdir, ".skills"); + await mkdir(skillsDir); + await writeFile( + join(skillsDir, "malformed.md"), + "Just some content\nNo separator here\nMore content", + "utf8", + ); + + const tool = createLoadSkillTool({ homeDir, workdir }); + const result = await tool.execute({ name: "malformed" }, stubCtx()); + + expect(result.isError).toBeUndefined(); + expect(result.content).toBe("Just some content\nNo separator here\nMore content"); + }); + + it("cwd skill shadows home skill of the same name", async () => { + const homeSkillsDir = join(homeDir, ".skills"); + await mkdir(homeSkillsDir); + await writeFile(join(homeSkillsDir, "shared.md"), "Home summary\n---\nHome body", "utf8"); + + const cwdSkillsDir = join(workdir, ".skills"); + await mkdir(cwdSkillsDir); + await writeFile(join(cwdSkillsDir, "shared.md"), "Cwd summary\n---\nCwd body", "utf8"); + + const tool = createLoadSkillTool({ homeDir, workdir }); + const result = await tool.execute({ name: "shared" }, stubCtx()); + + expect(result.isError).toBeUndefined(); + expect(result.content).toBe("Cwd body"); + }); + + it("reads from ctx.cwd when set", async () => { + const ctxDir = await mkdtemp(join(tmpdir(), "skills-ctx-test-")); + try { + const ctxSkillsDir = join(ctxDir, ".skills"); + await mkdir(ctxSkillsDir); + await writeFile(join(ctxSkillsDir, "ctx-skill.md"), "Ctx summary\n---\nFrom ctx cwd", "utf8"); + + const tool = createLoadSkillTool({ homeDir, workdir }); + const result = await tool.execute({ name: "ctx-skill" }, stubCtx({ cwd: ctxDir })); + + expect(result.isError).toBeUndefined(); + expect(result.content).toBe("From ctx cwd"); + } finally { + await rm(ctxDir, { recursive: true, force: true }); + } + }); + + it("concurrencySafe is true", () => { + const tool = createLoadSkillTool({ homeDir, workdir }); + expect(tool.concurrencySafe).toBe(true); + }); }); describe("scanSkillsDir", () => { - it("scans .md files and parses metadata", async () => { - const skillsDir = join(workdir, ".skills"); - await mkdir(skillsDir); - await writeFile(join(skillsDir, "valid.md"), "Summary\n---\nBody", "utf8"); - await writeFile(join(skillsDir, "malformed.md"), "No separator\nBody", "utf8"); - await writeFile(join(skillsDir, "other.txt"), "Not a skill", "utf8"); - - const result = await scanSkillsDir(skillsDir); - - expect(result).toHaveLength(2); - const valid = result.find((e) => e.name === "valid"); - expect(valid?.summary).toBe("Summary"); - const malformed = result.find((e) => e.name === "malformed"); - expect(malformed?.summary).toBeUndefined(); - }); - - it("returns empty array for nonexistent directory", async () => { - const result = await scanSkillsDir(join(workdir, "nonexistent")); - expect(result).toEqual([]); - }); + it("scans .md files and parses metadata", async () => { + const skillsDir = join(workdir, ".skills"); + await mkdir(skillsDir); + await writeFile(join(skillsDir, "valid.md"), "Summary\n---\nBody", "utf8"); + await writeFile(join(skillsDir, "malformed.md"), "No separator\nBody", "utf8"); + await writeFile(join(skillsDir, "other.txt"), "Not a skill", "utf8"); + + const result = await scanSkillsDir(skillsDir); + + expect(result).toHaveLength(2); + const valid = result.find((e) => e.name === "valid"); + expect(valid?.summary).toBe("Summary"); + const malformed = result.find((e) => e.name === "malformed"); + expect(malformed?.summary).toBeUndefined(); + }); + + it("returns empty array for nonexistent directory", async () => { + const result = await scanSkillsDir(join(workdir, "nonexistent")); + expect(result).toEqual([]); + }); }); describe("tools filter", () => { - it("rewrites load_skill description with the current catalog (cwd-aware)", async () => { - const homeSkillsDir = join(homeDir, ".skills"); - await mkdir(homeSkillsDir); - await writeFile(join(homeSkillsDir, "global.md"), "Global summary\n---\nBody", "utf8"); - - const cwdSkillsDir = join(workdir, ".skills"); - await mkdir(cwdSkillsDir); - await writeFile(join(cwdSkillsDir, "local.md"), "Local summary\n---\nBody", "utf8"); - - const filter = makeSkillsToolFilter({ homeDir, workdir }); - - const tool = createLoadSkillTool({ homeDir, workdir }); - const asm: ToolAssembly = { - tools: [tool], - cwd: workdir, - conversationId: "test-conv", - }; - - const result = await filter(asm); - const loadSkill = result.tools.find( - (t: import("@dispatch/kernel").ToolContract) => t.name === "load_skill", - ); - expect(loadSkill).toBeDefined(); - expect(loadSkill?.description).toContain("global"); - expect(loadSkill?.description).toContain("Global summary"); - expect(loadSkill?.description).toContain("local"); - expect(loadSkill?.description).toContain("Local summary"); - }); - - it("updates the name parameter enum with available skills", async () => { - const cwdSkillsDir = join(workdir, ".skills"); - await mkdir(cwdSkillsDir); - await writeFile(join(cwdSkillsDir, "alpha.md"), "Alpha\n---\nBody", "utf8"); - await writeFile(join(cwdSkillsDir, "beta.md"), "Beta\n---\nBody", "utf8"); - - const filter = makeSkillsToolFilter({ homeDir, workdir }); - - const tool = createLoadSkillTool({ homeDir, workdir }); - const asm: ToolAssembly = { - tools: [tool], - cwd: workdir, - conversationId: "test-conv", - }; - - const result = await filter(asm); - const loadSkill = result.tools.find( - (t: import("@dispatch/kernel").ToolContract) => t.name === "load_skill", - ); - expect(loadSkill?.parameters.properties?.name?.enum).toEqual(["alpha", "beta"]); - }); - - it("handles empty skill directories gracefully", async () => { - const filter = makeSkillsToolFilter({ homeDir, workdir }); - - const tool = createLoadSkillTool({ homeDir, workdir }); - const asm: ToolAssembly = { - tools: [tool], - cwd: workdir, - conversationId: "test-conv", - }; - - const result = await filter(asm); - const loadSkill = result.tools.find( - (t: import("@dispatch/kernel").ToolContract) => t.name === "load_skill", - ); - expect(loadSkill?.description).toContain("No skills are currently available"); - }); - - it("passes through non-load_skill tools unchanged", async () => { - const filter = makeSkillsToolFilter({ homeDir, workdir }); - - const otherTool = { - name: "other_tool", - description: "Some other tool", - parameters: { type: "object" as const }, - execute: async () => ({ content: "ok" }), - }; - const asm: ToolAssembly = { - tools: [otherTool], - cwd: workdir, - conversationId: "test-conv", - }; - - const result = await filter(asm); - expect(result.tools[0]?.name).toBe("other_tool"); - expect(result.tools[0]?.description).toBe("Some other tool"); - }); + it("rewrites load_skill description with the current catalog (cwd-aware)", async () => { + const homeSkillsDir = join(homeDir, ".skills"); + await mkdir(homeSkillsDir); + await writeFile(join(homeSkillsDir, "global.md"), "Global summary\n---\nBody", "utf8"); + + const cwdSkillsDir = join(workdir, ".skills"); + await mkdir(cwdSkillsDir); + await writeFile(join(cwdSkillsDir, "local.md"), "Local summary\n---\nBody", "utf8"); + + const filter = makeSkillsToolFilter({ homeDir, workdir }); + + const tool = createLoadSkillTool({ homeDir, workdir }); + const asm: ToolAssembly = { + tools: [tool], + cwd: workdir, + conversationId: "test-conv", + }; + + const result = await filter(asm); + const loadSkill = result.tools.find( + (t: import("@dispatch/kernel").ToolContract) => t.name === "load_skill", + ); + expect(loadSkill).toBeDefined(); + expect(loadSkill?.description).toContain("global"); + expect(loadSkill?.description).toContain("Global summary"); + expect(loadSkill?.description).toContain("local"); + expect(loadSkill?.description).toContain("Local summary"); + }); + + it("updates the name parameter enum with available skills", async () => { + const cwdSkillsDir = join(workdir, ".skills"); + await mkdir(cwdSkillsDir); + await writeFile(join(cwdSkillsDir, "alpha.md"), "Alpha\n---\nBody", "utf8"); + await writeFile(join(cwdSkillsDir, "beta.md"), "Beta\n---\nBody", "utf8"); + + const filter = makeSkillsToolFilter({ homeDir, workdir }); + + const tool = createLoadSkillTool({ homeDir, workdir }); + const asm: ToolAssembly = { + tools: [tool], + cwd: workdir, + conversationId: "test-conv", + }; + + const result = await filter(asm); + const loadSkill = result.tools.find( + (t: import("@dispatch/kernel").ToolContract) => t.name === "load_skill", + ); + expect(loadSkill?.parameters.properties?.name?.enum).toEqual(["alpha", "beta"]); + }); + + it("handles empty skill directories gracefully", async () => { + const filter = makeSkillsToolFilter({ homeDir, workdir }); + + const tool = createLoadSkillTool({ homeDir, workdir }); + const asm: ToolAssembly = { + tools: [tool], + cwd: workdir, + conversationId: "test-conv", + }; + + const result = await filter(asm); + const loadSkill = result.tools.find( + (t: import("@dispatch/kernel").ToolContract) => t.name === "load_skill", + ); + expect(loadSkill?.description).toContain("No skills are currently available"); + }); + + it("passes through non-load_skill tools unchanged", async () => { + const filter = makeSkillsToolFilter({ homeDir, workdir }); + + const otherTool = { + name: "other_tool", + description: "Some other tool", + parameters: { type: "object" as const }, + execute: async () => ({ content: "ok" }), + }; + const asm: ToolAssembly = { + tools: [otherTool], + cwd: workdir, + conversationId: "test-conv", + }; + + const result = await filter(asm); + expect(result.tools[0]?.name).toBe("other_tool"); + expect(result.tools[0]?.description).toBe("Some other tool"); + }); }); |
