Z ZPI 文档 v0.3.8
介绍页

源码

packages/ui/test/assistant-preview-cards.test.ts

typescript186 行查看原文
import { describe, expect, it, vi } from "vitest";
import {
  buildAssistantPreviewCardsFromReferences,
  extractAssistantFileReferences,
  getAssistantPreviewCardFilePath,
  isValidAssistantPreviewWebsiteUrl,
} from "../src/preview/assistant-preview-cards.ts";
import { resolveValidatedAssistantPreviewCards } from "../src/preview/assistant-preview-validation.ts";
import { advancePreviewAutoOpen, createPreviewLoader } from "../src/preview/preview-lifecycle.ts";

const cwd = "/Users/test/project";
const build = (text: string, changedFilePaths: string[] = [], homePath?: string) =>
  buildAssistantPreviewCardsFromReferences(
    text,
    cwd,
    extractAssistantFileReferences(text, cwd, { homePath }),
    { changedFilePaths, homePath },
  );
const paths = (text: string, changes: string[] = [], home?: string) =>
  build(text, changes, home).map(getAssistantPreviewCardFilePath);

describe("ZCode terminal assistant preview candidates", () => {
  it.each([
    '```text\n::zcode-file-citation{path="report.pdf" artifact_kind="pdf"}\n```',
    '`::zcode-file-citation{path="report.pdf" artifact_kind="pdf"}`',
    "```markdown\n[example](./report.pdf)\n```",
    "`[example](./report.pdf)`",
  ])("does not turn quoted citation or link syntax into cards: %s", (text) => {
    expect(extractAssistantFileReferences(text, cwd)).toEqual([]);
  });

  it.each([
    ["`file:///Users/test/project/report.pdf`", `${cwd}/report.pdf`],
    ["`report.pdf`", `${cwd}/report.pdf`],
    ['`"./docs/report.md"`', `${cwd}/docs/report.md`],
  ])("preserves supported inline path references in %s", (text, path) => {
    expect(extractAssistantFileReferences(text, cwd)).toEqual([expect.objectContaining({ path })]);
  });

  it.each(["~//report.pdf", "~///report.pdf", "~\\/report.pdf"])("keeps %s relative to home", (path) => {
    expect(paths(`[home](${path})`, [], "/Users/test")).toEqual(["/Users/test/report.pdf"]);
  });
  it("gates only MD/HTML on this turn's changes, resolving an unambiguous basename", () => {
    expect(paths("report.md page.html old.docx old.xlsx old.pptx old.pdf video.mp4 audio.opus")).toEqual([
      `${cwd}/audio.opus`,
      `${cwd}/video.mp4`,
      `${cwd}/old.pdf`,
      `${cwd}/old.pptx`,
      `${cwd}/old.xlsx`,
      `${cwd}/old.docx`,
    ]);
    expect(paths("report.md page.html", ["out/report.md", "out/page.html"])).toEqual([
      `${cwd}/out/page.html`,
      `${cwd}/out/report.md`,
    ]);
    expect(paths("report.md", ["a/report.md", "b/report.md"])).toEqual([]);
    expect(paths("wrong/report.md", ["out/report.md"])).toEqual([]);
    expect(paths("report.md", [])).toEqual([]);
  });
  it.each([
    ["[报告](<./报告 空格.pdf>)", `${cwd}/报告 空格.pdf`],
    ["`./报告 空格.pdf:12:3`", `${cwd}/报告 空格.pdf`],
    ["“./报告 空格.pdf”", `${cwd}/报告 空格.pdf`],
    ["/Users/test/report.pdf", "/Users/test/report.pdf"],
    ["file:///Users/test/report%20one.pdf", "/Users/test/report one.pdf"],
    ["[encoded](./report%20one.pdf#L2-L5)", `${cwd}/report one.pdf`],
    ["[literal](./report%2520one.pdf)", `${cwd}/report%20one.pdf`],
    ["[Windows](C:\\work\\report.pdf:2)", "C:/work/report.pdf"],
    ["file:///C:/work/report.pdf", "C:/work/report.pdf"],
    ["[home](~/report.pdf)", "/Users/test/report.pdf"],
  ])("resolves %s", (text, expected) => expect(paths(text, [], "/Users/test")).toEqual([expected]));
  it("rejects malformed quotes, unknown home, unsupported directives and external file-looking URLs", () => {
    for (const text of [
      "~/report.pdf",
      "[home](~/report.pdf)",
      "“report.pdf'",
      "[outside](https://example.com/report.pdf)",
      '::zcode-file-citation{path="report.md"}',
      '::zcode-file-citation{path="report.pdf" artifact_kind="presentation"}',
      '::zcode-file-citation{path="report.docx" broken}',
    ])
      expect(build(text, ["report.md"]), text).toEqual([]);
    expect(paths(':::zcode-file-citation{path="报告.docx" artifact_kind="document"}')).toEqual([
      `${cwd}/报告.docx`,
    ]);
    expect(paths(':zcode-file-citation{path="movie.m4v" artifact_kind="video"}')).toEqual([
      `${cwd}/movie.m4v`,
    ]);
  });
  it.each([
    ["http://localhost:3000/path?q=1#part", true],
    ["https://127.0.0.1", true],
    ["http://localhost:0", false],
    ["http://localhost:65536", false],
    ["http://localhost:123456", false],
    ["http://user:pw@localhost:3000", false],
    ["http://localhost.evil.test", false],
    ["https://example.com", false],
    ["http://[::1]:3000", false],
    ["http://127.0.0.1:65535/。", true],
  ])("validates URL %s", (url, valid) => expect(isValidAssistantPreviewWebsiteUrl(url)).toBe(valid));
  it("preserves localhost routes, titles, trailing punctuation, priority, and root URL deduplication", () => {
    const cards = build(
      "page.html [我的网站](http://localhost:3000/app?q=1)。 http://127.0.0.1:4000 http://127.0.0.1:4000/",
      ["page.html"],
    );
    expect(cards).toHaveLength(2);
    expect(cards[1]).toMatchObject({
      type: "website",
      title: "我的网站",
      url: "http://localhost:3000/app?q=1",
    });
    expect(build("http://localhost:3000/page.html", ["page.html"])[0]).toMatchObject({
      filePath: `${cwd}/page.html`,
    });
    expect(build("http://user:pw@localhost:3000 https://example.com")).toEqual([]);
  });
  it("uses the last mention, caps at 15 candidates, stats in one batch and shows at most 10", async () => {
    const text = `${Array.from({ length: 20 }, (_, i) => `file${i}.pdf`).join(" ")} file4.pdf`;
    const candidates = build(text);
    expect(candidates).toHaveLength(15);
    expect(candidates[0].title).toBe("file4.pdf");
    const checkFilesExist = vi.fn(async ({ paths }: { paths: string[] }) =>
      paths.map((path, i) => ({ path, exists: i >= 3 })),
    );
    const visible = await resolveValidatedAssistantPreviewCards(candidates, { checkFilesExist });
    expect(checkFilesExist).toHaveBeenCalledTimes(1);
    expect(visible).toHaveLength(10);
    expect(visible[0].title).toBe("file17.pdf");
    const pure = await resolveValidatedAssistantPreviewCards(build("http://localhost:1"), {
      checkFilesExist,
    });
    expect(pure).toHaveLength(1);
    expect(checkFilesExist).toHaveBeenCalledTimes(1);
  });
  it("all registered media extensions use their actual kind and no changed-file gate", () => {
    for (const ext of ["mp4", "mov", "webm", "m4v", "mp3", "wav", "m4a", "ogg", "opus", "flac", "weba"]) {
      expect(build(`out.${ext}`)[0]).toMatchObject({
        type: "file",
        kind: ["mp4", "mov", "webm", "m4v"].includes(ext) ? "video" : "audio",
      });
    }
    expect(build("image.png text.txt")).toEqual([]);
  });
});

