205 lines
7.4 KiB
TypeScript
205 lines
7.4 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { InvestigationPresentationHistoryEvent } from "../api/investigation-presentation";
|
|
import type { InvestigationMapPresentation } from "../map/investigation-map-presentation";
|
|
import { presentationHistoryPorts } from "../map/presentation-history-ports";
|
|
import { hydrateInvestigationPresentationArchive } from "./investigation-presentation-archive";
|
|
import { PresentationViewHistory } from "./presentation-view-history";
|
|
|
|
const task = {
|
|
task_id: "50000000-0000-4000-8000-000000000001",
|
|
title: "已保存调查",
|
|
execution_generation: 2,
|
|
evidence: [],
|
|
latest_report: null
|
|
};
|
|
const chart = (title: string) => ({
|
|
action: "open_chart",
|
|
chart_id: "pressure",
|
|
evidence_refs: ["reading"],
|
|
chart: { title, chart_type: "line", x_data: ["08:00"], series: [{ name: "压力", data: [20] }] }
|
|
});
|
|
const event = (
|
|
event_seq: number,
|
|
command: unknown,
|
|
execution_generation = 2
|
|
): InvestigationPresentationHistoryEvent => ({
|
|
task_id: task.task_id,
|
|
event_seq,
|
|
run_id: "55000000-0000-4000-8000-000000000001",
|
|
execution_generation,
|
|
occurred_at: "2026-10-27T08:00:00Z",
|
|
command
|
|
});
|
|
function setup() {
|
|
const history = new PresentationViewHistory();
|
|
const scene = {
|
|
highlight: vi.fn(),
|
|
clearHighlights: vi.fn(),
|
|
setOverlay: vi.fn(),
|
|
clearOverlay: vi.fn(),
|
|
renderScadaAnalysis: vi.fn(),
|
|
clearScadaAnalysis: vi.fn()
|
|
};
|
|
const controller = {
|
|
queryPresentationFeatures: vi.fn(async () => ({
|
|
type: "FeatureCollection" as const,
|
|
features: [
|
|
{
|
|
type: "Feature" as const,
|
|
properties: { device_id: "P1" },
|
|
geometry: { type: "Point" as const, coordinates: [121.4, 31.1] }
|
|
}
|
|
]
|
|
}))
|
|
};
|
|
const createPorts = (staging: PresentationViewHistory) =>
|
|
presentationHistoryPorts({
|
|
controller,
|
|
scene: scene as unknown as InvestigationMapPresentation,
|
|
map: () => null,
|
|
padding: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
|
|
update: (updater) => staging.update(updater(staging.current)),
|
|
rendering: () => false,
|
|
cameraAllowed: () => true,
|
|
waitForChart: async () => undefined,
|
|
frame: async () => undefined
|
|
});
|
|
const load = (
|
|
events: InvestigationPresentationHistoryEvent[],
|
|
signal = new AbortController().signal
|
|
) => hydrateInvestigationPresentationArchive({ events, task, history, createPorts, signal });
|
|
return { history, scene, controller, load, createPorts };
|
|
}
|
|
|
|
describe("saved investigation presentation", () => {
|
|
it("hydrates sequential snapshots, applies cleanup without history rows and deduplicates task reselection", async () => {
|
|
const test = setup();
|
|
const events = [
|
|
event(1, {
|
|
action: "highlight_features",
|
|
layer: "scada",
|
|
feature_ids: ["P1"],
|
|
effect: "pulse"
|
|
}),
|
|
event(2, chart("原始压力")),
|
|
event(3, { action: "close_chart", chart_id: "pressure" }),
|
|
event(4, { action: "clear_highlights" }),
|
|
event(5, chart("最终压力"))
|
|
];
|
|
const current = await test.load(events);
|
|
expect(current.charts[0].chart.title).toBe("最终压力");
|
|
expect(current.highlights.features).toHaveLength(0);
|
|
expect(test.history.entries).toHaveLength(4);
|
|
expect(test.history.entries[1].snapshot.charts[0].chart.title).toBe("原始压力");
|
|
expect(test.history.entries[1].snapshot.highlights.features).toHaveLength(1);
|
|
const again = await test.load(events);
|
|
expect(again.charts[0].chart.title).toBe("最终压力");
|
|
expect(test.history.entries).toHaveLength(4);
|
|
expect(test.controller.queryPresentationFeatures).toHaveBeenCalledTimes(1);
|
|
expect(Object.values(test.scene).every((fn) => fn.mock.calls.length === 0)).toBe(true);
|
|
});
|
|
it("retains prior generations as history without leaking their charts into the current generation", async () => {
|
|
const test = setup();
|
|
const next = await test.load([event(1, chart("上一轮"), 1), event(2, chart("本轮"))]);
|
|
expect(next.charts.map((item) => item.chart.title)).toEqual(["本轮"]);
|
|
expect(test.history.entries[0].taskTitle).toContain("第 1 轮");
|
|
const previousOnly = await test.load([event(1, chart("上一轮"), 1)]);
|
|
expect(previousOnly.charts).toEqual([]);
|
|
});
|
|
it("rejects unregistered commands individually and refuses another task's records", async () => {
|
|
const test = setup();
|
|
const current = await test.load([
|
|
event(1, { action: "execute", command: "unsafe" }),
|
|
event(2, chart("有效曲线"))
|
|
]);
|
|
expect(test.history.entries.map((item) => item.status)).toEqual(["error", "completed"]);
|
|
expect(current.charts[0].chart.title).toBe("有效曲线");
|
|
await expect(
|
|
test.load([
|
|
{ ...event(3, chart("错误任务")), task_id: "50000000-0000-4000-8000-000000000002" }
|
|
])
|
|
).rejects.toThrow("不属于所选调查");
|
|
});
|
|
it("does not append late results after a task switch aborts a feature lookup", async () => {
|
|
const test = setup();
|
|
let done!: () => void;
|
|
const pending = new Promise<void>((resolve) => {
|
|
done = resolve;
|
|
});
|
|
test.controller.queryPresentationFeatures.mockImplementationOnce(async () => {
|
|
await pending;
|
|
return { type: "FeatureCollection", features: [] };
|
|
});
|
|
const controller = new AbortController();
|
|
const loading = test.load(
|
|
[event(1, { action: "locate_features", layer: "scada", feature_ids: ["P1"] })],
|
|
controller.signal
|
|
);
|
|
controller.abort();
|
|
done();
|
|
await expect(loading).rejects.toThrow();
|
|
expect(test.history.entries).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
it("loads saved report blocks without display events and does not duplicate them on reselection", async () => {
|
|
const test = setup();
|
|
const reportTask = {
|
|
...task,
|
|
latest_report: {
|
|
revision: 3,
|
|
execution_generation: 2,
|
|
source_session_id: "saved",
|
|
published_by: "agent",
|
|
published_at: "2026-10-27T08:00:00Z",
|
|
report: {
|
|
title: "保存的调查报告",
|
|
current_conclusion: "待现场核查",
|
|
conclusion_boundary: "合成测试",
|
|
evidence_refs: [],
|
|
recommendations: [],
|
|
pending_items: [],
|
|
ui_blocks: [
|
|
{
|
|
evidence_refs: [],
|
|
block: {
|
|
version: "answer-ui@1" as const,
|
|
block_id: "saved-valves",
|
|
phase: "final" as const,
|
|
fallback_text: "阀门数据",
|
|
spec: {
|
|
root: "note",
|
|
elements: {
|
|
note: {
|
|
type: "Text" as const,
|
|
props: { text: "已保存阀门核查结果", tone: "neutral" as const },
|
|
children: []
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
};
|
|
const first = await hydrateInvestigationPresentationArchive({
|
|
events: [],
|
|
task: reportTask,
|
|
history: test.history,
|
|
createPorts: () => ({}) as never,
|
|
signal: new AbortController().signal
|
|
});
|
|
expect(first.blocks[0].block_id).toBe("saved-valves");
|
|
const second = await hydrateInvestigationPresentationArchive({
|
|
events: [],
|
|
task: reportTask,
|
|
history: test.history,
|
|
createPorts: () => ({}) as never,
|
|
signal: new AbortController().signal
|
|
});
|
|
expect(second.blocks).toBe(first.blocks);
|
|
expect(test.history.entries).toHaveLength(1);
|
|
expect(test.history.entries[0].label).toContain("报告第 3 版");
|
|
});
|