Files
rootandClaude Opus 5.5 1ade49fe8e chore(release): v0.3.41
智能体抽屉读 ?open= 上下文、回复链接用抽屉地址;抽屉打开不亮焦点框;语句统计重复 key。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-08 14:39:24 +08:00

190 lines
8.6 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 type { AgentRunEvent } from "@umt/contracts";
import { describe, expect, it } from "vitest";
import { authDenial, draftCheckOf, 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();
// 列表页的记录抽屉(?open=)与日志页的任务抽屉(?run=)
expect(contextFromPath("/console/sites", "?open=hdhome&tab=rss")?.siteId).toBe("hdhome");
expect(contextFromPath("/console/organize", "?open=r1")?.recordId).toBe("r1");
expect(contextFromPath("/console/logs", "?type=task&run=t9")?.taskId).toBe("t9");
expect(contextFromPath("/console/sites", "?tab=rss")).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");
});
});
describe("起草巡检卡片", () => {
it("成功的 schedule.draft_check 取巡检 ID 与名称;失败 / 其它能力不出卡片", () => {
const out =
'{"jobId":"check.custom.1a2b3c4d","name":"失败下载日报","everyHours":24,"capabilities":["download.list"]';
expect(
draftCheckOf(call({ capabilityId: "schedule.draft_check", status: "succeeded", outputSummary: out })),
).toEqual([{ jobId: "check.custom.1a2b3c4d", name: "失败下载日报" }]);
expect(draftCheckOf(call({ capabilityId: "schedule.draft_check", status: "failed", outputSummary: out }))).toEqual(
[],
);
expect(draftCheckOf(call({ capabilityId: "site.list", status: "succeeded", outputSummary: out }))).toEqual([]);
});
});