Z ZPI 文档 v0.3.8
介绍页

源码

tests/desktop/tool-presentation.spec.ts

typescript476 行查看原文
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ElectronApplication } from "@playwright/test";
import { chunk, deferred, 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";

const call = (id: string, name: string, args: Record<string, unknown>, index = 0) => ({
  index,
  id,
  type: "function",
  function: { name, arguments: JSON.stringify(args) },
});

test("long grouped commands keep the running label separate while rolling and at narrow widths", async () => {
  const dir = await mkdtemp(join(tmpdir(), "zpi-tool-summary-spacing-"));
  const project = join(dir, "project");
  await mkdir(project);
  await writeFile(join(project, "a.ts"), "export const a = 1;\n");
  const finish = deferred();
  const path = "trusteeship-performance-model/src/main/java/com/ke/".repeat(8);
  const exploreCommand = `git log --since="2025-09-01" --pretty=format:"%an" -- ${path}; while [ ! -f explore-release ]; do sleep 0.05; done`;
  const executeCommand = `printf '%s' '${path}'; while [ ! -f execute-release ]; do sleep 0.05; done`;
  const server = await fakeServer(async (_, response, index) => {
    if (index === 0) {
      send(response, chunk({ tool_calls: [call("read", "read", { path: "a.ts" })] }));
    } else if (index === 1) {
      send(response, chunk({ tool_calls: [call("explore", "bash", { command: exploreCommand })] }));
    } else if (index === 2) {
      send(response, chunk({ reasoning_content: "查阅完成,执行命令。" }));
      send(
        response,
        chunk({
          tool_calls: [
            call("execute-first", "bash", { command: "printf first" }),
            call("execute-long", "bash", { command: executeCommand }, 1),
          ],
        }),
      );
    } else {
      await finish.promise;
      send(response, chunk({ content: "完成。" }));
      done(response);
      return;
    }
    done(response, "tool_calls");
  });
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, project, url: server.url });
    const page = await app.firstWindow();
    await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
    await page.getByLabel("消息", { exact: true }).fill("检查长命令摘要间距");
    await page.getByLabel("发送", { exact: true }).click();
    for (const [kind, command] of [
      ["explore", exploreCommand],
      ["execute", executeCommand],
    ] as const) {
      const group = page.locator(`[data-tool-group="${kind}"]`);
      const summary = group.locator(":scope > .tool-layout > .tool-summary-row");
      await expect(summary.locator(".tool-command-summary").last()).toHaveText(command);
      await expect(group.locator("..")).toHaveAttribute("data-stream-entrance", "true");
      await expect(group.locator("..")).toHaveCSS("animation-duration", "0.9s");
      // Sample rendered geometry throughout the rolling transition and hold.
      const gaps = await summary.evaluate(async (el, command) => {
        const gaps: number[] = [];
        const start = performance.now();
        while (performance.now() - start < 900) {
          for (const code of el.querySelectorAll(".tool-command-summary")) {
            if (code.textContent !== command) continue;
            const label =
              code.previousElementSibling?.querySelector(".tool-active-label") ?? code.previousElementSibling;
            if (label) gaps.push(code.getBoundingClientRect().left - label.getBoundingClientRect().right);
          }
          await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
        }
        return gaps;
      }, command);
      expect(gaps.length).toBeGreaterThan(0);
      expect(Math.min(...gaps)).toBeGreaterThanOrEqual(7);
      for (const reducedMotion of ["no-preference", "reduce"] as const) {
        await page.emulateMedia({ reducedMotion });
        for (const width of [700, 350]) {
          await page.locator(".conversation").evaluate((el, width) => {
            (el as HTMLElement).style.width = `${width}px`;
          }, width);
          const label = summary.locator(".tool-active-label");
          const code = summary.locator(".tool-command-summary");
          await expect(label).toHaveText("正在执行");
          await expect(code).toHaveText(command);
          const labelBox = await label.boundingBox();
          const commandBox = await code.boundingBox();
          expect(labelBox).not.toBeNull();
          expect(commandBox).not.toBeNull();
          if (!labelBox || !commandBox) throw new Error("Missing running label or command");
          expect(commandBox.x - (labelBox.x + labelBox.width)).toBeGreaterThanOrEqual(7);
          expect(await code.evaluate((el) => el.scrollWidth)).toBeGreaterThan(commandBox.width);
          expect(await summary.evaluate((el) => el.scrollWidth - el.clientWidth)).toBeLessThanOrEqual(1);
        }
      }
      await page.screenshot({ path: `test-results/tool-summary-${kind}-long-command.png` });
      await summary.click();
      await expect(summary).not.toContainText("正在执行");
      await summary.click();
      await expect(summary.locator(".tool-active-label")).toHaveText("正在执行");
      await page.locator(".conversation").evaluate((el) => {
        (el as HTMLElement).style.width = "";
      });
      await page.emulateMedia({ reducedMotion: "no-preference" });
      await writeFile(join(project, `${kind}-release`), "");
    }
    finish.resolve();
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
    await page.getByLabel("新建任务", { exact: true }).first().click();
    await page.locator(".session-row").first().click();
    if (!(await page.locator('[data-tool-group="explore"]').isVisible())) {
      await page.getByTestId("progress").click();
    }
    await expect(page.locator(".tool-group")).toHaveCount(2);
    await expect(page.locator('.process [data-stream-entrance="true"]')).toHaveCount(0);
  } finally {
    finish.resolve();
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
      () => writeFile(join(project, "execute-release"), ""),
      () => writeFile(join(project, "explore-release"), ""),
    ]);
  }
});

