diff --git a/src/features/workbench/api/operational-task-client.test.ts b/src/features/workbench/api/operational-task-client.test.ts index ce5a7fe..1a2f7a5 100644 --- a/src/features/workbench/api/operational-task-client.test.ts +++ b/src/features/workbench/api/operational-task-client.test.ts @@ -42,6 +42,35 @@ function task(generation: number | null | undefined = 2) { afterEach(() => vi.unstubAllGlobals()); describe("operational task contract", () => { + describe.each(["event_investigation", "external_work_order", "scheduled_flow"])("%s replay context", (taskType) => { + const replayContext = { replay_id: id, knowledge_cutoff: time, batch_sequence: 1 }; + + it.each([null, replayContext])("loads summaries and details with replay_context %j", (context) => { + const value = task(); + const detail = { ...value, task_type: taskType, source: { ...value.source, replay_context: context } }; + const { + latest_plan: _latestPlan, latest_report: _latestReport, evidence: _evidence, + actions: _actions, permissions: _permissions, ...summary + } = detail; + + expect(parseOperationalTaskSummary(summary).source).toHaveProperty("replay_context", context); + expect(parseOperationalTask(detail).source).toHaveProperty("replay_context", context); + }); + + it.each([ + { ...replayContext, replay_id: "not-a-uuid" }, + { ...replayContext, knowledge_cutoff: "not-a-date" }, + { ...replayContext, batch_sequence: 0 }, + { ...replayContext, batch_sequence: 1.5 }, + { ...replayContext, unexpected: true } + ])("rejects invalid replay metadata %j", (context) => { + const value = task(); + expect(() => parseOperationalTask({ + ...value, task_type: taskType, source: { ...value.source, replay_context: context } + })).toThrow(); + }); + }); + it("loads task runs used to resolve a selected report round", async () => { const run = { run_id: id, diff --git a/src/features/workbench/api/operational-task-client.ts b/src/features/workbench/api/operational-task-client.ts index 070cfbd..c2832df 100644 --- a/src/features/workbench/api/operational-task-client.ts +++ b/src/features/workbench/api/operational-task-client.ts @@ -13,7 +13,12 @@ const taskSourceSchema = z.object({ external_id: z.string().min(1).nullable(), external_revision: z.number().int().positive().nullable(), event_kind: z.string().min(1).nullable(), - deleted_at: workbenchDateTimeSchema.nullish() + deleted_at: workbenchDateTimeSchema.nullish(), + replay_context: z.object({ + replay_id: z.string().uuid(), + knowledge_cutoff: workbenchDateTimeSchema, + batch_sequence: z.number().int().positive() + }).strict().nullish() }).strict(); const taskTriggerSchema = z.object({ kind: z.enum(["schedule", "external_sync", "event", "manual"]), diff --git a/tests/browser/operational-task-loading.e2e.ts b/tests/browser/operational-task-loading.e2e.ts new file mode 100644 index 0000000..ef0b807 --- /dev/null +++ b/tests/browser/operational-task-loading.e2e.ts @@ -0,0 +1,77 @@ +import { expect, test } from "@playwright/test"; +import type { OperationalTask, OperationalTaskSummary } from "../../src/features/workbench/api/operational-task-client"; +import { mockAgentApi } from "./support/mock-agent-api"; +import { mockWorkbenchContext } from "./support/mock-workbench-context"; + +test("调查和预约任务接收 Server 返回的空回放上下文并展示详情", async ({ page }, testInfo) => { + await page.emulateMedia({ reducedMotion: "reduce" }); + const now = new Date().toISOString(); + const id = "72000000-0000-4000-8000-000000000001"; + const appointmentId = "72000000-0000-4000-8000-000000000002"; + const summary: OperationalTaskSummary = { + project_id: "324ff7ee-c2d4-5184-a84b-48fb14a2933e", task_id: id, + task_type: "event_investigation", workflow: "burst_report", + lifecycle_status: "waiting_human", active_run: null, status: "waiting_human", + priority: "urgent", revision: 1, execution_generation: 1, execution_request_id: id, + title: "凤阳爆管调查加载核验", summary: "核对调查任务详情", + source: { kind: "external_system", external_id: id, external_revision: 1, + event_kind: "burst_report", replay_context: null }, + trigger: { kind: "event", occurred_at: now, detail: "核对调查任务详情" }, + scheduled_for: null, agent_session_id: null, agent_lease_expires_at: null, + current_plan_revision: null, current_report_revision: null, created_at: now, updated_at: now + }; + const appointment: OperationalTaskSummary = { + ...summary, task_id: appointmentId, task_type: "external_work_order", workflow: "external-work-order", + lifecycle_status: "open", status: "running", priority: "normal", execution_request_id: appointmentId, + title: "凤阳预约维修加载核验", summary: "核对预约任务详情", scheduled_for: now, + source: { ...summary.source, external_id: `peripheral_sample:${appointmentId}`, event_kind: "general" }, + trigger: { kind: "external_sync", occurred_at: now, detail: "核对预约任务详情" } + }; + const summaries = [summary, appointment]; + const tasks: OperationalTask[] = summaries.map((item) => ({ + ...item, latest_plan: null, latest_report: null, evidence: [], actions: [], + 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 }, + work_order_snapshot: item.task_type === "external_work_order" ? { + work_order_id: appointmentId, revision: 1, title: item.title, summary: item.summary, + status: "open", priority: "normal", assignee: "凤阳维修组", scheduled_at: now, + work_order_type: "general", location: "凤阳核验点", scenario_code: null, + origin: "system_data", updated_at: now + } : null + })); + const 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: [] + }; + await mockWorkbenchContext(page, runtime); + await mockAgentApi(page); + await page.route("https://tjwater-api.invalid/api/v1/workbench/sync-snapshot", (route) => route.fulfill({ json: { + schema: "workbench-sync-snapshot@1", event_cursor: 0, runtime, + tasks: summaries, notifications: [], generated_at: now + } })); + await page.route("https://tjwater-api.invalid/api/v1/operational-tasks/*", (route) => { + const taskId = new URL(route.request().url()).pathname.split("/").at(-1); + const task = tasks.find((item) => item.task_id === taskId); + return route.fulfill({ status: task ? 200 : 404, json: task ?? {} }); + }); + + await page.goto("/"); + await page.getByRole("button", { name: "工况任务", exact: true }).click(); + const panel = page.getByRole("region", { name: "工况任务", exact: true }); + await panel.getByRole("button", { name: "展开工况任务" }).click(); + await panel.getByRole("button", { name: /调查任务/ }).click(); + await panel.getByText(summary.title, { exact: true }).first().click(); + await expect(panel.getByTestId("scheduled-condition-detail-scroll")).toContainText(summary.summary); + await testInfo.attach("investigation-loaded", { body: await panel.screenshot(), contentType: "image/png" }); + + await panel.getByRole("button", { name: /预约任务/ }).click(); + await panel.getByText(appointment.title, { exact: true }).first().click(); + const detail = panel.getByTestId("scheduled-condition-detail-scroll"); + await expect(detail).toContainText("核对预约任务详情"); + await expect(detail).toContainText("凤阳维修组"); + await expect(detail).toContainText("凤阳核验点"); + await testInfo.attach("appointment-loaded", { body: await panel.screenshot(), contentType: "image/png" }); +});