it("coalesces semantic terminal requests and fails closed without guessing changes", async () => {
  const getChangedPaths = vi.fn(async () => {
    throw new Error("unavailable");
  });
  const checkFilesExist = vi.fn(async ({ paths }: { paths: string[] }) =>
    paths.map((path) => ({ path, exists: !path.endsWith("missing.pdf") })),
  );
  const loader = createPreviewLoader({ getChangedPaths, checkFilesExist });
  const input = { sessionId: "a", runId: "r", text: "report.md ok.pdf missing.pdf", cwd };
  const [one, two] = await Promise.all([loader.load(input), loader.load({ ...input })]);
  expect(one).toEqual(two);
  expect(one.map((card) => card.title)).toEqual(["ok.pdf"]);
  expect(getChangedPaths).toHaveBeenCalledTimes(1);
  expect(checkFilesExist).toHaveBeenCalledTimes(1);
  await loader.load({ ...input, sessionId: "b" });
  expect(getChangedPaths).toHaveBeenCalledTimes(2);
});

it("rechecks mutable filesystem existence when a historical turn is mounted again", async () => {
  let exists = true;
  const checkFilesExist = vi.fn(async ({ paths }: { paths: string[] }) =>
    paths.map((path) => ({ path, exists })),
  );
  const loader = createPreviewLoader({ getChangedPaths: async () => [], checkFilesExist });
  const input = { sessionId: "a", runId: "r", text: "ok.pdf", cwd };
  expect(await loader.load(input)).toHaveLength(1);
  exists = false;
  expect(await loader.load(input)).toEqual([]);
  expect(checkFilesExist).toHaveBeenCalledTimes(2);
});

it("arms PPTX only on an observed running edge, clears on session switches and interruption", () => {
  const idle = { scope: "", armed: false };
  expect(advancePreviewAutoOpen(idle, "a", "completed", "r").target).toBeNull();
  const running = advancePreviewAutoOpen(idle, "a", "running", "r").state;
  expect(advancePreviewAutoOpen(running, "a", "completed", "r").target).toBe("a:r");
  expect(advancePreviewAutoOpen(running, "b", "completed", "r").target).toBeNull();
  expect(advancePreviewAutoOpen(running, "a", "aborted", "r").target).toBeNull();
});