Parent directory

planner.test.ts

2637 bytes
 1import { describe, it } from "node:test";
 2import assert from "node:assert/strict";
 3import { DefaultSkillTogglePlanner } from "./planner.ts";
 4import { SimpleFrontmatterCodec } from "../frontmatter/parser.ts";
 5import { MinimalFrontmatterPatcher } from "../frontmatter/patcher.ts";
 6import type { FileSystem } from "../ports/fs.ts";
 7import type { SkillRecord } from "../types.ts";
 8
 9const codec = new SimpleFrontmatterCodec();
10const patcher = new MinimalFrontmatterPatcher();
11
12describe("DefaultSkillTogglePlanner", () => {
13  it("plans a normalization change for duplicated disable-model-invocation keys even if the mode is unchanged", async () => {
14    const filePath = "/skills/handoff/SKILL.md";
15    const raw = [
16      "---",
17      "name: handoff",
18      "description: Compact the conversation.",
19      "disable-model-invocation: true",
20      "argument-hint: What next?",
21      "disable-model-invocation: true",
22      "---",
23      "",
24      "# Handoff",
25      "",
26    ].join("\n");
27    const fs = new MemoryFileSystem(new Map([[filePath, raw]]));
28    const planner = new DefaultSkillTogglePlanner(fs, codec, patcher);
29    const record = skillRecord(filePath, "manual-only");
30
31    const changes = await planner.plan([record], [{ skill: record, desiredMode: "manual-only" }]);
32
33    assert.equal(changes.length, 1);
34    assert.equal(changes[0]?.from, "manual-only");
35    assert.equal(changes[0]?.to, "manual-only");
36    assert.equal((changes[0]?.patch.newText.match(/^disable-model-invocation\s*:/gm) ?? []).length, 1);
37  });
38});
39
40function skillRecord(filePath: string, mode: SkillRecord["mode"]): SkillRecord {
41  return {
42    id: filePath,
43    name: "handoff",
44    description: "Compact the conversation.",
45    filePath,
46    baseDir: "/skills/handoff",
47    source: { kind: "user", root: "/skills" },
48    editable: true,
49    mode,
50    diagnostics: [],
51  };
52}
53
54class MemoryFileSystem implements FileSystem {
55  constructor(private readonly files: Map<string, string>) {}
56
57  async readFile(path: string): Promise<string> {
58    const value = this.files.get(path);
59    if (value === undefined) throw new Error(`missing file: ${path}`);
60    return value;
61  }
62
63  async writeFileAtomic(path: string, content: string): Promise<void> {
64    this.files.set(path, content);
65  }
66
67  async access(path: string): Promise<boolean> {
68    return this.files.has(path);
69  }
70
71  async readdir(): Promise<Array<{ name: string; isDirectory: boolean; isFile: boolean; isSymbolicLink: boolean }>> {
72    return [];
73  }
74
75  async stat(): Promise<{ isDirectory: boolean; isFile: boolean; mode: number }> {
76    return { isDirectory: false, isFile: true, mode: 0o644 };
77  }
78}