Files
UHD_Media_Tool/apps/web/app/components/agent/agent-helpers.test.ts
T
rootandClaude Opus 5.5 417af9f253 fix(web): 智能体汇报展示层兜底与进度口径(FIX-7)
- 汇报正文里的 UUID、能力 ID、原因码展示时换成名称和人话,原文收进「正文原文」,历史不改
- 步骤进度统一 stepProgress:分子只数完成,跳过单独计(「4/6 · 跳过 2」)
- 进度行不再把失败的调用算进动作计数(3 次被拒的派发曾写成「派出了 5 个子会话」);
  实际结果行按会话树显示派出的子会话数
- 会话标题里的 ISO 时间与 download_failed 等码转人话

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 00:00:03 +08:00

171 lines
7.4 KiB
TypeScript
Raw 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 type { AgentRunEvent } from "@umt/contracts";
import { describe, expect, it } from "vitest";
import { authDenial, isDenied, pairAudit, runPlanIds, toolSummary } from "./agent-conversation.tsx";
import { matchesHistory } from "./agent-session-list.tsx";
import { contextFromPath, contextPrefix, shortGoalTitle, splitContextGoal } from "./agent-store.ts";
import { isLongReport } from "./conversation-model.ts";
import { foldEvents, type StreamItem, type ToolCallView } from "./use-agent-stream.ts";
const call = (p: Partial<ToolCallView>): ToolCallView => ({
toolCallId: "t1",
capabilityId: "task.retry",
inputSummary: null,
outputSummary: null,
status: "succeeded",
reason: null,
startedAt: "2026-09-27T00:00:00.000Z",
endedAt: null,
recovery: false,
...p,
});
describe("拒绝判定", () => {
it("授权校验失败(tool_call_end + unauthorized)按拒绝展示", () => {
const c = call({
status: "failed",
outputSummary: '{"error":{"code":"unauthorized","message":"缺少 task.retry 的持续授权"}}',
});
expect(authDenial(c)).toBe("缺少 task.retry 的持续授权");
expect(isDenied(c)).toBe(true);
});
it("截断的摘要也能认出授权错误;普通失败不算拒绝", () => {
expect(authDenial(call({ status: "failed", outputSummary: '{"error":{"code":"forbidden","message":"x"}…' }))).toBe(
"x",
);
expect(isDenied(call({ status: "failed", outputSummary: '{"error":{"code":"internal"}}' }))).toBe(false);
expect(isDenied(call({ status: "denied", reason: "不在白名单" }))).toBe(true);
});
});
describe("计划与审计", () => {
const plan = "32320000-0000-4000-8000-000700000002";
it("从提交输入与下载计划输出里收集计划 ID", () => {
const items: StreamItem[] = [
{ kind: "tool", key: "a", call: call({ capabilityId: "download.submit", inputSummary: `{"planId":"${plan}"}` }) },
{
kind: "tool",
key: "b",
call: call({
toolCallId: "t2",
capabilityId: "download.plan",
outputSummary: '{"plan":{"id":"33000000-0000-4000-8000-000000000701","status":"draft"…',
}),
},
];
expect(runPlanIds(items, null).sort()).toEqual([plan, "33000000-0000-4000-8000-000000000701"]);
});
it("同能力的审计按先后与工具调用配对", () => {
const calls = [
call({ toolCallId: "a", capabilityId: "download.submit", startedAt: "2026-09-27T00:00:01.000Z" }),
call({ toolCallId: "b", capabilityId: "download.submit", startedAt: "2026-09-27T00:00:02.000Z" }),
];
const audit = ["k1", "k2"].map((k, i) => ({
capabilityId: "download.submit",
at: `2026-09-27T00:00:0${i + 1}.500Z`,
outcome: "succeeded",
idempotencyKey: k,
durationMs: 1,
grantId: null,
approvalId: null,
}));
const m = pairAudit(calls, audit);
expect(m.get("a")?.idempotencyKey).toBe("k1");
expect(m.get("b")?.idempotencyKey).toBe("k2");
});
});
describe("侧栏上下文", () => {
it("按路由推断上下文并生成前缀,列表里能拆回正文", () => {
const ctx = contextFromPath("/console/engine/5b4e76e0-f27f-4e7a-8e08-71486387df1d");
expect(ctx?.taskId).toBe("5b4e76e0-f27f-4e7a-8e08-71486387df1d");
expect(contextFromPath("/console/sites/hdhome")?.siteId).toBe("hdhome");
expect(contextFromPath("/console/agent")).toBeNull();
const goal = `${contextPrefix(ctx)}排查这个任务`;
expect(splitContextGoal(goal)).toEqual({ context: expect.stringContaining("taskId="), text: "排查这个任务" });
});
});
describe("事件折叠", () => {
const ev = (seq: number, body: Record<string, unknown>) =>
({ runId: "r1", seq, at: "2026-09-28T00:00:00.000Z", ...body }) as unknown as AgentRunEvent;
it("挂起后恢复重放同一 toolCallId:只保留一张工具卡,状态以重放为准", () => {
const start = {
type: "tool_call_start",
toolCallId: "tc1",
capabilityId: "download.submit",
inputSummary: "{}",
};
const items = foldEvents([
ev(1, { ...start, recovery: false }),
ev(2, { ...start, recovery: true }),
ev(3, {
type: "tool_call_end",
toolCallId: "tc1",
capabilityId: "download.submit",
outputSummary: "ok",
isError: false,
recovery: true,
}),
]);
const tools = items.filter((i) => i.kind === "tool");
expect(tools).toHaveLength(1);
expect(new Set(items.map((i) => i.key)).size).toBe(items.length);
expect(tools[0]?.kind === "tool" && tools[0].call).toMatchObject({ status: "succeeded", recovery: true });
});
});
describe("工具结果摘要", () => {
it("展开后每步一行的「→ 结果」", () => {
expect(toolSummary({ status: "running", outputSummary: null })).toBe("等待结果…");
expect(toolSummary({ status: "succeeded", outputSummary: '{"items":[1,2,3]}' })).toBe("3 条");
expect(toolSummary({ status: "failed", outputSummary: '{"error":{"code":"x","message":"超时"}}' })).toBe("超时");
// 没有人话可写的对象不上屏(字段名进技术信息)
expect(toolSummary({ status: "succeeded", outputSummary: '{"found":true,"count":2}' })).toBeNull();
expect(toolSummary({ status: "succeeded", outputSummary: '{"plan":{"id":"x"…' })).toBeNull();
expect(toolSummary({ status: "succeeded", outputSummary: "下载器连接超时" })).toBe("下载器连接超时");
});
it("长正文判定", () => {
expect(isLongReport("短")).toBe(false);
expect(isLongReport("x".repeat(601))).toBe(true);
expect(isLongReport(Array(14).fill("行").join("\n"))).toBe(true);
});
});
describe("会话历史搜索", () => {
const run = { id: "abcd1234-0000", goal: "[上下文 作品 x] 把 沙丘 收进来" };
it("搜索匹配目标或 ID 前缀", () => {
expect(matchesHistory(run, { q: "沙丘" })).toBe(true);
expect(matchesHistory(run, { q: "abcd" })).toBe(true);
expect(matchesHistory(run, { q: "奥本海默" })).toBe(false);
});
});
describe("shortGoalTitle(会话短标题)", () => {
it("去 UUID、JSON 参数,能力 ID 换中文名,取第一句", () => {
const t = shortGoalTitle(
'在浏览器会话 16be7338-1111-4222-8333-944445555666 上直接调用一次 browser.solve_challenge,参数:{"sessionId":"x","mode":"auto"}',
);
expect(t).not.toMatch(/16be7338|\{|sessionId/);
expect(t.startsWith("在浏览器会话上直接调用一次")).toBe(true);
});
it("去上下文前缀,超长截断", () => {
expect(shortGoalTitle("[上下文 作品:沙丘] 把沙丘:第二部的 4K 收进来。然后再看看别的")).toBe(
"把沙丘:第二部的 4K 收进来",
);
expect(shortGoalTitle("啊".repeat(60))).toBe(`${"啊".repeat(40)}…`);
expect(shortGoalTitle(" ")).toBe("未命名会话");
});
it("ISO 时间与待处理类型码转人话(FIX-7)", () => {
const iso = "2026-09-28T18:53:50.468Z";
const d = new Date(iso);
const hm = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
expect(shortGoalTitle(`巡检待处理(${iso}):有 1 条未关闭的待处理事项。逐条查看`)).toBe(
`巡检待处理(${d.getMonth() + 1}月${d.getDate()}日 ${hm}):有 1 条未关闭的待处理事项`,
);
const t = shortGoalTitle(
"调查待处理事项 728960d1-b892-4ae7-8de3-baf61f8abbd7(download_failed):下载异常:Dune.Part.Two。摘要",
);
expect(t).toBe("调查待处理事项(下载失败):下载异常:Dune.Part.Two");
});
});