test("ZCode tool summaries group by phase, preserve expansion and open per-operation diffs", async () => {
  test.setTimeout(60_000);
  const dir = await mkdtemp(join(tmpdir(), "zpi-tool-presentation-"));
  const project = join(dir, "project");
  await mkdir(join(project, "src"), { recursive: true });
  await writeFile(join(project, "src/a.ts"), "const value = 'original';\n");
  await writeFile(join(project, "src/b.ts"), "export const b = 2;\n");
  const second = deferred(),
    third = deferred(),
    execute = deferred(),
    edit = deferred(),
    finish = deferred();
  const server = await fakeServer(async (_, response, index) => {
    if (index === 0) send(response, chunk({ tool_calls: [call("read-a", "read", { path: "src/a.ts" })] }));
    else if (index === 1) {
      await second.promise;
      send(response, chunk({ tool_calls: [call("read-b", "read", { path: "src/b.ts" })] }));
    } else if (index === 2) {
      await third.promise;
      send(response, chunk({ tool_calls: [call("missing", "read", { path: "src/missing.ts" })] }));
    } else if (index === 3) {
      send(response, chunk({ reasoning_content: "读取阶段结束。" }));
      await execute.promise;
      send(
        response,
        chunk({
          tool_calls: [
            call("hello", "bash", { command: "printf hello" }),
            call("empty", "bash", { command: "true" }, 1),
          ],
        }),
      );
    } else if (index === 4) {
      await edit.promise;
      send(response, chunk({ reasoning_content: "终端阶段结束。" }));
      send(
        response,
        chunk({
          tool_calls: [
            call("edit-first", "edit", {
              path: "src/a.ts",
              edits: [{ oldText: "original", newText: "intermediate" }],
            }),
            call(
              "edit-second",
              "edit",
              { path: "src/a.ts", edits: [{ oldText: "intermediate", newText: "final" }] },
              1,
            ),
          ],
        }),
      );
    } else {
      send(response, chunk({ content: "检查和编辑完成。" }));
      await finish.promise;
      done(response);
      return;
    }
    done(response, "tool_calls");
  });
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, project, url: server.url });
    const page = await app.firstWindow();
    await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
    await page.getByLabel("消息", { exact: true }).fill("验证工具摘要");
    await page.getByLabel("发送", { exact: true }).click();
    const tools = page.getByTestId("tool-block");
    await expect(tools).toHaveCount(1);
    await expect(tools.first().getByTestId("tool-summary")).toHaveText("读取a.tssrc/");
    await expect(tools.first().getByTestId("tool-summary")).not.toHaveAttribute("role", "button");
    await expect(tools.first().locator(".process-chevron")).toHaveCount(0);
    await expect(tools.first().locator(".file-type-icon")).toHaveAttribute("src", /typescript\.svg$/);
    await tools.first().getByRole("button", { name: "a.ts", exact: true }).click();
    await expect(page.locator(".file-text-preview")).toContainText("original");
    await page.getByLabel("收起右侧栏", { exact: true }).click();
    second.resolve();
    const explore = page.locator('[data-tool-group="explore"]');
    const summary = explore.locator(":scope > .tool-layout > .tool-summary-row");
    await expect(summary).toContainText("查阅·正在读取b.tssrc/");
    await expect(summary).toHaveAttribute("aria-expanded", "false");
    await summary.focus();
    await summary.press("Enter");
    await expect(summary).toHaveText("查阅·2 文件");
    await expect(explore.locator(".tool-group-children > .tool-block")).toHaveCount(2);
    await expect(explore.locator(".tool-block .process-icon")).toHaveCount(0);
    await explore.getByRole("button", { name: "a.ts", exact: true }).click();
    await expect(summary).toHaveAttribute("aria-expanded", "true");
    await page.getByLabel("收起右侧栏", { exact: true }).click();
    third.resolve();
    await expect(explore.locator(".tool-group-children > .tool-block")).toHaveCount(3);
    await expect(summary).toHaveAttribute("aria-expanded", "true");
    await expect(summary).toHaveText("查阅·3 文件");
    await expect(page.getByTestId("thinking-block")).toHaveCount(1);
    await expect
      .poll(() => explore.locator(".tool-content-shell").evaluate((el) => el.getAnimations().length))
      .toBe(0);
    await explore.locator(".tool-failure").hover();
    await expect(page.locator(".tool-error-content")).toContainText("missing.ts");
    await page.getByRole("button", { name: "复制错误详情", exact: true }).click();
    await expect.poll(() => app?.evaluate(({ clipboard }) => clipboard.readText())).toContain("missing.ts");
    await expect(summary).toHaveAttribute("aria-expanded", "true");
    await summary.focus();
    await summary.press("Space");
    await expect(summary).toHaveText("查阅·3 文件");
    await expect(summary.locator(".thinking-label-streaming")).toHaveCount(0);
    execute.resolve();
    const terminal = page.locator('[data-tool-group="execute"]');
    const terminalSummary = terminal.locator(":scope > .tool-layout > .tool-summary-row");
    await expect(terminalSummary).toContainText("终端·正在执行true");
    await terminalSummary.click();
    await expect(terminalSummary).toHaveText("终端·2 个命令");
    const commands = terminal.locator(".tool-block");
    await commands.nth(0).getByTestId("tool-summary").click();
    await expect(commands.nth(0).getByTestId("tool-summary")).toHaveText("终端");
    await expect(commands.nth(0).locator(".tool-command")).toHaveText("printf hello");
    await expect(commands.nth(0).locator(".tool-output")).toHaveText("hello");
    await commands.nth(1).getByTestId("tool-summary").click();
    await expect(commands.nth(1).locator(".tool-no-output")).toHaveText("没有输出。");
    edit.resolve();
    const edits = page.locator('[data-tool-name="edit"]');
    await expect(edits).toHaveCount(2);
    await expect(edits.first().locator(".tool-diff-counts")).toHaveText("+1-1");
    await expect(
      edits.first().locator(".tool-diff-counts").getByRole("img", { name: "1" }).first(),
    ).toBeVisible();
    await expect(edits.first().getByTestId("tool-summary")).toHaveAttribute("aria-expanded", "false");
    await edits.first().locator(".tool-kind-label").click();
    await expect(edits.first().locator(".tool-inline-diff")).toContainText("original");
    await expect(edits.first().locator(".tool-inline-diff")).toContainText("intermediate");
    await expect(edits.first().locator(".tool-diff-line.added > code")).not.toContainText("+");
    await expect.poll(() => edits.first().locator(".tool-diff-line code span").count()).toBeGreaterThan(0);
    for (const [index, before, after] of [
      [0, "original", "intermediate"],
      [1, "intermediate", "final"],
    ] as const) {
      await edits.nth(index).getByRole("button", { name: "a.ts", exact: true }).click();
      const diff = page.getByRole("tabpanel", { name: "a.ts", exact: true }).locator("diffs-container");
      await expect(diff).toContainText(before);
      await expect(diff).toContainText(after);
      await expect(diff).not.toContainText(index === 0 ? "final" : "original");
      await page.getByLabel("收起右侧栏", { exact: true }).click();
    }
    await edits.last().locator(".tool-kind-label").click();
    await edits.last().scrollIntoViewIfNeeded();
    await page.screenshot({ path: "test-results/tool-presentation-expanded.png" });
    await page.emulateMedia({ colorScheme: "dark" });
    await expect(edits.first().locator(".tool-diff-counts .diff-added")).toHaveCSS(
      "color",
      "rgb(70, 191, 114)",
    );
    await page.screenshot({ path: "test-results/tool-presentation-dark.png" });
    await page.emulateMedia({ colorScheme: "light" });
    await app.evaluate(({ BrowserWindow }) => {
      const window = BrowserWindow.getAllWindows()[0];
      window?.setSize(840, 720);
    });
    // The desktop reserves at least 380px for the chat; exercise the shared row's
    // ZCode container rule below 360px without changing that workbench constraint.
    await page.locator(".conversation").evaluate((el) => {
      (el as HTMLElement).style.width = "350px";
    });
    await expect
      .poll(() => page.locator(".conversation").evaluate((el) => el.clientWidth))
      .toBeLessThanOrEqual(360);
    await expect(edits.first().locator(".tool-file-directory")).toBeHidden();
    await expect(edits.first().locator(".tool-kind-label")).toBeHidden();
    await expect(edits.first().getByRole("button", { name: "a.ts", exact: true })).toBeVisible();
    await expect
      .poll(() => page.getByTestId("process").evaluate((el) => el.scrollWidth - el.clientWidth))
      .toBe(0);
    await page.screenshot({ path: "test-results/tool-presentation-narrow.png" });
    await page.locator(".conversation").evaluate((el) => {
      (el as HTMLElement).style.width = "";
    });
    finish.resolve();
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
    await page.getByTestId("progress").click();
    await expect(summary).toHaveText("查阅·3 文件");
    await expect(terminalSummary).toHaveText("终端·2 个命令");
  } finally {
    for (const gate of [second, third, execute, edit, finish]) gate.resolve();
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
    ]);
  }
});

