Z ZPI 文档 v0.3.8
介绍页

源码

tests/desktop/preview-lifecycle.spec.ts

typescript242 行查看原文
import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ElectronApplication } from "@playwright/test";
import { chunk, done, fakeServer, send } from "../fake-server.ts";
import { runCleanup } from "../helpers/cleanup.ts";
import { launchDesktop } from "../helpers/desktop.ts";
import { expect, test } from "../helpers/test.ts";

test("references from earlier assistant segments produce one card below the final answer", async () => {
  const dir = await realpath(await mkdtemp(join(tmpdir(), "zpi-card-turn-")));
  const project = join(dir, "project");
  await mkdir(project);
  await writeFile(join(project, "report.pdf"), "pdf");
  const server = await fakeServer((body, response) => {
    const messages = body.messages as unknown as { role: string }[];
    if (!messages.some((message) => message.role === "tool")) {
      send(response, chunk({ content: "可以查看 report.pdf,我先检查工作目录。" }));
      send(
        response,
        chunk({
          tool_calls: [
            {
              index: 0,
              id: "pwd",
              type: "function",
              function: { name: "bash", arguments: JSON.stringify({ command: "pwd" }) },
            },
          ],
        }),
      );
      done(response, "tool_calls");
    } else {
      send(response, chunk({ content: "处理完成。" }));
      done(response);
    }
  });
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, project, url: server.url });
    const page = await app.firstWindow();
    await page.getByLabel("添加项目", { exact: true }).first().click();
    await page.getByLabel("消息", { exact: true }).fill("检查");
    await page.getByLabel("消息", { exact: true }).press("Enter");
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
    await expect(page.locator(".assistant-preview-title")).toHaveText("report.pdf");
    const answer = page.locator(".answer").last();
    await expect(answer).toHaveText("处理完成。");
    const card = page.locator(".assistant-preview-card");
    await expect(card).toHaveCount(1);
    const a = await answer.boundingBox();
    const c = await card.boundingBox();
    expect(a).not.toBeNull();
    expect(c?.y).toBeGreaterThanOrEqual((a?.y ?? 0) + (a?.height ?? 0));
  } finally {
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
    ]);
  }
});

test("delayed file checks stay with their task, stable projections share checks and interruption reveals cards", async () => {
  const dir = await realpath(await mkdtemp(join(tmpdir(), "zpi-card-races-")));
  const project = join(dir, "project");
  await mkdir(project);
  await writeFile(join(project, "a.pdf"), "pdf");
  await writeFile(join(project, "b.pdf"), "pdf");
  const server = await fakeServer((body, response) => {
    const user = (body.messages as unknown as { role: string; content: string }[])
      .filter((m) => m.role === "user")
      .at(-1)?.content;
    send(response, chunk({ content: user === "B" ? "b.pdf" : "a.pdf" }));
    if (user !== "中断") done(response);
  });
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, project, url: server.url });
    const page = await app.firstWindow();
    await app.evaluate(({ ipcMain }) => {
      const bus = ipcMain as typeof ipcMain & {
        _invokeHandlers: Map<string, (...args: unknown[]) => Promise<unknown>>;
      };
      const original = bus._invokeHandlers.get("zpi:call");
      if (!original) throw new Error("missing IPC handler");
      const state = { calls: [] as string[], release: undefined as (() => void) | undefined };
      (globalThis as typeof globalThis & { previewRace: typeof state }).previewRace = state;
      ipcMain.removeHandler("zpi:call");
      ipcMain.handle("zpi:call", async (event, method, args) => {
        const result = await original(event, method, args);
        if (method === "checkPreviewFiles") {
          state.calls.push(args[0]);
          if (state.calls.length === 1)
            await new Promise<void>((resolve) => {
              state.release = resolve;
            });
        }
        return result;
      });
    });
    await page.getByLabel("添加项目", { exact: true }).first().click();
    const editor = page.getByLabel("消息", { exact: true });
    await editor.fill("A");
    await editor.press("Enter");
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
    const a = await page.locator(".session-row.active").getAttribute("data-session-id");
    await expect
      .poll(() =>
        app?.evaluate(
          () =>
            (globalThis as typeof globalThis & { previewRace: { calls: string[] } }).previewRace.calls.length,
        ),
      )
      .toBe(1);
    await expect(page.locator(".assistant-preview-card")).toHaveCount(0);
    await page.locator(".project-title").hover();
    await page.getByLabel("新建任务 project", { exact: true }).click();
    await editor.fill("B");
    await editor.press("Enter");
    await expect(page.locator(".assistant-preview-title")).toHaveText("b.pdf");
    await app.evaluate(() =>
      (globalThis as typeof globalThis & { previewRace: { release?: () => void } }).previewRace.release?.(),
    );
    await page.getByLabel("筛选视图", { exact: true }).click();
    await page.getByRole("menuitemradio", { name: "最近更新" }).click();
    await expect(page.locator(".assistant-preview-title")).toHaveText("b.pdf");
    await page.locator(`[data-session-id="${a}"] .session-name`).click();
    await expect(page.locator(".assistant-preview-title")).toHaveText("a.pdf");
    expect(
      await app.evaluate(
        () =>
          (globalThis as typeof globalThis & { previewRace: { calls: string[] } }).previewRace.calls.length,
      ),
    ).toBe(3);
    await editor.fill("中断");
    await editor.press("Enter");
    await expect(page.getByTestId("run").last().locator(".answer")).toHaveText("a.pdf");
    await expect(page.getByTestId("run").last().locator(".assistant-preview-card")).toHaveCount(0);
    await page.getByLabel("停止", { exact: true }).click();
    await expect(page.getByTestId("run").last()).toHaveAttribute("data-status", "aborted");
    await expect(page.getByTestId("run").last().locator(".assistant-preview-title")).toHaveText("a.pdf");
    await page.getByRole("button", { name: "B", exact: true }).click();
    await rm(join(project, "a.pdf"));
    await page.locator(`[data-session-id="${a}"] .session-name`).click();
    await expect
      .poll(() =>
        app?.evaluate(
          () =>
            (globalThis as typeof globalThis & { previewRace: { calls: string[] } }).previewRace.calls.length,
        ),
      )
      .toBe(7);
    await expect(page.locator(".assistant-preview-card")).toHaveCount(0);
  } finally {
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
    ]);
  }
});

