133 lines
9.2 KiB
TypeScript
133 lines
9.2 KiB
TypeScript
import type { Page } from "@playwright/test";
|
|
import type { OperationalTask, OperationalTaskNotification } from "../../../src/features/workbench/api/operational-task-client";
|
|
import { mockAgentApi } from "./mock-agent-api";
|
|
import { mockWorkbenchContext } from "./mock-workbench-context";
|
|
|
|
const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
|
|
const now = "2026-10-27T08:00:00Z";
|
|
|
|
export function investigationFixture(index: number, title: string): OperationalTask {
|
|
const taskId = `92000000-0000-4000-8000-${String(index).padStart(12, "0")}`;
|
|
const runId = `93000000-0000-4000-8000-${String(index).padStart(12, "0")}`;
|
|
const requestId = `94000000-0000-4000-8000-${String(index).padStart(12, "0")}`;
|
|
return {
|
|
project_id: projectId, task_id: taskId, task_type: "event_investigation", workflow: "burst_report",
|
|
lifecycle_status: "open", status: "running", priority: "normal", revision: 1, execution_generation: 1,
|
|
execution_request_id: requestId, title, summary: "仅供浏览器测试的合成调查",
|
|
source: { kind: "operator", external_id: null, external_revision: null, event_kind: "burst_report" },
|
|
trigger: { kind: "manual", occurred_at: now, detail: "synthetic" }, scheduled_for: null,
|
|
agent_session_id: "investigation-dev-test", agent_lease_expires_at: now, current_plan_revision: null,
|
|
current_report_revision: null, created_at: now, updated_at: now,
|
|
active_run: { run_id: runId, task_id: taskId, generation: 1, execution_request_id: requestId,
|
|
status: "running", revision: 1, agent_session_id: "investigation-dev-test", lease_expires_at: now,
|
|
heartbeat_at: now, started_at: now, finished_at: null, failure_code: null, failure_detail: null,
|
|
dispatch_attempts: 1, next_dispatch_at: now, created_at: now, updated_at: now },
|
|
latest_plan: null, latest_report: null,
|
|
latest_progress: { revision: 1, run_id: runId, execution_generation: 1, published_at: now,
|
|
source_session_id: "investigation-dev-test", progress: { summary: "正在核对候选管段", diagram: {
|
|
title: "合成调查", mermaid: 'flowchart TD\nA["核查监测"]', node_refs: [{ node_id: "A", label: "核查监测",
|
|
status: "running", evidence_refs: [], action_refs: [], map_targets: [{ source_id: "scada", feature_id: "P1" }] }]
|
|
} } },
|
|
evidence: [], actions: [], access_requests: [], work_order_snapshot: 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 }
|
|
};
|
|
}
|
|
|
|
export type SavedPresentationEvent = { task_id: string; event_seq: number; run_id: string; execution_generation: number; occurred_at: string; command: unknown };
|
|
export async function mockInvestigationPresentation(page: Page, tasks: OperationalTask[], savedEvents: SavedPresentationEvent[] = [], notifications: OperationalTaskNotification[] = []) {
|
|
await mockWorkbenchContext(page);
|
|
await mockAgentApi(page);
|
|
const mutations: string[] = [];
|
|
page.on("request", (request) => {
|
|
if (request.url().startsWith("https://tjwater-api.invalid/") && ["POST", "PATCH", "DELETE", "PUT"].includes(request.method()))
|
|
mutations.push(`${request.method()} ${new URL(request.url()).pathname}`);
|
|
});
|
|
await page.route("**/runtime-config.js", (route) => route.fulfill({ contentType: "application/javascript",
|
|
body: `globalThis.__TJWATER_CONFIG__=${JSON.stringify({
|
|
TJWATER_AUTH_MODE: "disabled", TJWATER_SERVER_API_BASE_URL: "https://tjwater-api.invalid",
|
|
TJWATER_AGENT_URL: "http://127.0.0.1:8787", TJWATER_ENABLE_DEV_PANEL: "true",
|
|
TJWATER_MAP_URL: "https://workbench-map.invalid/geoserver", TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
|
|
TJWATER_MAPBOX_ACCESS_TOKEN: "", TJWATER_TIANDITU_TOKEN: "", TJWATER_ENABLE_MSW: "false"
|
|
})};`
|
|
}));
|
|
await page.route("https://tjwater-api.invalid/api/v1/scada-devices**", (route) => route.fulfill({ json: {
|
|
items: [{ device_id: "P1", device_type: "pressure", node_id: null, link_id: null, api_query_id: null,
|
|
measurement_unit: "m", transmission_mode: "test", transmission_frequency: "15min", reliability: 1,
|
|
x: null, y: null, longitude: 121.48, latitude: 31.10 }], total: 1, limit: 1000, offset: 0
|
|
} }));
|
|
await page.route("https://tjwater-api.invalid/api/v1/workbench/sync-snapshot", (route) => {
|
|
return route.fulfill({ json: { schema: "workbench-sync-snapshot@1", event_cursor: 10, generated_at: now,
|
|
runtime: { availability: "not_configured", source: "none", scenario_availability: "not_configured",
|
|
artifact_availability: "not_configured", schedule_availability: "not_configured", work_order_availability: "not_configured",
|
|
alert_availability: "not_configured", generated_at: now, conditions: [], alerts: [], scenarios: [], latest_analysis: null, work_orders: [] },
|
|
tasks: tasks.map((task) => Object.fromEntries(Object.entries(task).filter(([key]) =>
|
|
!["latest_plan", "latest_report", "latest_progress", "evidence", "actions", "access_requests", "work_order_snapshot", "permissions", "questions"].includes(key)))),
|
|
notifications } });
|
|
});
|
|
await page.route("https://tjwater-api.invalid/api/v1/operational-tasks/**", (route) => {
|
|
const task = tasks.find((item) => route.request().url().includes(item.task_id));
|
|
if (!task) return route.fulfill({ status: 404 });
|
|
if (route.request().url().endsWith("/presentation-cursor")) return route.fulfill({ json: {
|
|
task_id: task.task_id, execution_generation: task.execution_generation, active_run: task.active_run ? {
|
|
run_id: task.active_run.run_id, generation: task.execution_generation, status: "running"
|
|
} : null, event_cursor: sequence
|
|
} });
|
|
if (route.request().url().includes("/presentation-events")) {
|
|
const url = new URL(route.request().url());
|
|
const through = Number(url.searchParams.get("through") ?? sequence);
|
|
const after = Number(url.searchParams.get("after") ?? 0);
|
|
const limit = Number(url.searchParams.get("limit") ?? 100);
|
|
const events = savedEvents.filter((item) => item.task_id === task.task_id && item.event_seq > after && item.event_seq <= through).sort((a, b) => a.event_seq - b.event_seq);
|
|
return route.fulfill({ json: { task_id: task.task_id, event_cursor: through,
|
|
items: events.slice(0, limit).map(({ task_id: _, ...item }) => item),
|
|
next_after: events.length > limit ? events[limit - 1].event_seq : null
|
|
} });
|
|
}
|
|
if (route.request().url().includes("/reports")) return route.fulfill({ json: { items: [], next_offset: null } });
|
|
return route.fulfill({ json: task });
|
|
});
|
|
await page.addInitScript(() => {
|
|
const fetch = window.fetch.bind(window);
|
|
const streams = new Set<ReadableStreamDefaultController<Uint8Array>>();
|
|
Object.assign(window, { emitInvestigationTestEvent: (event: unknown) => {
|
|
const data = new TextEncoder().encode(`event: workbench_event\ndata: ${JSON.stringify(event)}\n\n`);
|
|
for (const stream of streams) stream.enqueue(data);
|
|
} });
|
|
window.fetch = async (input, init) => {
|
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
if (!url.includes("/api/v1/workbench/events")) return fetch(input, init);
|
|
let current: ReadableStreamDefaultController<Uint8Array>;
|
|
const body = new ReadableStream<Uint8Array>({ start(controller) { current = controller; streams.add(controller); }, cancel() { streams.delete(current); } });
|
|
init?.signal?.addEventListener("abort", () => { if (streams.delete(current)) current.close(); }, { once: true });
|
|
return new Response(body, { headers: { "Content-Type": "text/event-stream" } });
|
|
};
|
|
});
|
|
let sequence = Math.max(10, ...savedEvents.map((item) => item.event_seq));
|
|
return {
|
|
mutations,
|
|
emit: async (task: OperationalTask, command: unknown) => {
|
|
savedEvents.push({ task_id: task.task_id, run_id: task.active_run!.run_id, event_seq: sequence + 1, execution_generation: task.execution_generation, occurred_at: now, command });
|
|
return page.evaluate((event) => {
|
|
(window as unknown as { emitInvestigationTestEvent: (event: unknown) => void }).emitInvestigationTestEvent(event);
|
|
}, { schema: "workbench-event@1", event_seq: ++sequence,
|
|
event_id: `95000000-0000-4000-8000-${String(sequence).padStart(12, "0")}`, project_id: projectId,
|
|
topic: "operational.presentation.requested", aggregate_type: "operational_task", aggregate_id: task.task_id,
|
|
aggregate_revision: 1, occurred_at: now, metadata: {
|
|
task_id: task.task_id, run_id: task.active_run!.run_id, execution_generation: task.execution_generation, command
|
|
} });
|
|
}
|
|
};
|
|
}
|
|
|
|
export async function selectInvestigation(page: Page, title: string) {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
|
const panel = page.getByRole("region", { name: "工况任务", exact: true });
|
|
const expand = panel.getByRole("button", { name: "展开工况任务" });
|
|
if (await expand.isVisible()) await expand.click();
|
|
await panel.getByRole("button", { name: /调查任务/ }).click();
|
|
await panel.getByText(title, { exact: true }).first().click();
|
|
}
|