import { describe, expect, it, vi } from "vitest"; import type { FeatureCollection } from "geojson"; import { presentationCommandSchema } from "@/features/agent"; import { executePresentationCommand, type PresentationPorts } from "./presentation-command-executor"; import { parseInvestigationDevJsonl } from "../utils/investigation-dev-preview"; import { investigationPresentationEventSchema } from "../api/investigation-presentation"; const chart = { title: "压力", chart_type: "line", x_data: ["10:00"], series: [{ name: "D1", data: [0.4] }] }; const data: FeatureCollection = { type: "FeatureCollection", features: [ { type: "Feature", properties: { id: "D1" }, geometry: { type: "Point", coordinates: [121, 31] } } ] }; const commands = [ { action: "locate_features", layer: "scada", feature_ids: ["D1", "missing"] }, { action: "zoom_to_map", center: [121, 31], zoom: 14 }, { action: "highlight_features", layer: "scada", feature_ids: ["D1", "missing"], effect: "pulse" }, { action: "clear_highlights" }, { action: "open_chart", chart_id: "pressure", chart, evidence_refs: ["E1"] }, { action: "close_chart", chart_id: "pressure" }, { action: "set_overlay", overlay_id: "points", title: "证据", mode: "geometry", evidence_id: "E1", data }, { action: "clear_overlay", overlay_id: "points" }, { action: "render_scada_analysis", items: [{ device_id: "D1", level: "high" }] }, { action: "clear_scada_analysis" } ]; function ports(): PresentationPorts { return { controller: { queryPresentationFeatures: vi.fn(async () => data) }, scene: { highlight: vi.fn(), clearHighlights: vi.fn(), setOverlay: vi.fn(), clearOverlay: vi.fn(), renderScadaAnalysis: vi.fn(() => ({ rendered_ids: ["D1"], missing_ids: [], fitted: true as const, level_counts: { high: 1, medium: 0, low: 0, unrated: 0 } })), clearScadaAnalysis: vi.fn(() => ({ cleared: true as const })) }, cameraAllowed: () => false, zoom: vi.fn(async () => {}), openChart: vi.fn(async () => {}), closeChart: vi.fn(async () => {}), overlayChanged: vi.fn(), highlightsChanged: vi.fn() }; } describe("shared presentation execution", () => { it.each(commands)( "accepts the same $action payload through chat, Dev and task events", async (input) => { const chat = presentationCommandSchema.parse(input); const preview = parseInvestigationDevJsonl( JSON.stringify({ record_type: "presentation", command: input }) ).commands[0]; const task = investigationPresentationEventSchema.parse({ task_id: "71000000-0000-4000-8000-000000000001", run_id: "71000000-0000-4000-8000-000000000002", execution_generation: 1, command: input }).command; const results = await Promise.all( [chat, preview, task].map((command) => executePresentationCommand(command, new AbortController().signal, ports()) ) ); expect(results[1]).toEqual(results[0]); expect(results[2]).toEqual(results[0]); } ); it("reports actual matching IDs and respects paused automatic camera following", async () => { const target = ports(); expect( await executePresentationCommand( presentationCommandSchema.parse(commands[2]), new AbortController().signal, target ) ).toEqual({ highlighted_ids: ["D1"], missing_ids: ["missing"], fitted: false }); expect(target.scene.highlight).toHaveBeenCalledWith(data, true, false); }); it("rejects empty targets and cancelled actions before any scene mutation", async () => { const target = ports(); vi.mocked(target.controller.queryPresentationFeatures).mockResolvedValueOnce({ type: "FeatureCollection", features: [] }); await expect( executePresentationCommand( presentationCommandSchema.parse(commands[0]), new AbortController().signal, target ) ).rejects.toThrow("FEATURE_NOT_FOUND"); const controller = new AbortController(); controller.abort(); await expect( executePresentationCommand( presentationCommandSchema.parse(commands[3]), controller.signal, target ) ).rejects.toThrow(); expect(target.scene.highlight).not.toHaveBeenCalled(); expect(target.scene.clearHighlights).not.toHaveBeenCalled(); }); });