Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/test/custom-commands.test.ts

typescript760 行查看原文
import fs from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
  CustomCommandsStore,
  type CustomCommandsStoreOptions,
  isCustomCommandId,
  parseCustomCommandMarkdown,
  validateCustomCommandName,
} from "../src/main/custom-commands.ts";

let temporary: string;
let root: string;
let state: string;
let store: CustomCommandsStore;
function makeStore(options: Partial<CustomCommandsStoreOptions> = {}) {
  return new CustomCommandsStore({ rootDirectory: root, stateFilePath: state, ...options });
}
async function put(id: string, content: string) {
  const path = join(root, id);
  await fs.mkdir(dirname(path), { recursive: true });
  await fs.writeFile(path, content);
  return path;
}
beforeEach(async () => {
  temporary = await fs.realpath(await fs.mkdtemp(join(tmpdir(), "zpi-custom-commands-")));
  root = join(temporary, ".zpi", "commands");
  state = join(temporary, ".zpi", "command-state.json");
  store = makeStore();
});
afterEach(async () => {
  vi.restoreAllMocks();
  await fs.rm(temporary, { recursive: true, force: true });
});

describe("custom command Markdown", () => {
  it("ignores comments in continued scalars while preserving literal block content", () => {
    expect(
      parseCustomCommandMarkdown("---\ndescription: Summary\n  # note\n  continued\n---\nPrompt").description,
    ).toBe("Summary continued");
    expect(parseCustomCommandMarkdown("---\ndescription: |\n  # heading\n---\nPrompt").description).toBe(
      "# heading",
    );
  });
  it("reads scalar, quoted, CRLF, folded and literal metadata without interpreting prompt text", () => {
    expect(
      parseCustomCommandMarkdown(
        "---\r\ndescription: 'Review: it''s useful'\r\nargument-hint: \"file\"\r\n---\r\n\r\nRun $1 and $ARGUMENTS",
      ),
    ).toEqual({
      description: "Review: it's useful",
      argumentHint: "file",
      prompt: "Run $1 and $ARGUMENTS",
    });
    expect(
      parseCustomCommandMarkdown(
        "---\ndescription: >\n  Review the\n  current changes\nargument-hint: |\n  file\n  context\n---\nPrompt",
      ),
    ).toEqual({
      description: "Review the current changes",
      argumentHint: "file\ncontext",
      prompt: "Prompt",
    });
  });
  it.each([
    ["frontend-review", ["frontend-review"]],
    ["frontend-review, testing", ["frontend-review", "testing"]],
    ["[frontend-review, testing]", ["frontend-review", "testing"]],
    [`["frontend-review", 'testing']`, ["frontend-review", "testing"]],
    ['"frontend-review, testing"', ["frontend-review", "testing"]],
    ["frontend-review, frontend-review", ["frontend-review"]],
    ["[]", []],
  ])("parses inert flat skills metadata %s", (value, skills) => {
    expect(parseCustomCommandMarkdown(`---\nskills: ${value}\n---\nPrompt`).skills).toEqual(skills);
  });
  it.each([
    "skills:\n  - frontend-review",
    "skills:\n- frontend-review",
    "skills:\n  # note\n\n  - frontend-review",
    "skills: # note\n  - frontend-review",
  ])("reports unsupported block skills instead of silently dropping %s", (metadata) => {
    expect(() => parseCustomCommandMarkdown(`---\n${metadata}\n---\nPrompt`)).toThrow("skills: [a, b]");
  });
  it("accepts empty skills metadata followed by comments and another field", () => {
    expect(
      parseCustomCommandMarkdown("---\nskills:\n  # none\ndescription: Summary\n---\nPrompt"),
    ).toMatchObject({
      skills: [],
      description: "Summary",
    });
  });
  it.each([
    ["true", true],
    ["yes", true],
    ['"YES"', true],
    ['" YES "', true],
    ["false", false],
    ["no", false],
    ["other", false],
  ])("parses disable-noninteractive %s", (value, expected) => {
    expect(
      parseCustomCommandMarkdown(`---\ndisable-noninteractive: ${value}\n---\nPrompt`).disableNonInteractive,
    ).toBe(expected);
  });
  it("bounds skill metadata and never evaluates YAML tags or aliases", () => {
    expect(() => parseCustomCommandMarkdown(`---\nskills: ${"x".repeat(129)}\n---\nPrompt`)).toThrow(
      "invalid_frontmatter",
    );
    expect(() =>
      parseCustomCommandMarkdown(
        `---\nskills: ${Array.from({ length: 65 }, (_, index) => `skill-${index}`).join(",")}\n---\nPrompt`,
      ),
    ).toThrow("invalid_frontmatter");
    expect(() => parseCustomCommandMarkdown("---\nskills: [broken\n---\nPrompt")).toThrow(
      "invalid_frontmatter",
    );
    expect(parseCustomCommandMarkdown("---\nskills: !!custom-tag, *alias\n---\nPrompt").skills).toEqual([
      "!!custom-tag",
      "*alias",
    ]);
    expect(parseCustomCommandMarkdown("Prompt").skills).toBeUndefined();
    expect(parseCustomCommandMarkdown("Prompt").disableNonInteractive).toBeUndefined();
  });
  it("only reads frontmatter at the start and reports an unclosed fence", () => {
    const prompt = "A prompt\n---\ndescription: ordinary prompt text\n---\nEnd";
    expect(parseCustomCommandMarkdown(prompt).prompt).toBe(prompt);
    expect(parseCustomCommandMarkdown(prompt).description).toBeUndefined();
    expect(() => parseCustomCommandMarkdown("---\ndescription: broken")).toThrow("invalid_frontmatter");
    expect(parseCustomCommandMarkdown("\uFEFF---\ndescription: yes\n---\nPrompt").description).toBe("yes");
  });
  it.each(["", "../escape", "two words", "a/b", "中文", "a".repeat(51), "a.md", "a\\b"])(
    "rejects unsafe new name %s",
    (name) => expect(() => validateCustomCommandName(name)).toThrow("invalid_input"),
  );
  it("accepts documented new-name characters and a leading display slash", () => {
    expect(validateCustomCommandName("/Review_123-ok")).toBe("Review_123-ok");
  });
});

