ai-chatbot/tests/routes/zcode-agent-task.test.ts

97 lines
5.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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("不要换模型、新建对话或重新提交");
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);
});