fix(workbench): stabilize investigation presentation rendering

Keep investigation graphs stable during updates, defer expensive chart rendering, and prevent stale results when switching tasks. Adopt reference annotations and consolidate frontend documentation.
This commit is contained in:
2026-11-11 19:10:41 +08:00
parent 0ae0a41e61
commit a617aa95e9
35 changed files with 1268 additions and 715 deletions
@@ -0,0 +1,92 @@
import { expect, test } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
for (const width of [1440, 375]) {
test(`retains failed investigation paths through updates and new rounds at ${width}px`, async ({ page }, testInfo) => {
await page.setViewportSize({ width, height: 1000 });
await page.emulateMedia({ reducedMotion: "reduce" });
await mockAgentApi(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.setContent('<html lang="zh-CN"><head><link rel="stylesheet" href="/src/styles.css"></head><body><main id="reader" class="workbench-shadcn-surface bg-background text-foreground" style="max-width:900px;margin:auto;padding:12px"></main></body></html>');
await page.evaluate(async () => {
const entry = await (await fetch("/src/main.tsx")).text();
const reactUrl = entry.match(/from "([^"\n]*\/react\.js[^"\n]*)"/)?.[1];
const clientUrl = entry.match(/from "([^"\n]*\/react-dom_client\.js[^"\n]*)"/)?.[1];
if (!reactUrl || !clientUrl) throw new Error("Vite React module paths unavailable");
const reactModule = await import(reactUrl);
const React = reactModule.default ?? reactModule;
const clientModule = await import(clientUrl);
const { createRoot } = clientModule.default ?? clientModule;
const { InvestigationTaskReport } = await import("/src/features/workbench/components/investigation-task-report.tsx");
const { investigationDiagramSchema } = await import("/src/features/workbench/api/investigation-report.ts");
const now = "2026-11-10T01:00:00Z";
const initial = investigationDiagramSchema.parse({
title: "SCADA 累计调查脉络", source_mermaid: null,
mermaid: 'flowchart TD\nread["读取全量 SCADA"]\nlocate_1["第一次定位(失败)"]\nread --> locate_1',
node_refs: [
{ node_id: "read", label: "读取全量 SCADA", status: "recorded" },
{ node_id: "locate_1", label: "第一次定位", status: "failed", detail: "计算超时,已保留本次失败记录。" }
], edges: [{ source: "read", target: "locate_1" }]
});
const report = { revision: 1, execution_generation: 1, published_at: now, source_session_id: "session-1", report: {
title: "SCADA 调查", current_conclusion: "首次定位超时,继续核查。", conclusion_boundary: "现有资料",
stage: "final", outcome: "concluded", data_gaps: [], evidence_refs: [], recommendations: [], pending_items: [],
diagram: initial
} };
let task = { task_id: "cumulative-graph", title: "SCADA 数据问题调查", summary: "保留失败路径后继续调查",
execution_generation: 1, status: "running", lifecycle_status: "open", latest_report: report,
latest_plan: null, actions: [], evidence: [], questions: [],
active_run: { run_id: "run-1", status: "running" },
latest_progress: { revision: 1, run_id: "run-1", execution_generation: 1, published_at: now,
progress: { summary: "首次定位失败", diagram: initial } }
};
const root = createRoot(document.getElementById("reader"));
const render = () => root.render(React.createElement(InvestigationTaskReport, {
task, onContinue: () => undefined, renderAction: () => null, renderEvidence: () => null,
onLoadReports: async () => ({ items: [report], next_offset: null })
}));
Object.assign(window, { advanceInvestigationGraph: () => {
const diagram = investigationDiagramSchema.parse({ ...initial,
mermaid: initial.mermaid + '\nlocate_2["调整窗口重试"]\nlocate_1 -->|"重试"| locate_2',
node_refs: [...initial.node_refs, { node_id: "locate_2", label: "调整窗口重试", status: "recorded" }],
edges: [...initial.edges!, { source: "locate_1", target: "locate_2", label: "重试" }]
});
task = { ...task, execution_generation: 2, active_run: { run_id: "run-2", status: "running" },
latest_progress: { ...task.latest_progress, execution_generation: 2, run_id: "run-2", revision: 1,
progress: { summary: "重试成功,原失败记录保留", diagram } }
};
render();
}, startNextGraphRound: () => { task = { ...task, execution_generation: 3 }; render(); },
stopGraphRun: () => {
Object.assign(task, { active_run: null, status: "completed", lifecycle_status: "completed" });
render();
} });
render();
});
const graph = page.getByRole("region", { name: "调查脉络", exact: true });
await expect(graph.locator("svg g.node")).toHaveCount(2);
await expect(graph.locator("svg g.node.tjFailed")).toHaveCount(1);
await graph.getByRole("button", { name: /^第一次定位 ·/ }).click();
await expect(graph.getByLabel("节点详情 第一次定位")).toContainText("计算超时");
await page.evaluate(() => (window as unknown as { advanceInvestigationGraph: () => void }).advanceInvestigationGraph());
await expect(graph.locator("svg g.node")).toHaveCount(3);
await expect(graph.locator("svg g.node.tjFailed")).toHaveCount(1);
await expect(graph.locator("svg g.node.tjRecorded")).toHaveCount(2);
await expect(graph.locator("svg g.node").filter({ hasText: "第一次定位(失败)" })).toHaveCount(1);
await expect(graph.getByLabel("节点详情 第一次定位")).toContainText("计算超时");
await page.evaluate(() => (window as unknown as { startNextGraphRound: () => void }).startNextGraphRound());
await expect(graph.locator("svg g.node")).toHaveCount(3);
await expect(graph.locator("svg g.node.tjFailed > rect")).toHaveCSS("fill", "rgb(254, 242, 242)");
await page.evaluate(() => (window as unknown as { stopGraphRun: () => void }).stopGraphRun());
await expect(graph.locator("svg g.node.tjRecorded")).toHaveCount(2);
await expect(graph.locator("svg g.node.tjFailed")).toHaveCount(1);
await expect(graph).toContainText("不实时同步执行状态");
await graph.screenshot({ path: testInfo.outputPath(`cumulative-failed-path-${width}.png`) });
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
await page.getByRole("button", { name: "展开报告版本", exact: true }).click();
await page.getByRole("dialog", { name: "报告版本", exact: true }).getByRole("button", { name: /第 1 轮 · 报告第 1 版/ }).click();
await expect(graph.locator("svg g.node")).toHaveCount(2);
await page.getByRole("button", { name: "返回实时内容", exact: true }).click();
await expect(graph.locator("svg g.node")).toHaveCount(3);
});
}