import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import type { ComponentProps } from "react";
import { describe, expect, it, vi } from "vitest";
import { type AgentExecutionPlan } from "@/features/agent";
import type { OperationalTask } from "../api/operational-task-client";
import { AgentExecutionPlanPanel, ExecutionPlanSteps } from "./agent-execution-plan-panel";
describe("execution step cards", () => {
it("shows exact historical dependencies without assigning an unrecorded branch type", () => {
const plan: AgentExecutionPlan = {
schema_version: "execution-plan@1", session_id: "s1", revision: 1,
updated_at: "2026-09-23T01:00:00.000Z", title: "调查", reason: "核验",
nodes: ["接入", "上游", "邻区", "汇合"].map((title, index) => ({
id: String(index), title, status: index === 3 ? "waiting" : "completed",
owner: index === 3 ? "human" : "agent", detail: '', evidence: []
})),
edges: [{ from: "0", to: "1", label: "" }, { from: "0", to: "2", label: "" }, { from: "1", to: "3", label: "上游正常" }, { from: "2", to: "3", label: "投诉集中" }]
};
const { container } = render();
expect(screen.getByText(/历史流程按原始依赖展示/)).toBeInTheDocument();
expect(screen.queryByText(/并行处理/)).not.toBeInTheDocument();
expect(container.querySelectorAll("[data-execution-node]")).toHaveLength(4);
expect(container.querySelector("script")).toBeNull();
expect(screen.getByText("前置:上游(上游正常)、邻区(投诉集中)")).toBeInTheDocument();
expect(screen.getByText("待人工/资料")).toBeInTheDocument();
});
it("keeps all historical nodes and their recorded dependencies", () => {
const node = (id: string, title: string, status: AgentExecutionPlan["nodes"][number]["status"] = "completed") => ({
id, title, status, owner: "agent" as const, detail: `${title}详情`, evidence: []
});
const plan: AgentExecutionPlan = {
schema_version: "execution-plan@1", session_id: "s-nested", revision: 1,
updated_at: "2026-09-23T01:00:00.000Z", title: "多轮调查", reason: "按真实依赖推进",
nodes: [
node("start", "接收事件"),
node("a1", "上游核查"), node("a2", "上游复核"),
node("b1", "邻区投诉"),
node("c1", "压力比对"), node("c2", "水力校核"), node("c3", "阀门排查"),
node("d1", "运维记录"),
node("join1", "阶段判断"),
node("e1", "二次供水核查", "running"), node("e2", "水箱记录", "waiting"),
node("f1", "现场采样", "waiting"),
node("join2", "形成结论", "pending")
],
edges: [
{ from: "start", to: "a1", label: "" }, { from: "a1", to: "a2", label: "" }, { from: "a2", to: "join1", label: "" },
{ from: "start", to: "b1", label: "" }, { from: "b1", to: "join1", label: "" },
{ from: "start", to: "c1", label: "" }, { from: "c1", to: "c2", label: "" }, { from: "c2", to: "c3", label: "" }, { from: "c3", to: "join1", label: "" },
{ from: "start", to: "d1", label: "" }, { from: "d1", to: "join1", label: "" },
{ from: "join1", to: "e1", label: "" }, { from: "e1", to: "e2", label: "" }, { from: "e2", to: "join2", label: "" },
{ from: "join1", to: "f1", label: "" }, { from: "f1", to: "join2", label: "" }
]
};
const { container } = render();
expect(screen.getByText("前置:上游复核、邻区投诉、阀门排查、运维记录")).toBeVisible();
expect(screen.getByText("前置:水箱记录、现场采样")).toBeVisible();
expect(screen.queryByText(/并行处理/)).not.toBeInTheDocument();
expect(container.querySelectorAll("[data-execution-node]")).toHaveLength(plan.nodes.length);
});
});
const taskBase = {
project_id: "11111111-1111-4111-8111-111111111111",
task_id: "22222222-2222-4222-8222-222222222222",
task_type: "event_investigation",
workflow: "water_quality_complaint",
lifecycle_status: "waiting_human",
active_run: null,
status: "waiting_human",
priority: "normal",
revision: 2,
execution_generation: 1,
execution_request_id: "33333333-3333-4333-8333-333333333333",
title: "水质投诉调查",
summary: "居民反映异味",
source: { kind: "operator", external_id: null, external_revision: null, event_kind: "water_quality_complaint" },
trigger: { kind: "manual", occurred_at: "2026-09-23T01:00:00Z", detail: "演示小区" },
scheduled_for: null,
agent_session_id: "execution-session",
agent_lease_expires_at: null,
current_plan_revision: null,
current_report_revision: 1,
created_at: "2026-09-23T01:00:00Z",
updated_at: "2026-09-23T01:10:00Z",
latest_plan: null,
latest_report: {
revision: 1, execution_generation: 1, source_session_id: "execution-session", published_at: "2026-09-23T01:09:00Z", published_by: "agent",
report: { title: "调查报告", current_conclusion: "上游指标正常,待核查二次供水。", conclusion_boundary: "等待现场采样。", evidence_refs: ["e-1"], recommendations: ["安排现场采样"], pending_items: ["确认水箱清洗记录"] }
},
evidence: [{ evidence_id: "e-1", kind: "system_data", summary: "上游余氯处于正常范围", source_label: "SCADA 水质监测", source_ref: "WQ-01", observed_at: "2026-09-23T00:55:00Z", recorded_at: "2026-09-23T01:08:00Z", recorded_by: "agent", has_snapshot: false }],
actions: [{ action_id: "44444444-4444-4444-8444-444444444444", task_id: "22222222-2222-4222-8222-222222222222", version: 3, kind: "create_work_order", node_id: null, reason: "需要现场采样", payload: { title: "现场采样", location: "演示小区", scheduled_at: "2026-09-23T02:00:00Z" }, evidence_refs: ["e-1"], required_approval_permission: "operational_action.work_order.approve", required_execution_permission: "operational_action.work_order.execute", approval_status: "pending", execution_status: "not_submitted", execution_request_id: null, proposed_by: "agent", proposed_at: "2026-09-23T01:09:00Z", decided_by: null, decided_at: null, decision_comment: null, executed_by: null, executed_at: null, receipt: null, execution_detail: null }],
permissions: { can_publish_artifacts: false, can_propose_actions: false, can_sync_external_feedback: false, can_decide_work_orders: false, can_execute_work_orders: false, can_decide_commands: false, can_execute_commands: false, can_decide_access: false },
access_requests: [],
work_order_snapshot: null
} as OperationalTask;
it("leads with the insufficient-data report and keeps previous rounds historical", () => {
const task: OperationalTask = { ...taskBase, status: "completed", lifecycle_status: "completed", actions: [],
latest_report: { ...taskBase.latest_report!, report: { ...taskBase.latest_report!.report,
stage: "final", outcome: "insufficient_data", stop_reason: "缺少末梢采样", recommendations: [], pending_items: [],
data_gaps: [{ gap_id: "gap-1", node_id: "sampling", missing_data: "末梢采样", scope: "投诉时段",
impact: "无法判断异常来源", required_data: "末梢采样结果", evidence_refs: ["e-1"], blocking: true }] } }
};
const { rerender, ...props } = taskPanel(task);
expect(screen.getByText("本轮已结束 · 无法形成结论")).toBeVisible();
expect(screen.getByLabelText("数据缺口")).toHaveTextContent("无法判断异常来源");
expect(screen.queryByText("待确认事项")).not.toBeInTheDocument();
expect(screen.queryByText("确认并创建预约工单")).not.toBeInTheDocument();
rerender();
expect(screen.queryByText("本轮已结束 · 无法形成结论")).not.toBeInTheDocument();
expect(screen.getByText("上一轮报告,本轮分析中", { exact: true })).toBeVisible();
});
function taskPanel(task: OperationalTask, overrides: Partial> = {}) {
const props = {
task,
onContinue: vi.fn(),
onDecision: vi.fn(async () => undefined),
onExecute: vi.fn(async () => undefined),
onReconcile: vi.fn(async () => undefined),
onRefresh: vi.fn(),
onLoadEvidenceSnapshot: vi.fn(async () => { throw new Error("no snapshot"); }),
...overrides
};
const { rerender } = render();
const summary = screen.queryByLabelText("当前需处理")?.querySelector("summary");
if (summary) fireEvent.click(summary);
return { ...props, rerender };
}
it.each(["failed", "unsupported"] as const)("retries an approved %s action using its current version", async (status) => {
let finish!: () => void;
const onExecute = vi.fn(() => new Promise((resolve) => { finish = resolve; }));
const task: OperationalTask = { ...taskBase,
permissions: { ...taskBase.permissions, can_execute_work_orders: true },
actions: [{ ...taskBase.actions[0], approval_status: "approved", execution_status: status }]
};
const props = taskPanel(task, { onExecute });
const section = screen.getByRole("region", { name: "当前需处理" });
expect(section).toHaveTextContent("待重试");
const retry = within(section).getByRole("button", { name: "重试执行" });
fireEvent.click(retry);
expect(retry).toBeDisabled();
fireEvent.click(retry);
expect(onExecute).toHaveBeenCalledExactlyOnceWith(task.actions[0].action_id, task.actions[0].version);
expect(props.onDecision).not.toHaveBeenCalled();
finish();
await waitFor(() => expect(retry).toBeEnabled());
props.rerender();
expect(screen.queryByRole("button", { name: "重试执行" })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "核对执行回执" })).toBeVisible();
});
it("keeps retry disabled without work-order execution permission", () => {
const props = taskPanel({ ...taskBase, actions: [{ ...taskBase.actions[0],
approval_status: "approved", execution_status: "unsupported"
}] });
const retry = screen.getByRole("button", { name: "重试执行" });
expect(retry).toBeDisabled();
fireEvent.click(retry);
expect(props.onExecute).not.toHaveBeenCalled();
});
it.each(["pending", "rejected"] as const)("does not retry a %s action", (approval) => {
taskPanel({ ...taskBase, permissions: { ...taskBase.permissions, can_execute_work_orders: true },
actions: [{ ...taskBase.actions[0], approval_status: approval, execution_status: "failed" }]
});
expect(screen.queryByRole("button", { name: "重试执行" })).not.toBeInTheDocument();
});
it("keeps fixed question answers in the report and focuses them from a data gap", () => {
const task: OperationalTask = { ...taskBase, actions: [], active_run: null, lifecycle_status: "waiting_human",
questions: [{ question_request_id: "70000000-0000-4000-8000-000000000001", task_id: taskBase.task_id,
run_id: "70000000-0000-4000-8000-000000000002", source_session_id: "s", execution_generation: taskBase.execution_generation,
status: "pending", version: 1, created_at: taskBase.created_at, answered_at: null, answered_by: null, answers: [],
questions: [{ question_id: "sampling", title: "是否有采样记录", body: "请补充采样信息", options: [], mode: "single", allow_custom: true, required: true }] }],
latest_report: { ...taskBase.latest_report!, report: { ...taskBase.latest_report!.report,
data_gaps: [{ gap_id: "sampling", missing_data: "采样记录", scope: "事件时段", impact: "待核验",
required_data: "采样记录", evidence_refs: ["e-1"], blocking: false }] } }
};
const props = taskPanel(task);
const questions = screen.getByRole("region", { name: "调查提问" });
questions.scrollIntoView = vi.fn();
fireEvent.click(screen.getByRole("button", { name: "回答当前调查问题" }));
expect(questions).toHaveFocus();
expect(questions.scrollIntoView).toHaveBeenCalled();
expect(props.onContinue).not.toHaveBeenCalled();
});
it("labels free discussion separately when no fixed question is pending", () => {
const props = taskPanel({ ...taskBase, latest_report: { ...taskBase.latest_report!, report: {
...taskBase.latest_report!.report, data_gaps: [{ gap_id: "sampling", missing_data: "采样记录", scope: "事件时段",
impact: "待核验", required_data: "采样记录", evidence_refs: ["e-1"], blocking: false }]
} } });
expect(screen.queryByRole("button", { name: "补充事实" })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "讨论此缺口" }));
expect(props.onContinue).toHaveBeenCalled();
});
it("references the selected report round instead of the latest task round, including legacy reports", () => {
const task = { ...taskBase, execution_generation: 2 };
const { rerender, ...props } = taskPanel(task);
const button = screen.getByRole("button", { name: "Agent 协作" });
expect(button).toHaveAttribute("title", "打开调查协作对话,引用此任务,不会直接发送。");
fireEvent.click(button);
expect(props.onContinue).toHaveBeenLastCalledWith({
context: { report_revision: task.latest_report!.revision },
reportGeneration: task.latest_report!.execution_generation
});
rerender();
fireEvent.click(screen.getByRole("button", { name: "Agent 协作" }));
expect(props.onContinue).toHaveBeenLastCalledWith({
context: { report_revision: task.latest_report!.revision }, reportGeneration: null
});
});
it("opens the investigation execution record separately from Agent collaboration", async () => {
const onViewExecutionHistory = vi.fn(async () => undefined);
taskPanel(taskBase, { onViewExecutionHistory });
const executionHistory = screen.getByRole("button", { name: "执行记录" });
expect(executionHistory).toHaveAttribute("title", "在左侧 Agent 中打开所选轮次自动同步的调查执行记录,并可创建调试会话。");
fireEvent.click(executionHistory);
await waitFor(() => expect(onViewExecutionHistory).toHaveBeenCalledWith({
executionGeneration: 1,
sessionId: "execution-session"
}));
});
it("opens the execution record bound to the selected report round", async () => {
const currentReport = {
...taskBase.latest_report!,
revision: 2,
execution_generation: 2,
source_session_id: "execution-session-2"
};
const previousReport = {
...taskBase.latest_report!,
revision: 1,
execution_generation: 1,
source_session_id: "execution-session-1"
};
const onViewExecutionHistory = vi.fn(async () => undefined);
taskPanel({
...taskBase,
execution_generation: 2,
current_report_revision: 2,
latest_report: currentReport
}, {
onViewExecutionHistory,
onLoadReports: vi.fn(async () => ({ items: [currentReport, previousReport], next_offset: null }))
});
fireEvent.click(screen.getByRole("button", { name: "展开报告版本" }));
fireEvent.click(await screen.findByRole("button", { name: /第 1 轮 · 报告第 1 版/ }));
expect(screen.getByText("调查任务 · 第 1 轮", { exact: true })).toBeVisible();
expect(screen.queryByText("调查任务 · 第 2 轮", { exact: true })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "执行记录" }));
await waitFor(() => expect(onViewExecutionHistory).toHaveBeenCalledWith({
executionGeneration: 1,
sessionId: "execution-session-1"
}));
});
describe("operational task approval and evidence", () => {
it("shows the selected v2 route first and keeps unchosen work available", () => {
const node = (id: string, title: string, status: "completed" | "waiting" | "skipped") => ({
id, title, status, owner: "agent" as const, detail: title
});
const plan = {
nodes: [
{ ...node("risk", "判断紧急风险", "completed"), kind: "decision" as const, selected_edge_id: "urgent" },
{ ...node("urgent", "立即现场采样", "waiting"), kind: "activity" as const },
{ ...node("routine", "常规核查", "skipped"), kind: "activity" as const }
],
edges: [
{ id: "urgent", from: "risk", to: "urgent", label: "有紧急风险" },
{ id: "routine", from: "risk", to: "routine", label: "无紧急风险" }
]
};
render();
expect(screen.getByText(/路线选择:/).parentElement).toHaveTextContent("有紧急风险");
expect(screen.getByText("其他规划路线 1 项")).toBeVisible();
expect(screen.getAllByText("立即现场采样")[0]).toBeVisible();
});
it("groups nested parallel work at its recorded joins", () => {
const node = (id: string, kind: "activity" | "decision" | "parallel_split" | "parallel_join") => ({
id, title: id, detail: `${id}详情`, owner: "agent" as const, status: "completed" as const, kind
});
const plan = {
nodes: [node("start", "parallel_split"), node("field", "activity"), node("agent", "activity"),
node("read", "parallel_split"), node("upstream", "activity"), node("network", "activity"),
node("evidence", "parallel_join"), node("result", "parallel_join")],
edges: [
{ id: "e1", from: "start", to: "field", condition: "现场" },
{ id: "e2", from: "start", to: "agent", condition: "Agent" },
{ id: "e3", from: "field", to: "result" },
{ id: "e4", from: "agent", to: "read" },
{ id: "e5", from: "read", to: "upstream", condition: "上游" },
{ id: "e6", from: "read", to: "network", condition: "管网" },
{ id: "e7", from: "upstream", to: "evidence" },
{ id: "e8", from: "network", to: "evidence" },
{ id: "e9", from: "evidence", to: "result" }
]
};
const { container } = render();
expect(screen.getAllByLabelText("并行处理 2 路")).toHaveLength(2);
expect(container.querySelectorAll('[data-execution-node="evidence"]')).toHaveLength(1);
expect(container.querySelectorAll('[data-execution-node="result"]')).toHaveLength(1);
expect(container.querySelectorAll("[data-execution-node]")).toHaveLength(plan.nodes.length);
expect(screen.getByText("汇合前置:upstream(已完成)、network(已完成)")).toBeVisible();
});
it("keeps route alternatives beside the decision and expands node evidence and approvals", () => {
const actionId = taskBase.actions[0].action_id;
const plan = {
nodes: [
{ id: "risk", title: "判断风险", detail: "按投诉核对", owner: "agent" as const, status: "completed" as const, kind: "decision" as const, selected_edge_id: "urgent" },
{ id: "urgent", title: "现场采样", detail: "采样完成", owner: "human" as const, status: "completed" as const, kind: "activity" as const, evidence_refs: ["e-1"] },
{ id: "routine", title: "常规核查", detail: "未采用", owner: "human" as const, status: "skipped" as const, kind: "activity" as const },
{ id: "recheck", title: "再次采样", detail: "需复核", owner: "human" as const, status: "waiting" as const, kind: "activity" as const, revisits_node_id: "urgent" },
{ id: "approve", title: "确认工单", detail: "等待确认", owner: "human" as const, status: "waiting" as const, kind: "gate" as const, gate_ref: actionId, action_refs: [actionId] }
],
edges: [
{ id: "urgent", from: "risk", to: "urgent", condition: "有紧急风险" },
{ id: "routine", from: "risk", to: "routine", condition: "无紧急风险" },
{ id: "next", from: "urgent", to: "recheck" },
{ id: "approval", from: "recheck", to: "approve" }
]
};
const { rerender } = render();
expect(screen.getByText("其他规划路线 1 项")).toBeVisible();
fireEvent.click(screen.getByText("其他规划路线 1 项"));
expect(screen.getByRole("region", { name: "规划路线 无紧急风险" })).toContainElement(screen.getByText("常规核查"));
fireEvent.click(screen.getByText("查看关联详情 · 1 条证据"));
expect(screen.getByText("上游余氯处于正常范围")).toBeVisible();
fireEvent.click(screen.getAllByText("查看关联详情")[0]);
expect(screen.getByText("再次核验:现场采样")).toBeVisible();
fireEvent.click(screen.getAllByText("查看关联详情")[1]);
expect(screen.getByText(/审核关联:动作待确认,需要现场采样/)).toBeVisible();
expect(screen.getByText("需要现场采样 · 待确认 · 未提交")).toBeVisible();
rerender();
expect(screen.getByText("证据 e-1 不可用")).toBeVisible();
expect(screen.getByText("审核关联:关联记录不可用")).toBeVisible();
expect(screen.getByText(`动作 ${actionId} 不可用`)).toBeVisible();
});
it("shows both planned routes while a decision is pending", () => {
const plan = {
nodes: [
{ id: "risk", title: "判断风险", detail: "等待资料", owner: "agent" as const, status: "waiting" as const, kind: "decision" as const, selected_edge_id: null },
{ id: "urgent", title: "紧急处置", detail: "待判断", owner: "human" as const, status: "pending" as const, kind: "activity" as const },
{ id: "routine", title: "常规核查", detail: "待判断", owner: "human" as const, status: "pending" as const, kind: "activity" as const }
],
edges: [
{ id: "urgent", from: "risk", to: "urgent", condition: "有紧急风险" },
{ id: "routine", from: "risk", to: "routine", condition: "无紧急风险" }
]
};
render();
expect(screen.getByText("待选路线 2 项")).toBeVisible();
fireEvent.click(screen.getByText("待选路线 2 项"));
expect(screen.getByText("紧急处置")).toBeVisible();
expect(screen.getByText("常规核查")).toBeVisible();
expect(screen.queryByText("未选择")).not.toBeInTheDocument();
});
it("lets a reviewer edit a work-order draft before one-click dispatch", async () => {
const onDraftUpdate = vi.fn(async () => undefined);
const onConfirmDispatch = vi.fn(async () => undefined);
const initialTask = { ...taskBase, permissions: { ...taskBase.permissions, can_decide_work_orders: true, can_execute_work_orders: true },
actions: [{ ...taskBase.actions[0], payload: { title: "旧草稿", description: "现场采样", work_order_type: "water_quality_complaint", priority: "normal" } }] };
const props = taskPanel(initialTask, { onDraftUpdate, onConfirmDispatch });
expect(screen.getByRole("heading", { name: "关联预约工单" })).toBeVisible();
expect(screen.getByText(/调查任务只保留创建关联/)).toBeVisible();
expect(screen.getByText("预约工单")).toBeVisible();
fireEvent.click(screen.getByRole("button", { name: "编辑草稿" }));
expect(screen.queryByRole("combobox", { name: "工单类型" })).not.toBeInTheDocument();
expect(screen.getByText("预约任务")).toBeVisible();
fireEvent.change(screen.getByRole("textbox", { name: "工单标题" }), { target: { value: "人工修订草稿" } });
fireEvent.click(screen.getByRole("button", { name: "保存草稿" }));
await waitFor(() => expect(onDraftUpdate).toHaveBeenCalledWith(taskBase.actions[0].action_id, 3, expect.objectContaining({ title: "人工修订草稿", work_order_type: "general" })));
await waitFor(() => expect(screen.getByRole("button", { name: "确认并创建预约工单" })).toBeDisabled());
props.rerender();
fireEvent.click(screen.getByRole("button", { name: "确认并创建预约工单" }));
await waitFor(() => expect(onConfirmDispatch).toHaveBeenCalledWith(taskBase.actions[0].action_id, 4));
});
it("distinguishes the selected report from the current investigation round", () => {
taskPanel(taskBase);
expect(screen.getByText("历史报告内容", { exact: true })).toBeVisible();
expect(screen.getByText("结果未记录", { exact: true })).toBeVisible();
const previousReport = {
...taskBase,
status: "running" as const,
execution_generation: 2,
latest_report: { ...taskBase.latest_report!, execution_generation: 1 }
};
const { rerender } = render();
expect(screen.getByText("上一轮报告,本轮分析中", { exact: true })).toBeVisible();
expect(screen.queryByText(/第\s*[12]\s*代|generation/i)).not.toBeInTheDocument();
rerender();
expect(screen.getByText("本轮执行失败", { exact: true })).toBeVisible();
expect(screen.getByText("上一轮报告", { exact: true })).toBeVisible();
expect(screen.queryByText("上一轮报告,本轮分析中", { exact: true })).not.toBeInTheDocument();
rerender();
expect(screen.getByText("本轮已取消", { exact: true })).toBeVisible();
rerender();
// Live reading follows published reports without requiring an update click.
expect(screen.queryByRole("button", { name: "查看新报告" })).not.toBeInTheDocument();
expect(screen.getByText(/正在阅读:历史报告/)).toBeVisible();
});
it("keeps plan nodes as explanatory relationships without presenting their statuses as task progress", () => {
const latestPlan: NonNullable = {
revision: 1,
execution_generation: 1,
source_session_id: "execution-session",
published_at: "2026-09-23T01:05:00Z",
published_by: "agent",
plan: {
schema: "execution-plan@1",
title: "调查流程",
reason: "核对真实状态",
nodes: [{
id: "investigate",
title: "核查事件",
owner: "agent",
status: "completed",
detail: "上一轮已完成。",
evidence_refs: [],
action_refs: []
}],
edges: []
}
};
const currentPlanTask = { ...taskBase, latest_plan: latestPlan };
const props = {
onContinue: vi.fn(), onDecision: vi.fn(), onExecute: vi.fn(), onReconcile: vi.fn(),
onRefresh: vi.fn(), onLoadEvidenceSnapshot: vi.fn()
};
const { rerender } = render();
expect(screen.getByRole("heading", { name: "调查脉络" })).toBeVisible();
expect(screen.queryByText(/已完成 1 项/)).not.toBeInTheDocument();
rerender();
expect(screen.getByText("上一轮报告,本轮分析中", { exact: true })).toBeVisible();
expect(screen.queryByText(/已完成 1 项/)).not.toBeInTheDocument();
rerender();
expect(screen.getByText("上一轮报告", { exact: true })).toBeVisible();
rerender();
// An unassigned historical plan cannot supply a numbered report's diagram.
expect(screen.queryByRole("heading", { name: "调查脉络" })).not.toBeInTheDocument();
expect(within(screen.getByRole("region", { name: "任务报告" }))
.getByText(taskBase.latest_report!.report.current_conclusion, { exact: true })).toBeVisible();
rerender();
// The legacy relationship remains readable when both records lack a round.
expect(screen.getByRole("heading", { name: "调查脉络" })).toBeVisible();
expect(screen.queryByText(/generation|第\s*\d+\s*代/i)).not.toBeInTheDocument();
});
it("shows exact payload and evidence metadata to a viewer without action controls", async () => {
taskPanel(taskBase);
expect(screen.getByText("工单标题")).toBeVisible();
expect(screen.getByText("位置")).toBeVisible();
expect(screen.getByText("预约时间")).toBeVisible();
expect(screen.getByText("现场采样")).toBeVisible();
expect(screen.getByText("演示小区")).toBeVisible();
expect(screen.getByText("确认依据")).toBeVisible();
expect(screen.getByText(/上游余氯处于正常范围 · SCADA 水质监测/)).toBeVisible();
expect(screen.getByText(/当前账号仅可查看/)).toBeVisible();
expect(screen.queryByText(/operational_action\.work_order\.approve/)).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "批准创建预约工单" })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "展开主要依据" }));
fireEvent.click(screen.getByRole("dialog", { name: "主要依据" }).querySelector("summary")!);
await waitFor(() => expect(within(screen.getByRole("dialog", { name: "主要依据" })).getByText("SCADA 水质监测")).toBeVisible());
expect(within(screen.getByRole("dialog", { name: "主要依据" })).getByText("系统数据")).toBeVisible();
});
it("keeps pending state after a decision network error", async () => {
const onDecision = vi.fn(async () => { throw new Error("network unavailable"); });
taskPanel({ ...taskBase, permissions: { ...taskBase.permissions, can_decide_work_orders: true } }, { onDecision });
fireEvent.click(screen.getByRole("button", { name: "批准创建预约工单" }));
expect(await screen.findByRole("alert")).toHaveTextContent("当前状态未按成功处理");
expect(screen.getByText("待确认")).toBeVisible();
});
it("requires refresh and reconfirmation after a version conflict", async () => {
const onRefresh = vi.fn();
const onDecision = vi.fn(async () => { throw new Error("409 version conflict"); });
taskPanel({ ...taskBase, permissions: { ...taskBase.permissions, can_decide_work_orders: true } }, { onDecision, onRefresh });
fireEvent.click(screen.getByRole("button", { name: "批准创建预约工单" }));
const refresh = await screen.findByRole("button", { name: "刷新后重新确认" });
fireEvent.click(refresh);
await waitFor(() => expect(onRefresh).toHaveBeenCalledOnce());
});
it("allows unknown submissions to reconcile and shows a persisted receipt", async () => {
const onReconcile = vi.fn(async () => undefined);
const unknownAction = {
...taskBase.actions[0],
approval_status: "approved" as const,
execution_status: "unknown" as const,
execution_request_id: "55555555-5555-4555-8555-555555555555"
};
const receiptAction = {
...unknownAction,
execution_status: "succeeded" as const,
receipt: {
adapter: "peripheral",
receipt_id: "receipt-20260923-01",
submitted_at: "2026-09-23T01:10:00Z",
completed_at: "2026-09-23T01:11:00Z",
external_status: "completed",
detail: "外围工单创建完成"
}
};
const { rerender } = render();
fireEvent.click(screen.getByText("现场采样 · 待核对回执"));
fireEvent.click(screen.getByRole("button", { name: "核对执行回执" }));
await waitFor(() => expect(onReconcile).toHaveBeenCalledWith(unknownAction.action_id, unknownAction.version));
rerender();
expect(screen.getByLabelText("处置进展")).toHaveTextContent("工单已创建");
expect(screen.getByLabelText("处置进展")).toHaveTextContent("动作回执只说明对应系统操作结果");
fireEvent.click(screen.getByText("现场采样 · 工单已创建"));
expect(screen.getByRole("region", { name: "预约工单创建回执" })).toHaveTextContent("receipt-20260923-01");
expect(screen.getByRole("region", { name: "预约工单创建回执" })).toHaveTextContent("外围工单创建完成");
});
it("orders evidence, report, approval, and receipt in one real timeline", () => {
const completedAction = {
...taskBase.actions[0],
approval_status: "approved" as const,
execution_status: "succeeded" as const,
decided_at: "2026-09-23T01:11:00Z",
executed_at: "2026-09-23T01:12:00Z",
receipt: {
adapter: "peripheral",
receipt_id: "receipt-timeline",
submitted_at: "2026-09-23T01:11:30Z",
completed_at: "2026-09-23T01:12:00Z",
external_status: "open",
detail: "预约工单已创建,等待现场处理"
}
};
taskPanel({
...taskBase,
latest_report: { ...taskBase.latest_report!, published_at: "2026-09-23T01:10:00Z" },
actions: [completedAction]
});
fireEvent.click(screen.getByRole("button", { name: "展开任务时间线" }));
const items = [...screen.getByLabelText("任务时间线记录").querySelectorAll("li")];
expect(items.map((item) => item.textContent)).toEqual([
expect.stringContaining("预约工单已创建,等待现场处理"),
expect.stringContaining("现场采样已批准"),
expect.stringContaining("报告第 1 版已发布"),
expect.stringContaining("现场采样已提出"),
expect.stringContaining("上游余氯处于正常范围")
]);
});
});