From 0ec74b15b24d1e963ccf46dded3409b47121b94a Mon Sep 17 00:00:00 2001 From: Huarch Date: Mon, 2 Nov 2026 18:37:55 +0800 Subject: [PATCH] fix: restore investigation and appointment task loading Server responses now include source.replay_context, including null for ordinary tasks. The strict frontend source schema rejected the new field and blocked the shared task snapshot and detail loading path. Validate replay metadata explicitly and cover investigation, appointment, and scheduled task contracts plus browser loading. Six acceptance regressions failed before the fix; all 877 unit tests and the focused browser loading test passed after it. Lint and type checking passed. The separate approval browser case remains blocked by its reconnect-toast assertion. --- .../api/operational-task-client.test.ts | 29 +++++++ .../workbench/api/operational-task-client.ts | 7 +- tests/browser/operational-task-loading.e2e.ts | 77 +++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 tests/browser/operational-task-loading.e2e.ts 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" }); +});