import { createCanvasSaveQueue } from "@/lib/client/canvas-save-queue"; import { canvasHistoryForChat } from "@/lib/client/canvas-history-response"; import { expect, test } from "@playwright/test"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { canvasAgentProgress, canvasAgentProgressLabel, canvasAgentActivities } from "@/lib/ai/canvas-agent-runtime/progress"; import { newAgentRunSnapshot, publicAgentError, type AgentRunView } from "@/lib/ai/canvas-agent-runtime/protocol"; import { serverLoader } from "../helpers/canvas-download-server-loader"; const { CanvasAgentProgressMessage } = serverLoader({})("components/canvas/chat-panel/canvas-agent-progress.tsx"); function fixture(): AgentRunView { return { id: "r", assistantMessageId: "assistant-1", chatId: "c", projectId: "p", status: "running", message: "ZCode 正在规划并执行画布任务", steps: 3, toolCount: 1, resultNodeIds: [], inputs: [], updatedAt: new Date().toISOString(), cancelRequested: false, resumable: false, activities: [{ id: "step-1", title: "生成图片", status: "started" }] }; } test("history accepts existing exact-key payloads and rejects another tagged chat", () => { const messages = [{ id: "assistant-1", parts: [{ type: "text", text: "completed" }] }]; expect(canvasHistoryForChat({ messages }, "chat-1")?.messages).toBe(messages); expect(canvasHistoryForChat({ chatId: "chat-1", messages }, "chat-1")?.messages).toBe(messages); expect(canvasHistoryForChat({ chatId: "chat-2", messages }, "chat-1")).toBeUndefined(); expect(canvasHistoryForChat({ messages }, "")).toBeUndefined(); }); test("public status converts legacy engine and isolation messages without exposing diagnostics", () => { for (const text of ["ZCode 正在规划并执行画布任务", "正在启动 ZCode 独立沙箱", "sandbox bwrap failed at /var/lib/agent"]) expect(canvasAgentProgressLabel(text)).not.toMatch(/zcode|沙箱|sandbox|bwrap|\/var\//i); expect(canvasAgentProgressLabel("正在生成图片")).toBe("正在生成图片"); expect(publicAgentError(new Error("ZCode sandbox failed: /opt/agent/bin/node"))).not.toMatch(/zcode|sandbox|\/opt\//i); }); test("public activity exposes real step titles but never tool arguments, output, engine or host paths", () => { const snapshot = newAgentRunSnapshot("检查图片", []); snapshot.calls.push({ id: "read", name: "readCanvas", input: { secret: "PRIVATE" }, output: { debug: "/var/lib/agent" }, status: "succeeded" }); expect(canvasAgentActivities(snapshot)).toEqual([{ id: "read", title: "查看画布", status: "succeeded" }]); const view = fixture(); const progress = canvasAgentProgress({ ...view, runtime: { engine: "zcode", sandbox: "linux-bwrap" } } as any); expect(Object.keys(progress).sort()).toEqual(["activities", "id", "message", "status", "toolCount"]); expect(JSON.stringify(progress)).not.toMatch(/zcode|sandbox|PRIVATE|\/var\//i); }); test("progress renders as inline assistant content with one stop action and no second input or mode badge", () => { const run = fixture();const html = renderToStaticMarkup(createElement(CanvasAgentProgressMessage, { progress: canvasAgentProgress(run), run, onCancel() {}, })); expect(html).toContain('data-agent-message-id="assistant-1"'); expect(html).toContain("生成图片"); expect(html).toContain("停止后续步骤"); expect(html).not.toMatch(/ZCode|沙箱|sandbox|Auto Agent| { const run = { ...fixture(), status: "completed" as const }; const part = JSON.parse(JSON.stringify({ type: "data-canvasAgentProgress", data: canvasAgentProgress(run) })); const html = renderToStaticMarkup(createElement(CanvasAgentProgressMessage, { progress: part.data })); expect(html).toContain("处理完成");expect(html).toContain("生成图片"); expect(html).not.toContain("停止后续步骤");expect(html).not.toContain("]+open/); }); test("pre-Agent flush and autosave read successive revisions instead of racing stale snapshots", async () => { const queue = createCanvasSaveQueue();let release!: () => void;let revision = 1;let title = "before"; const gate = new Promise(resolve => { release = resolve; });const sent: Array<{revision:number;title:string}> = []; const first = queue.enqueue(async () => { sent.push({revision,title});await gate;revision=2;return true; }); const second = queue.enqueue(async () => { sent.push({revision,title});revision=3;return true; }); await Promise.resolve();expect(sent).toEqual([{revision:1,title:"before"}]);title="latest local edit";release();await Promise.all([first,second]); expect(sent).toEqual([{revision:1,title:"before"},{revision:2,title:"latest local edit"}]); }); test("a failed local save does not deadlock later saves or replay a failed snapshot", async () => { const queue = createCanvasSaveQueue();let calls=0; await expect(queue.enqueue(async()=>{calls++;throw new Error("revision conflict");})).rejects.toThrow("revision conflict"); await expect(queue.enqueue(async()=>{calls++;return true;})).resolves.toBe(true);expect(calls).toBe(2); });