summaryrefslogtreecommitdiffhomepage
path: root/packages/skills/src/skills.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/skills/src/skills.test.ts')
-rw-r--r--packages/skills/src/skills.test.ts478
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");
+ });
});