feat(workbench): redesign investigation reports and diagrams

This commit is contained in:
2026-10-10 16:02:47 +08:00
parent 85a9a3ab45
commit ba78a24384
27 changed files with 2380 additions and 187 deletions
@@ -96,6 +96,23 @@ const taskBase = {
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(<AgentExecutionPlanPanel {...props} task={{ ...task, execution_generation: 2, status: "running", lifecycle_status: "open" }} />);
expect(screen.queryByText("本轮已结束 · 无法形成结论")).not.toBeInTheDocument();
expect(screen.getByText("上一轮报告,本轮分析中", { exact: true })).toBeVisible();
});
function taskPanel(task: OperationalTask, overrides: Partial<ComponentProps<typeof AgentExecutionPlanPanel>> = {}) {
const props = {
task,
@@ -108,6 +125,8 @@ function taskPanel(task: OperationalTask, overrides: Partial<ComponentProps<type
...overrides
};
const { rerender } = render(<AgentExecutionPlanPanel {...props} />);
const summary = screen.queryByLabelText("当前需处理")?.querySelector("summary");
if (summary) fireEvent.click(summary);
return { ...props, rerender };
}
@@ -158,26 +177,87 @@ describe("operational task approval and evidence", () => {
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(<ExecutionPlanSteps plan={plan} context={taskBase} onLoadEvidenceSnapshot={vi.fn()} />);
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(<ExecutionPlanSteps plan={plan} context={{ evidence: [], access_requests: [], actions: [] }} onLoadEvidenceSnapshot={vi.fn()} />);
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(<ExecutionPlanSteps plan={plan} />);
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: "general", priority: "normal" } }] };
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: "人工修订草稿" })));
await waitFor(() => expect(screen.getByRole("button", { name: "确认并下发工单" })).toBeDisabled());
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(<AgentExecutionPlanPanel {...props} task={{ ...initialTask, actions: [{ ...initialTask.actions[0], version: 4, payload: { ...initialTask.actions[0].payload, title: "人工修订草稿" } }] }} />);
fireEvent.click(screen.getByRole("button", { name: "确认并下发工单" }));
fireEvent.click(screen.getByRole("button", { name: "确认并创建预约工单" }));
await waitFor(() => expect(onConfirmDispatch).toHaveBeenCalledWith(taskBase.actions[0].action_id, 4));
});
it("distinguishes the current report from a previous execution without exposing generation numbers", () => {
it("distinguishes the selected report from the current investigation round", () => {
taskPanel(taskBase);
expect(screen.getByText("当前结论", { exact: true })).toBeVisible();
expect(screen.getByText("历史报告内容", { exact: true })).toBeVisible();
expect(screen.getByText("结果未记录", { exact: true })).toBeVisible();
const previousReport = {
...taskBase,
@@ -194,29 +274,31 @@ describe("operational task approval and evidence", () => {
expect(screen.queryByText(/第\s*[12]\s*代|generation/i)).not.toBeInTheDocument();
rerender(<AgentExecutionPlanPanel
task={{ ...previousReport, status: "failed" }}
task={{ ...previousReport, status: "failed", lifecycle_status: "failed" }}
onContinue={vi.fn()} onDecision={vi.fn()} onExecute={vi.fn()} onReconcile={vi.fn()}
onRefresh={vi.fn()} onLoadEvidenceSnapshot={vi.fn()}
/>);
expect(screen.getByText("上一轮报告,当前任务失败", { exact: true })).toBeVisible();
expect(screen.getByText("本轮执行失败", { exact: true })).toBeVisible();
expect(screen.getByText("上一轮报告", { exact: true })).toBeVisible();
expect(screen.queryByText("上一轮报告,本轮分析中", { exact: true })).not.toBeInTheDocument();
rerender(<AgentExecutionPlanPanel
task={{ ...previousReport, status: "cancelled" }}
task={{ ...previousReport, status: "cancelled", lifecycle_status: "cancelled" }}
onContinue={vi.fn()} onDecision={vi.fn()} onExecute={vi.fn()} onReconcile={vi.fn()}
onRefresh={vi.fn()} onLoadEvidenceSnapshot={vi.fn()}
/>);
expect(screen.getByText("上一轮报告,当前任务已取消", { exact: true })).toBeVisible();
expect(screen.getByText("本轮已取消", { exact: true })).toBeVisible();
rerender(<AgentExecutionPlanPanel
task={{ ...previousReport, latest_report: { ...previousReport.latest_report!, execution_generation: null } }}
task={{ ...previousReport, latest_report: { ...previousReport.latest_report!, revision: 2, execution_generation: null } }}
onContinue={vi.fn()} onDecision={vi.fn()} onExecute={vi.fn()} onReconcile={vi.fn()}
onRefresh={vi.fn()} onLoadEvidenceSnapshot={vi.fn()}
/>);
expect(screen.getByText("历史报告", { exact: true })).toBeVisible();
fireEvent.click(screen.getByRole("button", { name: "查看新报告" }));
expect(screen.getByText(/正在阅读:历史报告/)).toBeVisible();
});
it("labels current, previous and legacy execution plans without presenting old progress as current", () => {
it("keeps plan nodes as explanatory relationships without presenting their statuses as task progress", () => {
const latestPlan: NonNullable<OperationalTask["latest_plan"]> = {
revision: 1,
execution_generation: 1,
@@ -245,26 +327,27 @@ describe("operational task approval and evidence", () => {
onRefresh: vi.fn(), onLoadEvidenceSnapshot: vi.fn()
};
const { rerender } = render(<AgentExecutionPlanPanel task={currentPlanTask} {...props} />);
expect(screen.getByText("已完成 1 项 · 执行中 0 项 · 待人工或资料 0 项", { exact: true })).toBeVisible();
expect(screen.getByRole("heading", { name: "调查脉络" })).toBeVisible();
expect(screen.queryByText(/已完成 1 项/)).not.toBeInTheDocument();
rerender(<AgentExecutionPlanPanel
task={{ ...currentPlanTask, status: "running", execution_generation: 2 }}
{...props}
/>);
expect(screen.getByText("上一轮流程,本轮规划中", { exact: true })).toBeVisible();
expect(screen.getByText("上一轮报告,本轮分析中", { exact: true })).toBeVisible();
expect(screen.queryByText(/已完成 1 项/)).not.toBeInTheDocument();
rerender(<AgentExecutionPlanPanel
task={{ ...currentPlanTask, status: "failed", execution_generation: 2 }}
{...props}
/>);
expect(screen.getByText("上一轮流程,当前任务失败", { exact: true })).toBeVisible();
expect(screen.getByText("上一轮报告", { exact: true })).toBeVisible();
rerender(<AgentExecutionPlanPanel
task={{ ...currentPlanTask, latest_plan: { ...latestPlan, execution_generation: null } }}
{...props}
/>);
expect(screen.getByText("历史流程", { exact: true })).toBeVisible();
expect(screen.getByRole("heading", { name: "调查脉络" })).toBeVisible();
expect(screen.queryByText(/generation|第\s*\d+\s*代/i)).not.toBeInTheDocument();
});
@@ -279,8 +362,8 @@ describe("operational task approval and evidence", () => {
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.getByText("上游余氯处于正常范围"));
expect(screen.queryByRole("button", { name: "批准创建预约工单" })).not.toBeInTheDocument();
fireEvent.click(screen.getByLabelText("主要依据").querySelector("summary")!);
expect(screen.getByText("SCADA 水质监测")).toBeVisible();
expect(screen.getByText("系统数据")).toBeVisible();
});
@@ -288,7 +371,7 @@ describe("operational task approval and evidence", () => {
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: "批准创建工单" }));
fireEvent.click(screen.getByRole("button", { name: "批准创建预约工单" }));
expect(await screen.findByRole("alert")).toHaveTextContent("当前状态未按成功处理");
expect(screen.getByText("待确认")).toBeVisible();
});
@@ -297,7 +380,7 @@ describe("operational task approval and evidence", () => {
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: "批准创建工单" }));
fireEvent.click(screen.getByRole("button", { name: "批准创建预约工单" }));
const refresh = await screen.findByRole("button", { name: "刷新后重新确认" });
fireEvent.click(refresh);
await waitFor(() => expect(onRefresh).toHaveBeenCalledOnce());
@@ -328,6 +411,7 @@ describe("operational task approval and evidence", () => {
onContinue={vi.fn()} onDecision={vi.fn()} onExecute={vi.fn()} onReconcile={onReconcile}
onRefresh={vi.fn()} onLoadEvidenceSnapshot={vi.fn()}
/>);
fireEvent.click(screen.getByText("现场采样 · 待核对回执"));
fireEvent.click(screen.getByRole("button", { name: "核对执行回执" }));
await waitFor(() => expect(onReconcile).toHaveBeenCalledWith(unknownAction.action_id, unknownAction.version));
@@ -336,7 +420,43 @@ describe("operational task approval and evidence", () => {
onContinue={vi.fn()} onDecision={vi.fn()} onExecute={vi.fn()} onReconcile={onReconcile}
onRefresh={vi.fn()} onLoadEvidenceSnapshot={vi.fn()}
/>);
expect(screen.getByRole("region", { name: "执行回执" })).toHaveTextContent("receipt-20260923-01");
expect(screen.getByRole("region", { name: "执行回执" })).toHaveTextContent("外围工单创建完成");
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.getByText("任务时间线"));
const items = [...screen.getByLabelText("任务时间线记录").querySelectorAll("li")];
expect(items.map((item) => item.textContent)).toEqual([
expect.stringContaining("预约工单已创建,等待现场处理"),
expect.stringContaining("现场采样已批准"),
expect.stringContaining("报告第 1 版已发布"),
expect.stringContaining("现场采样已提出"),
expect.stringContaining("上游余氯处于正常范围")
]);
});
});