Files
ttstd-infinite-canvas/web/test/agent-error-events.test.ts

98 lines
4.9 KiB
TypeScript

import { expect, test } from "bun:test";
import type { CloudAgentChatMessage } from "@/components/canvas/canvas-cloud-agent-chat-ui";
import { applyAgentEvent } from "@/components/canvas/canvas-cloud-agent-events";
import { buildAgentFeedSegments } from "@/lib/canvas/agent-operation-feed";
import type { AgentEvent } from "@/services/api/agent";
const failureMessage = "Agent 执行中断,请重新发送消息;如反复出现,请把诊断号 ag-test-run 反馈给管理员";
const event = (type: string, payload: AgentEvent["payload"] = {}, runId = "ag-test-run"): AgentEvent => ({
eventId: `${runId}:${type}`,
runId,
seq: 1,
type,
payload,
createdAt: "2026-10-03T00:00:00Z",
});
const failed = event("run_failed", { error: "internal runner error" });
const status = event("run_status", { status: "failed", failureMessage });
function reduceEvents(events: AgentEvent[], initial: CloudAgentChatMessage[] = []) {
let messages = initial;
for (const item of events) {
applyAgentEvent(
item,
(update) => {
messages = typeof update === "function" ? update(messages) : update;
},
() => {},
() => {},
);
}
return messages;
}
test("failure event and terminal snapshot update a single error with the public reason", () => {
const messages = reduceEvents([failed, status]);
expect(messages).toHaveLength(1);
expect(messages[0]).toMatchObject({ id: "terminal-ag-test-run", role: "error", text: failureMessage, errorSeverity: "error" });
expect(JSON.stringify(messages)).not.toContain("internal runner error");
});
test("reversed delivery, interleaved messages and replay retain the detailed failure", () => {
const assistant = event("assistant_message", { messageId: "answer", text: "已有执行结果" });
const messages = reduceEvents([status, assistant, failed, status, failed]);
expect(messages.map((item) => item.role)).toEqual(["error", "assistant"]);
expect(messages[0].text).toBe(failureMessage);
});
test("separate failed runs are not deduplicated by identical text", () => {
const messages = reduceEvents([event("run_failed", { text: "配额不足" }, "run-1"), event("run_failed", { text: "配额不足" }, "run-2")]);
expect(messages).toHaveLength(2);
expect(messages[0].id).not.toBe(messages[1].id);
});
test("a failed snapshot without a reason still appears but never replaces a specific reason", () => {
const emptyStatus = event("run_status", { status: "failed" });
expect(reduceEvents([emptyStatus])).toHaveLength(1);
const specific = event("run_failed", { text: "模型额度不足" });
expect(reduceEvents([emptyStatus, specific, emptyStatus])[0].text).toBe("模型额度不足");
});
test("replaying a cached failure replaces its event row instead of preserving duplicates", () => {
const initial: CloudAgentChatMessage[] = [
{ id: failed.eventId, role: "error", title: "Agent 执行失败", text: "Agent 执行失败" },
{ id: "terminal-ag-test-run", role: "error", title: "Agent 执行失败", text: failureMessage },
{ id: "sync-error", role: "error", title: "画布同步冲突", text: "请重新打开画布" },
];
const messages = reduceEvents([failed, status], initial);
expect(messages).toHaveLength(2);
expect(messages[0].text).toBe(failureMessage);
expect(messages[1]).toBe(initial[2]);
expect(initial).toHaveLength(3);
});
test("error events share the run failure identity without hiding tool failures", () => {
const tool = event("tool_failed", { toolName: "canvas_get_state", text: "读取失败" });
const messages = reduceEvents([event("error", { message: "暂时无法执行" }), tool, failed, status]);
expect(messages.filter((item) => item.role === "error")).toHaveLength(1);
expect(messages.filter((item) => item.role === "tool")).toHaveLength(1);
});
test("completed, cancelled and rejected runs do not create a failure card", () => {
expect(reduceEvents(["completed", "cancelled", "rejected"].map((state) => event("run_status", { status: state })))).toEqual([]);
});
test("cached conversations fold only the event rows with a matching terminal run", () => {
const initial: CloudAgentChatMessage[] = [
{ id: "ag-first:9", role: "error", text: "Agent 执行失败" },
{ id: "tool", role: "tool", text: "已有执行记录" },
{ id: "terminal-ag-first", role: "error", text: "模型调用失败" },
{ id: "ag-second:2", role: "error", text: "模型调用失败" },
{ id: "terminal-ag-third", role: "error", text: "模型调用失败" },
{ id: "stream-error-ag-first", role: "error", text: "事件流已断开" },
];
const segments = buildAgentFeedSegments(initial);
expect(segments.map((item) => item.key)).toEqual(["tool", "terminal-ag-first", "ag-second:2", "terminal-ag-third", "stream-error-ag-first"]);
expect(initial).toHaveLength(6);
});