describe("custom command IDs", () => {
  it.each(["review.md", "tools/Legacy Name.MD", "中文.md", `${"a".repeat(1021)}.md`])(
    "accepts the shared import/storage ID %s",
    (id) => expect(isCustomCommandId(id)).toBe(true),
  );
  it.each([
    undefined,
    null,
    0,
    {},
    "",
    "/a.md",
    "../a.md",
    "a//b.md",
    "a/./b.md",
    "a\\b.md",
    "a:b.md",
    "a\u0000.md",
    "a\u007f.md",
    ".hidden.md",
    "a./b.md",
    "a /b.md",
    "a.txt",
    `${"a".repeat(1022)}.md`,
  ])("rejects invalid or unknown ID %s", (id) => {
    expect(isCustomCommandId(id)).toBe(false);
  });
});

describe("custom command storage", () => {
  it("lists an actionable diagnostic for block skills without hiding valid commands", async () => {
    const path = await put("block.md", "---\nskills:\n  - frontend-review\n---\nPrompt");
    await put("valid.md", "---\nskills: [testing]\n---\nPrompt");
    const result = await store.list();
    expect(result.commands.map((command) => command.id)).toEqual(["valid.md"]);
    expect(result.diagnostics).toEqual([
      { code: "invalid_frontmatter", path, message: expect.stringContaining("skills: [a, b]") },
    ]);
  });
  it.each(["init", "COMPACT", "side", "btw"])("rejects creating a reserved command %s", async (name) => {
    await expect(store.create({ name, prompt: "unreachable" })).rejects.toThrow("reserved");
  });
  it.each(["", "Short", "x".repeat(256 * 1024)])(
    "reads empty, small and maximum-size commands with a size-based buffer (%#)",
    async (content) => {
      const path = await put("bounded.md", content);
      const sizes: number[] = [];
      const open = fs.open.bind(fs);
      vi.spyOn(fs, "open").mockImplementation(async (...args) => {
        const handle = await open(...args);
        if (args[0] === path) {
          const read = handle.read.bind(handle);
          Object.defineProperty(handle, "read", {
            value: async (buffer: Buffer, offset: number, length: number, position: number) => {
              sizes.push(buffer.length);
              return read(buffer, offset, length, position);
            },
          });
        }
        return handle;
      });
      const result = await store.list();
      expect(result.diagnostics).toEqual([]);
      expect(result.commands[0].prompt).toBe(content);
      expect(sizes.length).toBeGreaterThan(0);
      expect(new Set(sizes)).toEqual(new Set([Buffer.byteLength(content) + 1]));
    },
  );
  it("continues short reads without losing UTF-8 bytes", async () => {
    const content = "Review 中文 prompt";
    const path = await put("short.md", content);
    const open = fs.open.bind(fs);
    vi.spyOn(fs, "open").mockImplementation(async (...args) => {
      const handle = await open(...args);
      if (args[0] === path) {
        const read = handle.read.bind(handle);
        Object.defineProperty(handle, "read", {
          value: (buffer: Buffer, offset: number, length: number, position: number) =>
            read(buffer, offset, Math.min(2, length), position),
        });
      }
      return handle;
    });
    const result = await store.list();
    expect(result.diagnostics).toEqual([]);
    expect(result.commands[0].prompt).toBe(content);
  });
  it.each([
    ["123456", "conflict", [2, 4, 8]],
    ["123456789", "file_limit", [2, 4, 8, 9]],
  ])(
    "bounds growth during reading and preserves %s / %s diagnostics",
    async (growth, expected, capacities) => {
      const path = await put("growing.md", "x");
      const sizes: number[] = [];
      const open = fs.open.bind(fs);
      let grew = false;
      vi.spyOn(fs, "open").mockImplementation(async (...args) => {
        const handle = await open(...args);
        if (args[0] === path) {
          const read = handle.read.bind(handle);
          Object.defineProperty(handle, "read", {
            value: async (buffer: Buffer, offset: number, length: number, position: number) => {
              if (!grew) {
                grew = true;
                await fs.appendFile(path, growth);
              }
              sizes.push(buffer.length);
              return read(buffer, offset, length, position);
            },
          });
        }
        return handle;
      });
      const result = await makeStore({ limits: { maxFileBytes: 8 } }).list();
      expect(result.commands).toEqual([]);
      expect(result.diagnostics[0].code).toBe(expected);
      expect([...new Set(sizes)]).toEqual(capacities);
    },
  );
  it("still rejects invalid UTF-8 after bounded reads", async () => {
    const path = await put("invalid.md", "");
    await fs.writeFile(path, Buffer.from([0xc3, 0x28]));
    const result = await store.list();
    expect(result.commands).toEqual([]);
    expect(result.diagnostics).toHaveLength(1);
    expect(result.diagnostics[0].message).toContain("encoded data");
  });
  it("lists an absent directory without creating any files", async () => {
    expect(await store.list()).toEqual({ commands: [], diagnostics: [], directoryPath: root });
    expect(await fs.readdir(temporary)).toEqual([]);
  });
  it("creates, persists enable state, updates, renames, deletes and resets a recreated command", async () => {
    const created = await store.create({
      name: "review",
      description: "Review changes",
      argumentHint: "[file]",
      prompt: "Review $1",
    });
    expect(created).toMatchObject({ id: "review.md", name: "/review", enabled: true, prompt: "Review $1" });
    expect(created.revision).toMatch(/^[a-f0-9]{64}$/);
    await store.setEnabled({ id: created.id, enabled: false });
    expect((await makeStore().list()).commands[0].enabled).toBe(false);
    const updated = await store.update({
      id: created.id,
      expectedRevision: created.revision,
      name: "inspect",
      prompt: "Inspect changes",
    });
    expect(updated).toMatchObject({
      id: "inspect.md",
      name: "/inspect",
      enabled: false,
      prompt: "Inspect changes",
    });
    expect(await fs.readdir(root)).toEqual(["inspect.md"]);
    await store.delete({ id: updated.id, expectedRevision: updated.revision });
    expect((await store.list()).commands).toEqual([]);
    expect((await store.create({ name: "inspect", prompt: "New" })).enabled).toBe(true);
  });
  it("preserves unknown frontmatter blocks, comments, and existing nested names on update", async () => {
    await put(
      "tools/Legacy Name.md",
      "---\n# Keep this comment\nallowed-tools:\n  - read\n  - bash\ncustom: |\n  keep: exactly\n  second line\ndescription: old\n  continuation\nargument-hint: old hint\n---\nOld prompt",
    );
    const original = (await store.list()).commands[0];
    const updated = await store.update({
      id: original.id,
      expectedRevision: original.revision,
      name: original.name,
      description: "New: description",
      argumentHint: "[new]\ncontext",
      prompt: "New prompt",
    });
    expect(updated.name).toBe("/tools/Legacy Name");
    expect(updated.argumentHint).toBe("[new]\ncontext");
    const raw = await fs.readFile(updated.filePath, "utf8");
    expect(raw).toContain(
      "# Keep this comment\nallowed-tools:\n  - read\n  - bash\ncustom: |\n  keep: exactly\n  second line",
    );
    expect(raw).not.toContain("continuation");
    expect(updated.description).toBe("New: description");
  });
  it("preserves unknown quoted and dotted frontmatter keys after a managed key", async () => {
    await put(
      "unknown.md",
      `---
description: Original
"quoted-key":
  keep: quoted
custom.key:
  keep: dotted
---
Original prompt`,
    );
    const original = (await store.list()).commands[0];
    expect(original.description).toBe("Original");
    const updated = await store.update({
      id: original.id,
      expectedRevision: original.revision,
      description: "Changed",
      prompt: "Changed prompt",
    });
    const content = await fs.readFile(updated.filePath, "utf8");
    expect(content).toContain('"quoted-key":\n  keep: quoted\ncustom.key:\n  keep: dotted');
    expect(updated.description).toBe("Changed");
  });
  it("preserves skills, interaction visibility, and unapplied model/tool metadata when editing", async () => {
    await put(
      "metadata.md",
      "---\nskills: [frontend-review, testing]\ndisable-noninteractive: yes\nmodel: other-model\nallowed-tools: bash\n---\nOriginal",
    );
    const command = (await store.list()).commands[0];
    expect(command).toMatchObject({ skills: ["frontend-review", "testing"], disableNonInteractive: true });
    const updated = await store.update({
      id: command.id,
      expectedRevision: command.revision,
      prompt: "Changed",
      description: "New",
    });
    expect(updated).toMatchObject({ skills: ["frontend-review", "testing"], disableNonInteractive: true });
    expect(await fs.readFile(updated.filePath, "utf8")).toContain(
      "skills: [frontend-review, testing]\ndisable-noninteractive: yes\nmodel: other-model\nallowed-tools: bash",
    );
  });
  it("does not mistake a new prompt's opening horizontal rule for metadata", async () => {
    const command = await store.create({ name: "rule", prompt: "---\nMarkdown content" });
    expect(command.prompt).toBe("---\nMarkdown content");
  });
  it("lists nested Markdown alphabetically, ignores hidden/non-Markdown files, and reports bad files", async () => {
    await put("z.md", "Z");
    await put("a/sub.md", "Nested");
    await put("b.MD", "B");
    await put(".hidden.md", "Hidden");
    await put(".hidden/secret.md", "Hidden");
    await put("notes.txt", "Ignore");
    await put("broken.md", "---\nInvalid");
    const result = await store.list();
    expect(result.commands.map((item) => item.name)).toEqual(["/a/sub", "/b", "/z"]);
    expect(result.diagnostics).toContainEqual(expect.objectContaining({ code: "invalid_frontmatter" }));
  });
  it("never overwrites a create/rename collision or a stale external edit", async () => {
    const original = await store.create({ name: "one", prompt: "One" });
    await store.create({ name: "two", prompt: "Two" });
    await expect(store.create({ name: "one", prompt: "Overwrite" })).rejects.toThrow("conflict");
    await expect(
      store.update({
        id: original.id,
        expectedRevision: original.revision,
        name: "two",
        prompt: "Overwrite",
      }),
    ).rejects.toThrow("conflict");
    expect(await fs.readFile(original.filePath, "utf8")).toBe("One");
    await fs.writeFile(original.filePath, "External change");
    await expect(
      store.update({ id: original.id, expectedRevision: original.revision, prompt: "Stale" }),
    ).rejects.toThrow("conflict");
    await expect(store.delete({ id: original.id, expectedRevision: original.revision })).rejects.toThrow(
      "conflict",
    );
    expect(await fs.readFile(original.filePath, "utf8")).toBe("External change");
  });
  it("rejects case-insensitive create and rename collisions", async () => {
    const original = await store.create({ name: "Review", prompt: "Original" });
    const other = await store.create({ name: "other", prompt: "Other" });
    await expect(store.create({ name: "review", prompt: "Overwrite" })).rejects.toThrow("case-insensitive");
    await expect(
      store.update({ id: other.id, expectedRevision: other.revision, name: "REVIEW", prompt: "Overwrite" }),
    ).rejects.toThrow("case-insensitive");
    await expect(
      store.update({
        id: original.id,
        expectedRevision: original.revision,
        name: "review",
        prompt: "Change case",
      }),
    ).rejects.toThrow("case-insensitive");
    expect(await fs.readFile(original.filePath, "utf8")).toBe("Original");
    expect(await fs.readFile(other.filePath, "utf8")).toBe("Other");
  });
  it.skipIf(process.platform === "darwin")(
    "selects one casefold duplicate deterministically and diagnoses it without changing files",
    async () => {
      await put("review.md", "Lowercase");
      await put("Review.md", "Uppercase");
      await put("tools/a.md", "Nested lowercase");
      await put("Tools/A.md", "Nested uppercase");
      const result = await store.list();
      expect(result.commands.map((command) => command.name)).toEqual(["/Review", "/Tools/A"]);
      expect(result.diagnostics.filter((item) => item.code === "duplicate_name")).toHaveLength(2);
      expect(await fs.readFile(join(root, "review.md"), "utf8")).toBe("Lowercase");
      expect(await fs.readFile(join(root, "tools/a.md"), "utf8")).toBe("Nested lowercase");
    },
  );
  it("retains editable legacy names with an invocation diagnostic", async () => {
    await put("tools/Legacy Name.md", "Legacy");
    const result = await store.list();
    expect(result.commands[0].name).toBe("/tools/Legacy Name");
    expect(result.diagnostics[0].code).toBe("unsupported_name");
  });
  it("serializes simultaneous updates and survives a rejected mutation", async () => {
    const original = await store.create({ name: "one", prompt: "One" });
    const results = await Promise.allSettled([
      store.update({ id: original.id, expectedRevision: original.revision, prompt: "First" }),
      store.update({ id: original.id, expectedRevision: original.revision, prompt: "Second" }),
    ]);
    expect(results.map((result) => result.status)).toEqual(["fulfilled", "rejected"]);
    expect((await store.list()).commands[0].prompt).toBe("First");
    await store.setEnabled({ id: original.id, enabled: false });
    expect((await store.list()).commands[0].enabled).toBe(false);
  });
  it("keeps old content and cleans staging files after failed atomic publish", async () => {
    const original = await store.create({ name: "one", prompt: "One" });
    vi.spyOn(fs, "rename").mockRejectedValueOnce(new Error("disk failure"));
    await expect(
      store.update({ id: original.id, expectedRevision: original.revision, prompt: "New" }),
    ).rejects.toThrow("disk failure");
    expect(await fs.readFile(original.filePath, "utf8")).toBe("One");
    expect(await fs.readdir(root)).toEqual(["one.md"]);
  });
  it("keeps the old file if writing a staged replacement fails", async () => {
    const original = await store.create({ name: "one", prompt: "One" });
    const open = fs.open.bind(fs);
    vi.spyOn(fs, "open").mockImplementation(async (...args) => {
      const handle = await open(...args);
      if (String(args[0]).endsWith(".tmp"))
        vi.spyOn(handle, "writeFile").mockRejectedValueOnce(new Error("disk full"));
      return handle;
    });
    await expect(
      store.update({ id: original.id, expectedRevision: original.revision, prompt: "New" }),
    ).rejects.toThrow("disk full");
    expect(await fs.readFile(original.filePath, "utf8")).toBe("One");
    expect(await fs.readdir(root)).toEqual(["one.md"]);
  });
  it("rejects a modified staging file before replacing the original", async () => {
    const original = await store.create({ name: "one", prompt: "One" });
    const open = fs.open.bind(fs);
    vi.spyOn(fs, "open").mockImplementation(async (...args) => {
      const handle = await open(...args);
      if (String(args[0]).endsWith(".tmp")) {
        vi.spyOn(handle, "sync").mockImplementationOnce(async () => {
          await fs.writeFile(args[0], "Bad");
        });
      }
      return handle;
    });
    await expect(
      store.update({ id: original.id, expectedRevision: original.revision, prompt: "New" }),
    ).rejects.toThrow("conflict");
    expect(await fs.readFile(original.filePath, "utf8")).toBe("One");
    expect(await fs.readdir(root)).toEqual(["one.md"]);
  });
  it("handles a target created between the preflight and exclusive publication", async () => {
    const link = fs.link.bind(fs);
    vi.spyOn(fs, "link").mockImplementationOnce(async (source, target) => {
      await fs.writeFile(target, "External winner");
      return link(source, target);
    });
    await expect(store.create({ name: "one", prompt: "Mine" })).rejects.toThrow("conflict");
    expect(await fs.readFile(join(root, "one.md"), "utf8")).toBe("External winner");
    expect(await fs.readdir(root)).toEqual(["one.md"]);
  });
  it("preserves enable state when its atomic replacement fails", async () => {
    const command = await store.create({ name: "one", prompt: "One" });
    await store.setEnabled({ id: command.id, enabled: false });
    const original = await fs.readFile(state, "utf8");
    vi.spyOn(fs, "rename").mockRejectedValueOnce(new Error("state write failed"));
    await expect(store.setEnabled({ id: command.id, enabled: true })).rejects.toThrow("state write failed");
    expect(await fs.readFile(state, "utf8")).toBe(original);
    expect((await store.list()).commands[0].enabled).toBe(false);
  });
  it("rolls back a renamed file if removing the old path fails", async () => {
    const original = await store.create({ name: "one", prompt: "One" });
    const unlink = fs.unlink.bind(fs);
    vi.spyOn(fs, "unlink").mockImplementation(async (path) => {
      if (path === original.filePath) throw new Error("cannot remove original");
      return unlink(path);
    });
    await expect(
      store.update({ id: original.id, expectedRevision: original.revision, name: "two", prompt: "New" }),
    ).rejects.toThrow("cannot remove original");
    expect(await fs.readFile(original.filePath, "utf8")).toBe("One");
    expect(await fs.readdir(root)).toEqual(["one.md"]);
  });
  it("retains a corrupt state file and disables entries until repaired", async () => {
    await put("one.md", "One");
    await fs.writeFile(state, "broken JSON");
    const result = await store.list();
    expect(result.commands[0].enabled).toBe(false);
    expect(result.diagnostics[0].code).toBe("invalid_state");
    await expect(store.setEnabled({ id: "one.md", enabled: true })).rejects.toThrow("invalid_state");
    expect(await fs.readFile(state, "utf8")).toBe("broken JSON");
  });
  it("bounds file sizes, directory depth, scan entries and total bytes", async () => {
    await put("a.md", "12345");
    await put("b.md", "67890");
    await put("nested/deeper/c.md", "C");
    expect(
      (await makeStore({ limits: { maxFileBytes: 4 } }).list()).diagnostics.some(
        (item) => item.code === "file_limit",
      ),
    ).toBe(true);
    const depth = await makeStore({ limits: { maxDepth: 1 } }).list();
    expect(depth.commands.map((item) => item.id)).toEqual(["a.md", "b.md"]);
    expect(depth.diagnostics.some((item) => item.code === "scan_limit")).toBe(true);
    const count = await makeStore({ limits: { maxEntries: 1 } }).list();
    expect(count.commands).toEqual([]);
    expect(count.diagnostics[0].code).toBe("scan_limit");
    const bytes = await makeStore({ limits: { maxTotalBytes: 5 } }).list();
    expect(bytes.commands.map((item) => item.id)).toEqual(["a.md"]);
    expect(bytes.diagnostics.some((item) => item.code === "scan_limit")).toBe(true);
    await expect(
      makeStore({ limits: { maxFileBytes: 4 } }).create({ name: "big", prompt: "12345" }),
    ).rejects.toThrow("file_limit");
  });
  it("bounds bytes even when every file has invalid frontmatter", async () => {
    await put("a.md", "---\nbad");
    await put("b.md", "---\nbad");
    const result = await makeStore({ limits: { maxTotalBytes: 7 } }).list();
    expect(result.commands).toEqual([]);
    expect(result.diagnostics.map((item) => item.code)).toEqual(["invalid_frontmatter", "scan_limit"]);
  });
  it("rejects unsafe IDs before reading or changing outside files", async () => {
    const outside = join(temporary, "outside.md");
    await fs.writeFile(outside, "Untouched");
    for (const id of [
      "../outside.md",
      "a/../../outside.md",
      outside,
      "a\\b.md",
      "a//b.md",
      ".hidden.md",
      "a/./b.md",
      "a\u0000.md",
      "a:stream.md",
    ]) {
      await expect(store.update({ id, expectedRevision: "a".repeat(64), prompt: "Unsafe" })).rejects.toThrow(
        "invalid_input",
      );
      await expect(store.delete({ id, expectedRevision: "a".repeat(64) })).rejects.toThrow("invalid_input");
      await expect(store.setEnabled({ id, enabled: false })).rejects.toThrow("invalid_input");
    }
    expect(await fs.readFile(outside, "utf8")).toBe("Untouched");
  });
  it("rejects command and directory symlinks without reading or writing their targets", async () => {
    const outside = join(temporary, "outside");
    await fs.mkdir(outside);
    await fs.writeFile(join(outside, "source.md"), "Untouched");
    await fs.mkdir(root, { recursive: true });
    await fs.symlink(join(outside, "source.md"), join(root, "link.md"));
    await fs.symlink(outside, join(root, "linked-folder"), "dir");
    const result = await store.list();
    expect(result.commands).toEqual([]);
    expect(result.diagnostics.every((item) => item.code === "unsafe_path")).toBe(true);
    await expect(
      store.update({ id: "link.md", expectedRevision: "a".repeat(64), prompt: "Unsafe" }),
    ).rejects.toThrow("unsafe_path");
    await expect(store.create({ name: "link", prompt: "Unsafe" })).rejects.toThrow("conflict");
    await expect(
      store.update({ id: "linked-folder/source.md", expectedRevision: "a".repeat(64), prompt: "Unsafe" }),
    ).rejects.toThrow("unsafe_path");
    expect(await fs.readFile(join(outside, "source.md"), "utf8")).toBe("Untouched");
  });
  it.each(["root", "ancestor", "state"])("rejects a symlink at the %s", async (kind) => {
    const outside = join(temporary, "outside");
    await fs.mkdir(outside);
    if (kind === "root") {
      await fs.mkdir(dirname(root), { recursive: true });
      await fs.symlink(outside, root, "dir");
    } else if (kind === "ancestor") {
      await fs.symlink(outside, dirname(root), "dir");
    } else {
      await fs.mkdir(dirname(root), { recursive: true });
      await fs.writeFile(join(outside, "state.json"), '{"version":1,"disabled":[]}');
      await fs.symlink(join(outside, "state.json"), state);
    }
    await expect(store.create({ name: "unsafe", prompt: "Unsafe" })).rejects.toThrow("unsafe_path");
    const result = await store.list();
    expect(result.commands).toEqual([]);
    expect(result.diagnostics.some((item) => item.code === "unsafe_path")).toBe(true);
    expect(await fs.readdir(outside)).toEqual(kind === "state" ? ["state.json"] : []);
  });
  it("rejects a hardlinked command so editing cannot mutate a shared inode", async () => {
    const outside = join(temporary, "outside.md");
    await fs.writeFile(outside, "Untouched");
    await fs.mkdir(root, { recursive: true });
    await fs.link(outside, join(root, "hard.md"));
    expect((await store.list()).diagnostics[0].code).toBe("unsafe_path");
  });
  it("reads only explicitly registered links, confirms source edits and deletes only the local link", async () => {
    const source = join(temporary, "approved", "source.md");
    await fs.mkdir(dirname(source));
    await fs.writeFile(source, "Original source");
    await fs.mkdir(root, { recursive: true });
    const link = join(root, "linked.md");
    await fs.symlink(source, link);
    const registered = makeStore({ importedLinkTargets: async () => new Map([["linked.md", source]]) });
    const command = (await registered.list()).commands[0];
    expect(command).toMatchObject({ importedSourcePath: source, prompt: "Original source" });
    await expect(
      registered.update({ id: command.id, expectedRevision: command.revision, prompt: "New" }),
    ).rejects.toThrow("confirmation_required");
    await expect(
      registered.update({
        id: command.id,
        expectedRevision: command.revision,
        prompt: "New",
        confirmLinkedSource: true,
        expectedSourcePath: join(temporary, "other.md"),
      }),
    ).rejects.toThrow("confirmation_required");
    const updated = await registered.update({
      id: command.id,
      expectedRevision: command.revision,
      prompt: "New source",
      confirmLinkedSource: true,
      expectedSourcePath: source,
    });
    expect(await fs.readFile(source, "utf8")).toBe("New source");
    expect((await fs.lstat(link)).isSymbolicLink()).toBe(true);
    expect(updated.importedSourcePath).toBe(source);
    await registered.delete({ id: updated.id, expectedRevision: updated.revision });
    expect(await fs.readFile(source, "utf8")).toBe("New source");
    expect(await fs.readdir(root)).toEqual([]);
  });
  it("rejects a registered link whose target or source ancestry changed", async () => {
    await fs.mkdir(root, { recursive: true });
    const source = join(temporary, "approved.md");
    const other = join(temporary, "other.md");
    await fs.writeFile(source, "Approved");
    await fs.writeFile(other, "Other");
    await fs.symlink(other, join(root, "linked.md"));
    const registered = makeStore({ importedLinkTargets: async () => new Map([["linked.md", source]]) });
    expect((await registered.list()).diagnostics[0].code).toBe("unsafe_path");
    await fs.unlink(join(root, "linked.md"));
    await fs.symlink(source, join(root, "linked.md"));
    await fs.unlink(source);
    await fs.symlink(other, source);
    expect((await registered.list()).diagnostics[0].code).toBe("unsafe_path");
  });
  it.each(["missing", "malformed", "unsafe"])(
    "keeps a registered link with a %s source visible and safely deletable",
    async (kind) => {
      const source = join(temporary, "source.md");
      const outside = join(temporary, "outside.md");
      await fs.writeFile(source, "Original");
      await fs.writeFile(outside, "Never modify");
      await fs.mkdir(root, { recursive: true });
      const local = join(root, "linked.md");
      await fs.symlink(source, local);
      const registered = makeStore({ importedLinkTargets: async () => new Map([["linked.md", source]]) });
      if (kind === "malformed") await fs.writeFile(source, "---\nUnclosed");
      else {
        await fs.unlink(source);
        if (kind === "unsafe") await fs.symlink(outside, source);
      }
      const result = await registered.list();
      const command = result.commands[0];
      expect(command).toMatchObject({
        id: "linked.md",
        prompt: "",
        importedSourcePath: source,
        unreadable: true,
        enabled: false,
      });
      expect(result.diagnostics).toHaveLength(1);
      await expect(
        registered.update({
          id: command.id,
          expectedRevision: command.revision,
          confirmLinkedSource: true,
          expectedSourcePath: source,
          prompt: "Should not save",
        }),
      ).rejects.toThrow();
      await registered.delete({ id: command.id, expectedRevision: command.revision });
      expect(await fs.readdir(root)).toEqual([]);
      expect(await fs.readFile(outside, "utf8")).toBe("Never modify");
      if (kind === "malformed") expect(await fs.readFile(source, "utf8")).toBe("---\nUnclosed");
      if (kind === "unsafe") expect((await fs.lstat(source)).isSymbolicLink()).toBe(true);
    },
  );
  it("restores the persisted enabled preference when a broken source is repaired", async () => {
    const source = join(temporary, "source.md");
    await fs.mkdir(root, { recursive: true });
    await fs.symlink(source, join(root, "linked.md"));
    const registered = makeStore({ importedLinkTargets: async () => new Map([["linked.md", source]]) });
    expect((await registered.list()).commands[0]).toMatchObject({ unreadable: true, enabled: false });
    await fs.writeFile(source, "Repaired");
    expect((await registered.list()).commands[0]).toMatchObject({ enabled: true, prompt: "Repaired" });
    await registered.setEnabled({ id: "linked.md", enabled: false });
    await fs.unlink(source);
    expect((await registered.list()).commands[0].unreadable).toBe(true);
    await fs.writeFile(source, "Repaired again");
    expect((await registered.list()).commands[0]).toMatchObject({ enabled: false, prompt: "Repaired again" });
  });
  it("preserves a linked source and link when atomic source publication fails", async () => {
    const source = join(temporary, "source.md");
    await fs.writeFile(source, "Original");
    await fs.mkdir(root, { recursive: true });
    await fs.symlink(source, join(root, "linked.md"));
    const registered = makeStore({ importedLinkTargets: async () => new Map([["linked.md", source]]) });
    const command = (await registered.list()).commands[0];
    vi.spyOn(fs, "rename").mockRejectedValueOnce(new Error("source write failed"));
    await expect(
      registered.update({
        id: command.id,
        expectedRevision: command.revision,
        prompt: "New",
        confirmLinkedSource: true,
        expectedSourcePath: source,
      }),
    ).rejects.toThrow("source write failed");
    expect(await fs.readFile(source, "utf8")).toBe("Original");
    expect((await fs.lstat(command.filePath)).isSymbolicLink()).toBe(true);
  });
});