源码
tests/desktop/conversation.spec.ts
import { mkdir, mkdtemp, readFile, 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";
import { seedHistory } from "../history-fixture.ts";
test("interrupted history keeps its status and content without an extra notice", async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-interrupted-notice-"));
const cwd = join(dir, "workspace");
await mkdir(cwd);
seedHistory(dir, cwd, 1, { interrupted: true });
let app: ElectronApplication | undefined;
try {
app = await launchDesktop({ dir, url: "" });
const page = await app.firstWindow();
const run = page.getByTestId("run");
await expect(run).toHaveAttribute("data-status", "interrupted");
await expect(run.getByTestId("progress")).toContainText("已中断");
await expect(run.locator(".answer")).toContainText("最终回复 0");
await expect(run.getByTestId("thinking-block")).toHaveCount(1);
await expect(run.locator(".run-notice")).toHaveCount(0);
await page.reload();
await expect(run).toHaveAttribute("data-status", "interrupted");
await expect(run.locator(".answer")).toContainText("最终回复 0");
await expect(run.locator(".run-notice")).toHaveCount(0);
} finally {
await runCleanup([() => rm(dir, { recursive: true, force: true }), () => app?.close()]);
}
});
test("streaming tools, manual expansion, default collapse, IME, safe renderer and restart", async () => {
const dir = await mkdtemp(join(tmpdir(), "zpi-e2e-"));
const project = join(dir, "project");
await mkdir(project);
await writeFile(join(project, "README.md"), "# Test project");
const release = deferred();
const server = await fakeServer(async (body, response) => {
const messages = body.messages as unknown as { role: string; content: string }[];
const results = messages.filter((m) => m.role === "tool");
const user = messages.find((m) => m.role === "user")?.content;
const calls = [
{ name: "read", arguments: { path: "README.md" } },
{ name: "write", arguments: { path: "demo.txt", content: "hello\n" } },
{ name: "edit", arguments: { path: "demo.txt", edits: [{ oldText: "hello", newText: "ZPI" }] } },
{ name: "bash", arguments: { command: "cat demo.txt" } },
];
if (user === "完整工具演示" && results.length < 4) {
const t = calls[results.length];
send(response, chunk({ reasoning_content: `正在执行 ${t.name}\n` }));
send(
response,
chunk({
tool_calls: [
{
index: 0,
id: `t${results.length}`,
type: "function",
function: { name: t.name, arguments: JSON.stringify(t.arguments) },
},
],
}),
);
done(response, "tool_calls");
} else {
send(response, chunk({ content: "首段已到达" }));
if (user === "完整工具演示") await release.promise;
send(response, chunk({ content: "\n\n**最终回复**:验证成功。" }));
done(response);
}
});
let app: ElectronApplication | undefined;
try {
app = await launchDesktop({ dir, project, url: server.url });
let page = await app.firstWindow();
const errors: string[] = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
await expect(page.getByRole("button", { name: "选择项目", exact: true })).toContainText("project");
await page.getByLabel("消息", { exact: true }).fill("完整工具演示");
await page.getByLabel("发送", { exact: true }).click();
await expect(page.locator(".answer")).toContainText("首段已到达");
await expect(page.getByTestId("run")).toHaveAttribute("data-status", "running");
if ((await page.getByTestId("progress").getAttribute("aria-expanded")) !== "true")
await page.getByTestId("progress").click();
await expect(page.getByTestId("process")).toContainText("README.md");
await expect(page.getByTestId("process")).toContainText("cat demo.txt");
await expect(page.locator('[data-tool-name="read"] .lucide-search')).toHaveCount(1);
await expect(page.locator('[data-tool-name="write"] .lucide-pencil')).toHaveCount(1);
await expect(page.locator('[data-tool-name="edit"] .lucide-pencil')).toHaveCount(1);
await expect(page.locator('[data-tool-name="bash"] .lucide-square-terminal')).toHaveCount(1);
await expect(page.locator(".process .status-dot")).toHaveCount(0);
await page.screenshot({ path: "test-results/desktop-streaming.png" });
release.resolve();
await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
await expect(page.getByTestId("progress")).toHaveAttribute("aria-expanded", "false");
await page.getByTestId("progress").click();
await expect(page.getByTestId("progress")).toHaveAttribute("aria-expanded", "true");
await expect(page.locator(".answer")).toContainText("最终回复");
expect(await readFile(join(project, "demo.txt"), "utf8")).toBe("ZPI\n");
await page.getByLabel("消息", { exact: true }).fill("第二轮");
await page.getByLabel("消息", { exact: true }).press("Enter");
await expect(page.getByTestId("run")).toHaveCount(2);
await expect(page.getByTestId("run").last()).toHaveAttribute("data-status", "completed");
await expect(page.getByTestId("progress").last()).toHaveAttribute("aria-expanded", "false");
const before = server.requests.length;
await page.getByLabel("消息", { exact: true }).fill("中文输入法");
await page.getByLabel("消息", { exact: true }).dispatchEvent("compositionstart");
await page
.getByLabel("消息", { exact: true })
.dispatchEvent("keydown", { key: "Enter", keyCode: 229, isComposing: true });
await expect(page.getByTestId("run")).toHaveCount(2);
expect(server.requests).toHaveLength(before);
await page.getByLabel("消息", { exact: true }).dispatchEvent("compositionend");
await page.getByLabel("消息", { exact: true }).press("Enter");
await expect(page.getByTestId("run")).toHaveCount(3);
expect(
await page.evaluate(() => ({
node: typeof (window as unknown as { require?: unknown }).require,
key: window.zpi.getSettings().then((r) => JSON.stringify(r)),
})),
).toMatchObject({ node: "undefined" });
const settings = await page.evaluate(() => window.zpi.getSettings());
expect(JSON.stringify(settings)).not.toContain("local-test-key");
expect(errors).toEqual([]);
await app.close();
app = await launchDesktop({ dir, project, url: server.url });
page = await app.firstWindow();
await expect(page.getByTestId("run")).toHaveCount(3);
await expect(page.getByTestId("progress").first()).toHaveAttribute("aria-expanded", "false");
await expect(page.locator(".answer").first()).toContainText("最终回复");
await page.screenshot({ path: "test-results/desktop-history.png" });
} finally {
release.resolve();
await runCleanup([
() => rm(dir, { recursive: true, force: true }),
() => server.close(),
() => app?.close(),
]);
}
});