test("rewind removes old turn cards and failed change reads suppress only gated formats", async () => {
  const dir = await realpath(await mkdtemp(join(tmpdir(), "zpi-card-rewind-")));
  const project = join(dir, "project");
  await mkdir(project);
  await writeFile(join(project, "ok.pdf"), "pdf");
  const server = await fakeServer((body, response) => {
    const messages = body.messages as unknown as { role: string; content: string }[];
    const user = messages.filter((m) => m.role === "user").at(-1)?.content;
    if (user === "生成" && !messages.some((m) => m.role === "tool")) {
      send(
        response,
        chunk({
          tool_calls: [
            {
              index: 0,
              id: "write",
              type: "function",
              function: {
                name: "write",
                arguments: JSON.stringify({ path: "out.html", content: "<h1>preview</h1>" }),
              },
            },
          ],
        }),
      );
      done(response, "tool_calls");
    } else {
      send(response, chunk({ content: "out.html ok.pdf" }));
      done(response);
    }
  });
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, project, url: server.url });
    const page = await app.firstWindow();
    await page.getByLabel("添加项目", { exact: true }).first().click();
    const editor = page.getByLabel("消息", { exact: true });
    await editor.fill("生成");
    await editor.press("Enter");
    await expect(page.locator(".assistant-preview-card")).toHaveCount(2);
    const id = await page.locator(".session-row.active").getAttribute("data-session-id");
    const runId = await page.getByTestId("run").getAttribute("data-run-id");
    const result = await page.evaluate(
      async ({ id, runId }) =>
        window.zpi.editUserMessage(id, runId, {
          text: "讨论",
          fileReferences: [],
          attachments: [],
          workspaceMode: "rewind",
        }),
      { id: id ?? "", runId: runId ?? "" },
    );
    expect(result.ok).toBe(true);
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
    await expect(page.locator(".assistant-preview-title")).toHaveText("ok.pdf");
    await app.evaluate(({ ipcMain }) => {
      const bus = ipcMain as typeof ipcMain & {
        _invokeHandlers: Map<string, (...args: unknown[]) => Promise<unknown>>;
      };
      const original = bus._invokeHandlers.get("zpi:call");
      if (!original) throw new Error("missing IPC handler");
      ipcMain.removeHandler("zpi:call");
      ipcMain.handle("zpi:call", (event, method, args) =>
        method === "getChanges"
          ? { ok: false, error: { code: "storage", message: "fixture snapshot unavailable" } }
          : original(event, method, args),
      );
    });
    await page.reload();
    await expect(page.locator(".assistant-preview-title")).toHaveText("ok.pdf");
  } finally {
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
    ]);
  }
});