96 lines
5.4 KiB
TypeScript
96 lines
5.4 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
||
import { tool } from "ai";
|
||
import { z } from "zod";
|
||
import type { AgentRunSnapshot } from "@/lib/ai/canvas-agent-runtime/protocol";
|
||
import type { runZcodeCanvasTask } from "@/lib/ai/zcode-agent/task";
|
||
import { serverLoader } from "../helpers/canvas-download-server-loader";
|
||
|
||
function fixture() {
|
||
const load = serverLoader({ "./native-runtime": {} });
|
||
const run: typeof runZcodeCanvasTask = load("lib/ai/zcode-agent/task.ts").runZcodeCanvasTask;
|
||
const snapshot: AgentRunSnapshot = load("lib/ai/canvas-agent-runtime/protocol.ts").newAgentRunSnapshot("替换图片中的底座", []);
|
||
const ports: Parameters<typeof runZcodeCanvasTask>[0] = {
|
||
runId: "original-request", snapshot, instructions: "", context: {},
|
||
gateway: { modelId: "test", apiType: "openai-chat-completions", forward: async () => Response.json({}) },
|
||
signal: new AbortController().signal,
|
||
definitions: {
|
||
planCanvasTask: tool({ inputSchema: z.object({}) }),
|
||
generateCanvasImages: tool({ inputSchema: z.object({ referenceNodeIds: z.array(z.string()) }) }),
|
||
},
|
||
tools: {
|
||
planCanvasTask: { title: "安排步骤", readOnly: true, concurrentSafe: true, execute: async () => ({ steps: ["替换底座"] }) },
|
||
generateCanvasImages: { title: "生成图片", readOnly: false, concurrentSafe: false, execute: async () => ({ createdNodeIds: ["repaired-result"] }) },
|
||
},
|
||
checkpoint: async () => undefined,
|
||
consumeInputs: async () => ({ cancelled: false }), isCancelled: async () => false,
|
||
verify: async () => ({ passed: true, issues: [] }),
|
||
};
|
||
return { ports, run, snapshot };
|
||
}
|
||
|
||
const result = { type: "result", sessionId: "test-session", response: "已完成" };
|
||
|
||
test("an unconfirmed paid tool stops the planner and immediately preserves the reason and original request ID", async () => {
|
||
const { ports, run, snapshot } = fixture();
|
||
const emitted: string[] = [], saved: string[] = [];
|
||
let submissions = 0;
|
||
ports.tools.generateCanvasImages.execute = async () => {
|
||
submissions++;
|
||
throw Object.assign(new Error("This operation was aborted"), { name: "AbortError" });
|
||
};
|
||
ports.onAssistantText = text => emitted.push(text);
|
||
ports.checkpoint = async state => { saved.push(state.message); };
|
||
ports.native = async options => {
|
||
await options.execute("planCanvasTask", {}, "plan", options.signal!);
|
||
await expect(options.execute("generateCanvasImages", { referenceNodeIds: ["original"] }, "unknown-result", options.signal!)).rejects.toThrow();
|
||
expect(options.signal!.aborted).toBe(true);
|
||
expect(saved.at(-1)).toContain("请求编号:original-request");
|
||
await options.onEvent?.({ type: "model.streaming", sessionId: "test-session", payload: { kind: "text_delta", assistantMessageId: "after-error", delta: "新建对话绕过原任务后再试" } });
|
||
await expect(options.execute("generateCanvasImages", { referenceNodeIds: ["original"] }, "duplicate-paid", options.signal!)).rejects.toThrow();
|
||
throw new Error("planner stopped");
|
||
};
|
||
expect(await run(ports)).toBe("needs_review");
|
||
expect(submissions).toBe(1);
|
||
expect(emitted).toEqual([]);
|
||
expect(snapshot.message).toContain("结果尚未确认");
|
||
expect(snapshot.message).toContain("不要换模型、新建对话或重新提交");
|
||
expect(snapshot.message).toContain(ports.runId);
|
||
ports.native = async () => { throw new Error("must not start a new attempt"); };
|
||
expect(await run(ports)).toBe("needs_review");
|
||
expect(submissions).toBe(1);
|
||
});
|
||
|
||
test("repairing a failed result permits original images as references without admitting another passed target", async () => {
|
||
const { ports, run, snapshot } = fixture();
|
||
let submissions = 0;
|
||
snapshot.verification = { passed: false, issues: ["底座不符"], retryable: true, failedNodeIds: ["failed-result"] };
|
||
ports.tools.generateCanvasImages.execute = async input => {
|
||
submissions++;
|
||
expect(input.referenceNodeIds).toEqual(["failed-result", "original-scene", "original-product"]);
|
||
return { createdNodeIds: ["repaired-result"] };
|
||
};
|
||
ports.native = async options => {
|
||
await options.execute("planCanvasTask", {}, "plan", options.signal!);
|
||
await expect(options.execute("generateCanvasImages", { referenceNodeIds: ["original-scene", "passed-result"] }, "wrong-target", options.signal!)).rejects.toThrow("未通过");
|
||
await options.execute("generateCanvasImages", { referenceNodeIds: ["failed-result", "original-scene", "original-product"] }, "repair-failed", options.signal!);
|
||
return result;
|
||
};
|
||
expect(await run(ports)).toBe("completed");
|
||
expect(submissions).toBe(1);
|
||
});
|
||
|
||
test("an unresolved image read reports the concrete input failure and recommendation", async () => {
|
||
const { ports, run, snapshot } = fixture();
|
||
ports.definitions.readCanvas = tool({ inputSchema: z.object({}) });
|
||
ports.tools.readCanvas = { title: "读取图片", readOnly: true, concurrentSafe: true, execute: async () => { throw new Error("无法获取图片,请检查图片链接是否有效:fetch failed"); } };
|
||
ports.native = async options => {
|
||
await expect(options.execute("readCanvas", {}, "failed-read", options.signal!)).rejects.toThrow();
|
||
expect(options.signal!.aborted).toBe(false);
|
||
return result;
|
||
};
|
||
expect(await run(ports)).toBe("needs_review");
|
||
expect(snapshot.message).toContain("图片无法读取");
|
||
expect(snapshot.message).toContain("重新上传原图");
|
||
expect(snapshot.message).toContain(ports.runId);
|
||
});
|