test("terminal output follows live data, freezes while reading and preserves completed position", async () => {
  test.setTimeout(60_000);
  const dir = await mkdtemp(join(tmpdir(), "zpi-tool-output-"));
  const project = join(dir, "project");
  await mkdir(project);
  const finish = deferred();
  const server = await fakeServer(async (_, response, index) => {
    if (index === 0) {
      send(
        response,
        chunk({
          tool_calls: [
            call("output", "bash", {
              command:
                "for i in $(seq 1 40); do echo line-$i; sleep 0.15; done; while [ ! -f release ]; do sleep 0.1; done; echo final-line",
            }),
          ],
        }),
      );
      done(response, "tool_calls");
    } else {
      await finish.promise;
      send(response, chunk({ content: "done" }));
      done(response);
    }
  });
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, project, url: server.url });
    const page = await app.firstWindow();
    await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
    await page.getByLabel("消息", { exact: true }).fill("输出多行");
    await page.getByLabel("发送", { exact: true }).click();
    const tool = page.getByTestId("tool-block");
    await tool.getByTestId("tool-summary").click();
    const viewport = tool.getByTestId("bash-output-scroll");
    await expect(viewport).toContainText("line-12");
    await expect(viewport).toHaveAttribute("data-following", "true");
    await expect(viewport).toHaveAttribute("data-scroll-mask", "top");
    expect(await viewport.evaluate((el) => el.clientHeight)).toBeLessThanOrEqual(100);
    await viewport.evaluate((el) => {
      el.scrollTop = 0;
      el.dispatchEvent(new Event("scroll", { bubbles: true }));
    });
    await expect(viewport).toHaveAttribute("data-following", "false");
    const frozen = await viewport.textContent();
    // The command finishes emitting its first batch while the reader stays at the top.
    await writeFile(join(project, "release"), "");
    await expect.poll(() => server.requests.length).toBe(2);
    await expect(viewport).toHaveText(frozen ?? "");
    expect(await viewport.evaluate((el) => el.scrollTop)).toBe(0);
    await viewport.evaluate((el) => {
      el.scrollTop = el.scrollHeight;
      el.dispatchEvent(new Event("scroll", { bubbles: true }));
    });
    await expect(viewport).toHaveAttribute("data-following", "true");
    await expect(viewport).toContainText("final-line");
    await expect(viewport).toHaveAttribute("data-scroll-mask", "top");
    await viewport.evaluate((el) => {
      el.scrollTop = 0;
      el.dispatchEvent(new Event("scroll", { bubbles: true }));
    });
    await expect(viewport).toHaveAttribute("data-following", "false");
    finish.resolve();
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
    await page.getByTestId("progress").click();
    await expect(viewport).toHaveAttribute("data-following", "true");
    expect(await viewport.evaluate((el) => el.scrollTop)).toBe(0);
    await page.screenshot({ path: "test-results/tool-output-frozen.png" });
    await app.close();
    app = await launchDesktop({ dir, project, url: server.url });
    const history = await app.firstWindow();
    await history.getByTestId("progress").click();
    await history.getByTestId("tool-summary").click();
    const historicalOutput = history.getByTestId("bash-output-scroll");
    await expect(historicalOutput).toContainText("final-line");
    expect(await historicalOutput.evaluate((el) => el.scrollTop)).toBe(0);
    await expect(historicalOutput).toHaveAttribute("data-scroll-mask", "bottom");
  } finally {
    finish.resolve();
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
      () => writeFile(join(project, "release"), ""),
    ]);
  }
});

