Files
UHD_Media_Tool/apps/web/app/components/agent/agent-session-ui.test.ts
T

595 lines
24 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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { AgentRunEvent } from "@umt/contracts";
import { createElement, type ReactNode } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { describe, expect, it } from "vitest";
import type { AgentRunView, GoalReport } from "~/api/agent.ts";
import { qk } from "~/lib/query-keys.ts";
import {
AgentConversation,
claimedCounts,
contextLabel,
DeniedLines,
ResultLine,
reportMismatch,
resultFacts,
StepSummary,
} from "./agent-conversation.tsx";
import { OptionsCard, optionPoint, orderOptions } from "./agent-interaction.tsx";
import { goalFromInput, RunStatsLine } from "./agent-run-panel.tsx";
import { arrangeHistory } from "./agent-session-list.tsx";
import {
type ActivityItem,
activitySpan,
activitySummary,
buildConversation,
deniedReason,
} from "./conversation-model.ts";
import { parseCommand, RUN_COMMANDS } from "./slash-commands.ts";
import {
type AgentStreamState,
foldEvents,
latestPlan,
type OptionsView,
type ToolCallView,
} from "./use-agent-stream.ts";
// AGUI-3 组件测试:进度行聚合、隐藏步骤清单 / 方案调用、拒绝一行、方案一键选定、历史置顶与子会话、指令。
// 渲染用 react-dom/server(没有 DOM 环境),交互逻辑放在纯函数里单测;绑定批准见服务端 e2e(e12-interaction)。
const AT = "2026-09-29T08:00:00.000Z";
const at = (s: number) => new Date(Date.parse(AT) + s * 1000).toISOString();
let seq = 0;
const ev = (s: number, body: Record<string, unknown>) =>
({ runId: "r1", seq: seq++, at: at(s), ...body }) as unknown as AgentRunEvent;
const tool = (s: number, id: string, cap: string, out: string, isError = false): AgentRunEvent[] => [
ev(s, { type: "tool_call_start", toolCallId: id, capabilityId: cap, inputSummary: "{}", recovery: false }),
ev(s + 1, {
type: "tool_call_end",
toolCallId: id,
capabilityId: cap,
outputSummary: out,
isError,
recovery: false,
}),
];
const call = (p: Partial<ToolCallView>): ToolCallView => ({
toolCallId: "c1",
capabilityId: "site.list",
inputSummary: null,
outputSummary: null,
status: "succeeded",
reason: null,
startedAt: AT,
endedAt: at(1),
recovery: false,
...p,
});
/** 旧会话形状的事件:步骤清单与方案都作为工具调用出现、授权拒绝、最终正文(9883534f 的样子) */
function legacyRun(): AgentRunEvent[] {
seq = 0;
return [
ev(0, { type: "run_started", goal: "检查站点、下载器和媒体服务器", resumed: false }),
...tool(1, "p1", "agent.plan", '{"steps":[],"revision":1}'),
ev(2, {
type: "plan_updated",
revision: 1,
steps: [
{ id: "s1", title: "看站点", status: "running", note: null },
{ id: "s2", title: "核对下载任务", status: "pending", note: null },
],
}),
ev(3, { type: "text_delta", delta: "先看站点列表。" }),
...tool(4, "t1", "site.list", '{"items":[{"name":"憨憨"}]}'),
...tool(6, "t2", "download.inspect", '{"summary":"没有进行中的下载"}'),
...tool(8, "t3", "download.inspect", '{"summary":"没有进行中的下载"}'),
...tool(10, "t4", "inventory.query", '{"items":[]}'),
...tool(
12,
"t5",
"site.health_check",
'{"error":{"code":"unauthorized","message":"缺少 site.health_check 的持续授权"}}',
true,
),
...tool(14, "p2", "agent.plan", '{"steps":[],"revision":2}'),
ev(16, { type: "thinking_delta", delta: "下载器没接入。" }),
ev(17, {
type: "text_delta",
delta: "下载器和媒体服务器都还没接入,所以没法检查。站点「憨憨」正常,在做种 24 个。",
}),
ev(41, { type: "run_completed", finalText: "", usage: { totalTokens: 1000, costUsd: 0.01 } }),
];
}
const liveOf = (events: AgentRunEvent[]): AgentStreamState => ({
events,
items: foldEvents(events),
plan: latestPlan(events),
usage: null,
finished: true,
error: null,
connected: "idle",
retry: () => {},
});
const RUN = {
id: "00000000-0000-4000-8000-000000000001",
createdAt: AT,
updatedAt: at(41),
taskId: null,
parentRunId: null,
trigger: "user",
goal: "[上下文 作品:沙丘:第二部 · entityId=74beab51-0000-4000-8000-000000000000] 检查站点、下载器和媒体服务器",
status: "succeeded",
provider: "opencode-go",
model: "deepseek-v4.1-flash",
thinkingLevel: "medium",
scope: { interactive: true },
budget: { maxToolCalls: 20 },
usage: {},
evidenceVersion: null,
sessionState: null,
summary: null,
toolCallCount: 5,
lastEventSeq: 0,
error: null,
cancelRequestedAt: null,
startedAt: AT,
finishedAt: at(41),
createdBy: "admin",
dedupeKey: null,
depth: 0,
spawnReason: null,
} as unknown as AgentRunView;
function render(node: ReactNode, admin = true): string {
const client = new QueryClient();
if (admin) client.setQueryData(qk.session, { actor: { type: "admin" }, expiresAt: "2099-01-01T00:00:00.000Z" });
return renderToStaticMarkup(createElement(QueryClientProvider, { client }, createElement(MemoryRouter, null, node)));
}
/** 去掉标签,只看文字 */
const text = (html: string) => html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ");
describe("隐藏:步骤清单与方案不作为工具调用出现", () => {
it("新旧事件里的 agent.plan 调用都被滤掉,步骤清单照样取到", () => {
const events = legacyRun();
const items = foldEvents(events);
const caps = items.flatMap((i) => (i.kind === "tool" ? [i.call.capabilityId] : []));
expect(caps).not.toContain("agent.plan");
expect(caps).toEqual(["site.list", "download.inspect", "download.inspect", "inventory.query", "site.health_check"]);
expect(latestPlan(events)?.steps).toHaveLength(2);
});
it("方案调用只留方案卡", () => {
seq = 0;
const items = foldEvents([
ev(0, {
type: "tool_call_start",
toolCallId: "o1",
capabilityId: "agent.propose_options",
inputSummary: "{}",
recovery: false,
}),
ev(1, {
type: "options_proposed",
invocationId: "o1",
decisionId: "00000000-0000-4000-8000-00000000d001",
question: "收哪个?",
options: [],
recommendedId: null,
stepId: null,
expiresAt: at(600),
timeoutAction: "recommended",
}),
]);
expect(items.map((i) => i.kind)).toEqual(["options"]);
});
});
describe("聚合:进度行用能力元数据的人话计数", () => {
it("同一动作合并对象,数字前后空格,被拒绝的不算", () => {
const calls = [
call({ capabilityId: "site.list" }),
call({ capabilityId: "download.inspect" }),
call({ capabilityId: "download.inspect" }),
call({ capabilityId: "inventory.query" }),
call({ capabilityId: "release.search" }),
call({ capabilityId: "release.search" }),
call({ capabilityId: "site.health_check", status: "denied", reason: "不在白名单" }),
call({ capabilityId: "agent.plan" }),
];
expect(activitySummary(calls)).toBe("查了站点、2 个下载任务和媒体库,搜了 2 次资源");
expect(activitySummary([call({ capabilityId: "download.inspect" }), call({ capabilityId: "site.list" })])).toBe(
"查了 1 个下载任务和站点",
);
// DL-3:不再说"生成了 1 份下载计划"
expect(activitySummary([call({ capabilityId: "download.plan" })])).toBe("准备下载 1 个资源");
});
it("一回合的全部调用聚成一行(1 次也折叠),展开前看不到能力 ID", () => {
const activity: ActivityItem[] = [{ kind: "tool", key: "c1", call: call({ capabilityId: "site.list" }) }];
const html = render(createElement(StepSummary, { activity, running: false }));
const line = text(html.slice(0, html.indexOf("</button>")));
expect(line).toContain("查了站点");
expect(line).toContain("1 秒");
expect(line).not.toContain("site.list");
});
it("旧会话整段:一行进度 + 正文 + 一行没做,不出现回合头、步骤清单调用和模型原文副本", () => {
const html = render(createElement(AgentConversation, { run: RUN, live: liveOf(legacyRun()) }));
const t = text(html);
expect(html.match(/data-slot="step-summary"/g)).toHaveLength(1);
expect(t).toContain("查了站点、2 个下载任务和媒体库");
expect(t).toContain("1 项没做");
expect(t).toContain("站点「憨憨」正常,在做种 24 个。");
expect(t).toContain("没做:检测站点,需要你批准");
expect(t).not.toContain("更新步骤清单");
expect(t).not.toContain("模型说的");
expect(t).not.toMatch(/智能体\s+\d{2}:\d{2}/);
// 过程中的话收进进度行,不在正文里
const body = t.slice(t.indexOf("1 项没做"));
expect(body.indexOf("先看站点列表")).toBeLessThan(body.indexOf("下载器和媒体服务器都还没接入"));
// 上下文标签只显示名称
expect(t).toContain("沙丘:第二部");
expect(t).not.toContain("entityId=");
});
});
describe("拒绝:缩成一行", () => {
const denied = call({
toolCallId: "t5",
capabilityId: "site.health_check",
status: "failed",
outputSummary: '{"error":{"code":"unauthorized","message":"缺少 site.health_check 的持续授权"}}',
});
it("授权拒绝 → 需要你批准 + [批准并重试];白名单拦截没有按钮", () => {
expect(deniedReason(denied)).toEqual({ text: "需要你批准", approvable: true });
expect(deniedReason(call({ status: "denied", reason: "不在白名单" }))).toEqual({
text: "这次会话没开放这个能力",
approvable: false,
});
const t = text(
render(createElement(DeniedLines, { runId: RUN.id, calls: [denied, { ...denied, toolCallId: "t6" }] })),
);
expect(t.match(/没做:/g)).toHaveLength(1);
expect(t).toContain("没做:检测站点,需要你批准");
expect(t).toContain("批准并重试");
// 不是管理员会话:只写一行,不给按钮
expect(text(render(createElement(DeniedLines, { runId: RUN.id, calls: [denied] }), false))).not.toContain(
"批准并重试",
);
});
it("原因里提到「单次审批」的普通授权拒绝仍算没做", () => {
seq = 0;
const items = foldEvents(
tool(
0,
"h1",
"site.health_check",
'{"error":{"code":"unauthorized","message":"缺少 site.health_check 的持续授权或有效的单次审批"}}',
true,
),
);
const [turn] = buildConversation(items);
expect(turn?.kind === "turn" && turn.denied.map((c) => c.capabilityId)).toEqual(["site.health_check"]);
});
it("已转成审批卡的拒绝不算没做", () => {
const items = foldEvents([
...tool(
0,
"s1",
"download.submit",
'{"error":{"code":"unauthorized","message":"已在步骤处发起审批,等待管理员批准"}}',
true,
),
ev(3, {
type: "approval_requested",
approvalId: "00000000-0000-4000-8000-00000000a001",
decisionId: null,
invocationId: "s1",
capabilityId: "download.submit",
planType: "acquisition_plan",
planId: "00000000-0000-4000-8000-00000000b001",
payloadHash: "h",
expectedRevision: 1,
riskLevel: "high",
summary: "下载《沙丘2》",
stepId: null,
expiresAt: at(3600),
}),
]);
const [turn] = buildConversation(items);
expect(turn?.kind === "turn" && turn.denied).toEqual([]);
});
});
describe("方案:一键选定", () => {
const view: OptionsView = {
decisionId: "00000000-0000-4000-8000-00000000d001",
question: "《沙丘2》1080p 怎么收?",
options: [
{ id: "b", title: "先修站点", summary: "站点登录过期,先刷新", risks: [], estimatedCost: null, action: null },
{
id: "a",
title: "提交下载",
summary: "用刚生成的计划",
risks: ["缓存卷会到 86%", "免费还剩 6 小时"],
estimatedCost: "12 GB",
action: {
capabilityId: "download.submit",
planId: "00000000-0000-4000-8000-00000000b001",
label: "提交下载",
detail: "《沙丘2》· 12.0 GB",
approves: true,
},
},
{ id: "c", title: "先不下", summary: "等 REMUX", risks: [], estimatedCost: null, action: null },
],
recommendedId: "a",
stepId: null,
expiresAt: at(1800),
timeoutAction: "recommended",
resolved: null,
};
it("推荐项第一位;动作要点来自服务端的计划摘要", () => {
expect(orderOptions(view.options, "a").map((o) => o.id)).toEqual(["a", "b", "c"]);
// 标题已经是动作名:只写对象和大小
expect(optionPoint(view.options[1]!)).toBe("《沙丘2》· 12.0 GB");
expect(optionPoint({ ...view.options[1]!, title: "收这个" })).toBe("提交下载 · 《沙丘2》· 12.0 GB");
expect(optionPoint(view.options[0]!)).toBeNull();
});
it("每个方案一个按钮(没有单选框和「选定」),推荐项是主按钮;风险折叠成「2 条风险」,都不要是链接", () => {
const html = render(createElement(OptionsCard, { runId: RUN.id, view, waiting: true }));
expect(html).not.toContain('type="radio"');
const buttons = [...html.matchAll(/<button[^>]*>([\s\S]*?)<\/button>/g)];
const labels = buttons.map((m) => text(m[1] ?? "").trim());
expect(labels[0]).toContain("提交下载");
expect(labels[0]).toContain("12.0 GB");
expect(labels[0]).toContain("(推荐)");
expect(labels[0]).toContain("选定即执行");
expect(buttons[0]?.[0]).toContain("pill-primary");
expect(labels.slice(1, 3).map((l) => l.split(" ")[0])).toEqual(["先修站点", "先不下"]);
expect(labels).toContain("2 条风险");
expect(labels).toContain("都不要…");
expect(labels.some((l) => l === "选定")).toBe(false);
expect(html).not.toContain("<textarea");
});
it("选完收成一行", () => {
const t = text(
render(
createElement(OptionsCard, {
runId: RUN.id,
view: { ...view, resolved: { outcome: "chosen", choice: "a", reason: null, by: "user" } },
waiting: true,
}),
),
);
expect(t).toContain("已选:提交下载 · 《沙丘2》· 12.0 GB");
expect(t).not.toContain("先修站点");
});
});
describe("历史:等你处理置顶,子会话收在父会话下", () => {
const r = (id: string, p: Partial<AgentRunView>) => ({ ...RUN, id, ...p }) as AgentRunView;
const runs = [
r("a", { goal: "巡检" }),
r("b", { goal: "子任务", parentRunId: "a", trigger: "subagent" }),
r("c", { goal: "收沙丘", status: "waiting", summary: "等待审批" }),
r("d", { goal: "失败的", status: "failed" }),
r("e", { goal: "孤儿子会话", parentRunId: "zzz", trigger: "subagent" }),
r("f", { goal: "等下载", status: "waiting", summary: "等待下载" }),
];
it("全部:等你处理置顶;子会话挂到父会话;父会话不在列表的单列", () => {
const { pinned, rows } = arrangeHistory(runs, { q: "", view: "all" });
expect(pinned.map((x) => x.run.id)).toEqual(["c"]);
expect(rows.map((x) => x.run.id)).toEqual(["a", "d", "e", "f"]);
expect(rows[0]?.children.map((x) => x.id)).toEqual(["b"]);
});
it("等你处理 / 失败 / 条数上限", () => {
expect(arrangeHistory(runs, { q: "", view: "pending" }).rows.map((x) => x.run.id)).toEqual(["c"]);
expect(arrangeHistory(runs, { q: "", view: "failed" }).rows.map((x) => x.run.id)).toEqual(["d"]);
const limited = arrangeHistory(runs, { q: "", view: "all" }, 3);
expect(limited.pinned.length + limited.rows.length).toBe(3);
});
});
describe("输入区:/ 指令与上下文标签", () => {
it("/先给方案 只作用于这一次", () => {
expect(goalFromInput("/先给方案 把沙丘收进来")).toEqual({ goal: "把沙丘收进来", opts: { proposeFirst: true } });
expect(goalFromInput("把沙丘收进来")).toEqual({ goal: "把沙丘收进来", opts: { proposeFirst: false } });
expect(goalFromInput("/先给方案")).toBeNull();
expect(parseCommand("/重跑", RUN_COMMANDS)).toEqual({ command: "/重跑", rest: "" });
expect(parseCommand("/先给方案x", RUN_COMMANDS).command).toBeNull();
});
it("上下文标签显示名称,只有 ID 时不显示", () => {
expect(contextLabel("作品:沙丘:第二部 · entityId=74beab51")).toBe("沙丘:第二部");
expect(contextLabel("entityId=74beab51")).toBeNull();
expect(contextLabel(null)).toBeNull();
});
});
describe("会话统计与本轮状态(AG-4)", () => {
const humanized = (): AgentRunEvent[] => {
seq = 0;
return [
ev(0, { type: "run_started", goal: "检查站点", resumed: false }),
...tool(1, "t1", "site.list", '{"items":[{"name":"憨憨"}]}'),
ev(3, {
type: "text_delta",
delta: "site.list 返回站点 4e71672f-0000-4000-8000-000000000000 正常,没有 invalid_input。",
}),
ev(9, { type: "run_completed", finalText: "", usage: { totalTokens: 1000, costUsd: 0.01 } }),
];
};
it("正文里不再列「换掉的标识」;原始汇报只在结果行的技术信息「原文」里", () => {
const live = liveOf(humanized());
const t = text(render(createElement(AgentConversation, { run: RUN, live })));
expect(t).not.toContain("换掉的标识");
expect(t).not.toContain("正文原文");
expect(t).not.toContain("→ 列出站点");
const result = render(createElement(ResultLine, { run: RUN, report: null, live }));
expect(text(result)).toContain("原文");
expect(result).toContain("4e71672f-0000-4000-8000-000000000000");
expect(result).not.toContain("换掉的标识");
});
it("结束后本轮调用次数并入进度行", () => {
const activity: ActivityItem[] = [
{ kind: "tool", key: "c1", call: call({ capabilityId: "site.list" }) },
{ kind: "tool", key: "c2", call: call({ toolCallId: "c2", capabilityId: "inventory.query" }) },
];
const html = render(createElement(StepSummary, { activity, running: false }));
expect(text(html.slice(0, html.indexOf("</button>")))).toContain("调用 2 次工具");
});
it("运行中:思考状态与本轮已调用次数放在一行", () => {
const events = humanized().slice(0, 3);
const live = { ...liveOf(events), finished: false };
const t = text(render(createElement(AgentConversation, { run: { ...RUN, status: "running" }, live })));
expect(t).toContain("思考中 · 已调用 1 次工具");
});
it("概况条上方的会话统计:工具调用、token(K)、剩余、费用", () => {
const live = liveOf(humanized());
const snapshot = {
run: { ...RUN, toolCallCount: 1, budget: { maxTokens: 200_000 }, usage: { totalTokens: 45_200, costUsd: 0.03 } },
report: null,
messages: [],
} as never;
const t = text(render(createElement(RunStatsLine, { snapshot, live })));
expect(t).toContain("工具调用 1 次 · token 45.2K · 剩 154.8K(77%) · 费用 $0.03");
});
});
describe("实际结果行(FIX-9):只列系统核实的事实", () => {
const ID = (n: number) => `00000000-0000-4000-8000-00000000010${n}`;
const report = (o: Partial<GoalReport>): GoalReport => ({
state: "no_actions",
summary: "本次没有提交下载或整理,结论见模型汇报。",
submitted: 0,
imported: 0,
inProgress: 0,
failed: 0,
plans: [],
downloads: [],
organization: [],
modelText: null,
generatedAt: AT,
...o,
});
const done = report({
state: "completed",
summary: "已提交下载 2 个",
submitted: 2,
imported: 1,
inProgress: 1,
downloads: [
{ downloadTaskId: ID(1), planId: null, status: "completed", name: "A" },
{ downloadTaskId: ID(2), planId: null, status: "downloading", name: "B" },
],
organization: [
{ recordId: ID(3), status: "succeeded", downloadTaskId: ID(1), mediaEntityId: null, targetPath: "/m/A" },
],
});
const withText = (t: string) =>
liveOf([
ev(0, { type: "run_started", goal: "g", resumed: false }),
ev(1, { type: "text_delta", delta: t }),
ev(2, { type: "run_completed", finalText: "", usage: { costUsd: 0.01 } }),
]);
it("没有动作:一行小字 + 时长 + 费用,不再有「说明」「汇总」「未经核查」", () => {
const html = render(
createElement(ResultLine, {
run: { ...RUN, usage: { costUsd: 0.01 } },
report: report({}),
live: withText("都正常。"),
}),
);
const t = text(html);
expect(t).toContain("没有提交下载或整理 · 41 秒 · 约 $0.01");
expect(t).not.toContain("说明");
expect(t).not.toContain("汇总");
expect(t).not.toContain("未经核查");
expect(t).not.toContain("实际结果:");
expect(t).not.toContain("结论见模型汇报");
});
it("统计行已显示费用时不重复", () => {
const t = text(
render(
createElement(ResultLine, {
run: { ...RUN, usage: { costUsd: 0.01 } },
report: report({}),
live: withText("x"),
showCost: false,
}),
),
);
expect(t).toContain("没有提交下载或整理 · 41 秒");
expect(t).not.toContain("$0.01");
});
it("有动作:列事实,单条记录链到记录本身,多条链到列表", () => {
const html = render(
createElement(ResultLine, { run: RUN, report: done, live: withText("提交了 2 个下载,整理了 1 个。") }),
);
expect(text(html).replace(/\s*、\s*/g, "、")).toContain("提交了 2 个下载、完成 1 次整理、1 个进行中 · 41 秒");
expect(html).toContain(`href="/console/organize/${ID(3)}"`);
expect(html).toContain('href="/console/downloads"');
expect(html).not.toContain("与汇报不一致");
});
it("数字与汇报不一致:行尾加标记,悬停和技术信息里说明", () => {
const html = render(createElement(ResultLine, { run: RUN, report: done, live: withText("已提交了 3 个下载。") }));
expect(html).toContain('data-slot="report-mismatch"');
expect(html).toContain("汇报说提交了 3 个下载,系统记录是 2 个");
});
it("汇报没写数字时不判不一致", () => {
expect(reportMismatch(done, "下载都安排好了。")).toEqual([]);
expect(claimedCounts("整理了 4 部电影")).toEqual({ organized: 4 });
expect(resultFacts(null)).toEqual([{ text: "汇总中…" }]);
});
});
describe("进度行时长(FIX-9):从这一轮开始算到结论正文", () => {
const quick: ActivityItem[] = [
{ kind: "tool", key: "c1", call: call({ capabilityId: "site.list", startedAt: at(6), endedAt: at(6) }) },
];
it("只有一个瞬时工具调用时不再是 0 秒", () => {
expect(activitySpan(quick)).toBe(0);
expect(activitySpan(quick, { since: at(0), until: at(7) })).toBe(7000);
const t = text(render(createElement(StepSummary, { activity: quick, running: false, since: at(0), until: at(7) })));
expect(t).toContain("· 7 秒");
});
it("没有工具调用、只有思考时也有时长;没有步骤为 null", () => {
expect(activitySpan([{ kind: "thinking", key: "k", text: "想", at: at(2) }], { since: at(0) })).toBe(2000);
expect(activitySpan([])).toBeNull();
});
it("整段会话:第一轮从会话开始算", () => {
const events = [
ev(0, { type: "run_started", goal: "g", resumed: false }),
...tool(5, "t1", "site.list", '{"items":[]}'),
ev(7, { type: "text_delta", delta: "都正常。" }),
ev(8, { type: "run_completed", finalText: "", usage: {} }),
];
const t = text(render(createElement(AgentConversation, { run: RUN, live: liveOf(events) })));
expect(t).toMatch(/· 7 秒/);
});
});
describe("步骤明细(FIX-9):能力 ID 换成能力标题", () => {
it("失败原因与工具之间的话里的能力 ID 走能力标题", () => {
const activity: ActivityItem[] = [
{ kind: "note", key: "n1", text: "site.health_check 被拒,改用 site.list。", at: AT },
{
kind: "tool",
key: "c1",
call: call({
capabilityId: "download.inspect",
status: "failed",
outputSummary: '{"error":{"code":"x","message":"site.health_check 调用失败"}}',
}),
},
];
const t = text(render(createElement(StepSummary, { activity, running: false })));
expect(t).not.toContain("site.health_check");
expect(t).not.toContain("site.list");
expect(t).toContain("检测站点 被拒");
});
});