Z ZPI 文档 v0.3.8
介绍页

源码

tests/desktop/compaction.spec.ts

typescript171 行查看原文
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 { testStorageDirectory } from "../helpers/storage.ts";
import { expect, test } from "../helpers/test.ts";

test("automatic overflow compaction uses the same divider and stays visible after the work is collapsed", async () => {
  const dir = await mkdtemp(join(tmpdir(), "zpi-auto-compaction-divider-"));
  const agentDir = join(testStorageDirectory(dir), "agent");
  await mkdir(agentDir, { recursive: true });
  await writeFile(join(agentDir, "settings.json"), JSON.stringify({ compaction: { keepRecentTokens: 0 } }));
  const release = deferred();
  let recovered = false;
  const server = await fakeServer(async (body, response) => {
    const messages = body.messages as unknown as { role: string; content: string }[];
    if (JSON.stringify(messages).includes("ONLY output the structured summary")) {
      await release.promise;
      send(response, chunk({ content: "Earlier work summary" }));
      recovered = true;
    } else if (
      !recovered &&
      messages.findLast((message) => message.role === "user")?.content === "overflow"
    ) {
      response.writeHead(400, { "content-type": "application/json" });
      response.end(JSON.stringify({ error: { message: "maximum context length exceeded" } }));
      return;
    } else send(response, chunk({ content: "完整回复" }));
    done(response);
  });
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, url: server.url });
    const page = await app.firstWindow();
    await page.getByLabel("消息", { exact: true }).fill("first");
    await page.getByLabel("发送", { exact: true }).click();
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
    await page.getByLabel("消息", { exact: true }).fill("overflow");
    await page.getByLabel("发送", { exact: true }).click();
    const marker = page.getByTestId("compaction-divider");
    await expect(marker).toHaveAttribute("data-status", "running");
    await expect(marker).toHaveAttribute("data-origin", "auto");
    await expect(marker).toHaveText("正在压缩上下文");
    await expect(page.locator(".chat-loading-slot")).toHaveCount(0);
    release.resolve();
    await expect(page.getByTestId("run").last()).toHaveAttribute("data-status", "completed");
    await expect(marker).toHaveText("上下文已自动压缩");
    await expect(marker).toBeVisible();
    await expect(page.getByTestId("process")).toHaveCount(0);
    await page.reload();
    await expect(marker).toHaveText("上下文已自动压缩");
    await expect(page.locator(".user-message-text")).toHaveText(["first", "overflow"]);
  } finally {
    release.resolve();
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
    ]);
  }
});

for (const outcome of ["completed", "error", "aborted"] as const) {
  test(`manual compaction is a timeline divider, persists ${outcome} and preserves chat history`, async () => {
    const dir = await mkdtemp(join(tmpdir(), "zpi-compaction-divider-"));
    const agentDir = join(testStorageDirectory(dir), "agent");
    await mkdir(agentDir, { recursive: true });
    await writeFile(join(agentDir, "settings.json"), JSON.stringify({ compaction: { keepRecentTokens: 0 } }));
    const release = deferred();
    const server = await fakeServer(async (body, response) => {
      const compacting = JSON.stringify(body.messages).includes("ONLY output the structured summary");
      if (compacting) {
        await release.promise;
        if (outcome === "error") {
          response.writeHead(400, { "content-type": "application/json" });
          response.end(JSON.stringify({ error: { message: "Synthetic summary failure" } }));
          return;
        }
        send(response, chunk({ content: "Earlier work summary" }));
      } else {
        send(response, chunk({ content: "完整回复" }));
        send(response, {
          ...chunk({}),
          choices: [],
          usage: { prompt_tokens: 2048, completion_tokens: 8, total_tokens: 2056 },
        });
      }
      done(response);
    });
    let app: ElectronApplication | undefined;
    try {
      app = await launchDesktop({ dir, url: server.url });
      const page = await app.firstWindow();
      await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.setSize(1200, 900));
      for (const [index, text] of ["first", "second"].entries()) {
        await page.getByLabel("消息", { exact: true }).fill(text);
        await page.getByLabel("发送", { exact: true }).click();
        await expect(page.getByTestId("run")).toHaveCount(index + 1);
        await expect(page.getByTestId("run").last()).toHaveAttribute("data-status", "completed");
      }
      await page.getByLabel("消息", { exact: true }).fill("/compact keep decisions");
      await page.getByLabel("发送", { exact: true }).click();
      const marker = page.getByTestId("compaction-divider");
      await expect(marker).toHaveAttribute("data-status", "running");
      await expect(marker).toHaveText("正在压缩上下文");
      await expect(marker.locator("svg")).toHaveCount(0);
      await expect(marker.locator(".thinking-label-streaming")).toBeVisible();
      await expect(page.getByTestId("run")).toHaveCount(2);
      await expect(page.locator(".user-message-text")).toHaveText(["first", "second"]);
      await expect(page.locator(".chat-loading-slot")).toHaveCount(0);
      await expect(page.getByLabel("消息", { exact: true })).toHaveAttribute(
        "data-placeholder",
        "继续输入以排队后续修改",
      );
      if (outcome === "completed") {
        await page.screenshot({ path: "test-results/compaction-running.png" });
        await page.getByLabel("消息", { exact: true }).fill("queued follow-up");
        await page.getByLabel("加入队列", { exact: true }).click();
        await expect(marker).toHaveAttribute("data-status", "running");
      }
      if (outcome === "aborted") await page.getByLabel("停止", { exact: true }).click();
      else release.resolve();
      await expect(marker).toHaveAttribute("data-status", outcome);
      await expect(marker).toHaveText(
        outcome === "completed"
          ? "上下文已压缩"
          : outcome === "error"
            ? "上下文压缩失败"
            : "上下文压缩已中断",
      );
      await expect(marker.locator("svg.lucide-archive")).toHaveCount(1);
      await expect(marker.locator(".thinking-label-streaming")).toHaveCount(0);
      if (outcome === "completed") {
        await expect(page.getByTestId("run")).toHaveCount(3);
        await expect(page.getByTestId("run").last()).toHaveAttribute("data-status", "completed");
        await expect(page.getByLabel("消息", { exact: true })).toHaveAttribute(
          "data-placeholder",
          "提出后续修改要求",
        );
        await page.screenshot({ path: "test-results/compaction-completed.png" });
      }
      await page.reload();
      await expect(marker).toHaveAttribute("data-status", outcome);
      await expect(page.locator(".user-message-text")).toHaveText(
        outcome === "completed" ? ["first", "second", "queued follow-up"] : ["first", "second"],
      );
      const summaryRequests = server.requests.filter((body) =>
        JSON.stringify(body.messages).includes("ONLY output the structured summary"),
      );
      expect(summaryRequests.length).toBeGreaterThan(0);
      if (outcome === "completed") {
        expect(summaryRequests).toHaveLength(2);
        const request = JSON.stringify(server.requests.at(-1)?.messages);
        expect(request).toContain("Earlier work summary");
        expect(request).not.toContain('"content":"first"');
        expect(request).not.toContain("/compact keep decisions");
      }
    } finally {
      release.resolve();
      await runCleanup([
        () => rm(dir, { recursive: true, force: true }),
        () => server.close(),
        () => app?.close(),
      ]);
    }
  });
}