test("write previews load large per-operation patches and failed edits retain readable parameters", async () => {
  const dir = await mkdtemp(join(tmpdir(), "zpi-tool-file-preview-"));
  const project = join(dir, "project");
  await mkdir(project);
  const finish = deferred();
  const content = Array.from({ length: 2200 }, (_, i) => `// item_${i} 中文内容中文内容`).join("\n");
  const server = await fakeServer(async (_, response, index) => {
    if (index < 2) {
      const tool =
        index === 0
          ? call("large-write", "write", { path: "large.ts", content })
          : call("failed-edit", "edit", {
              path: "large.ts",
              edits: [{ oldText: "absent-original", newText: "replacement" }],
            });
      send(response, chunk({ tool_calls: [tool] }));
      done(response, "tool_calls");
    } else {
      await finish.promise;
      send(response, chunk({ content: "done" }));
      done(response);
    }
  });
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, project, url: server.url });
    const page = await app.firstWindow();
    await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
    await page.getByLabel("消息", { exact: true }).fill("写入并尝试编辑");
    await page.getByLabel("发送", { exact: true }).click();
    await expect.poll(() => server.requests.length).toBe(3);
    const write = page.locator('[data-tool-name="write"]');
    await expect(write.getByTestId("tool-summary")).toContainText("写入large.ts+2200");
    await write.locator(".tool-kind-label").click();
    await expect(write.locator(".tool-inline-diff")).toContainText("item_0");
    await expect(write.locator(".tool-inline-diff")).toContainText("item_2199");
    await expect(write.locator(".tool-diff-omitted")).toContainText("Diff 预览已截断");
    expect(await write.locator(".tool-diff-line").count()).toBeLessThanOrEqual(799);
    expect(await write.locator(".tool-inline-diff").evaluate((el) => el.clientHeight)).toBeLessThanOrEqual(
      240,
    );
    const edit = page.locator('[data-tool-name="edit"]');
    await expect(edit.getByTestId("tool-summary")).toContainText("执行失败");
    await expect(edit.getByTestId("tool-summary")).toHaveAttribute("aria-expanded", "false");
    await edit.locator(".tool-kind-label").click();
    await expect(edit.locator(".tool-parameters h4")).toHaveText("Parameters");
    await expect(edit.locator(".tool-parameters h4")).toHaveCSS("text-transform", "uppercase");
    await expect(edit.locator(".tool-input-code")).toContainText("absent-original");
    await expect(edit.locator(".tool-detail-error")).toContainText("Error");
    await expect(edit.locator(".tool-detail-error")).toContainText("Could not find");
    await page.screenshot({ path: "test-results/tool-file-failure.png" });
    finish.resolve();
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
  } finally {
    finish.resolve();
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
    ]);
  }
});