From aa9b12662eeda5e2692ef0040d024c66e474c053 Mon Sep 17 00:00:00 2001 From: Huarch Date: Sun, 8 Nov 2026 03:44:25 +0800 Subject: [PATCH] fix(tasks): accept execution scope in task responses Server responses now require execution_scope, but the strict frontend schema rejected it and prevented investigation and appointment loading. Declare it in the shared response schema and align task fixtures; regression coverage exercises summaries, details and both task categories. --- .../api/operational-task-client.test.ts | 24 +++++++++++++++++++ .../workbench/api/operational-task-client.ts | 1 + .../agent-execution-plan-panel.test.tsx | 1 + .../hooks/use-operational-tasks.test.ts | 1 + .../browser/dynamic-investigation-flow.e2e.ts | 1 + .../browser/operational-task-approval.e2e.ts | 1 + tests/browser/operational-task-loading.e2e.ts | 8 +++---- tests/browser/peripheral-sample-panel.e2e.ts | 1 + .../browser/peripheral-work-order-push.e2e.ts | 1 + .../mock-investigation-presentation.ts | 1 + 10 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/features/workbench/api/operational-task-client.test.ts b/src/features/workbench/api/operational-task-client.test.ts index 1a2f7a5..839cdec 100644 --- a/src/features/workbench/api/operational-task-client.test.ts +++ b/src/features/workbench/api/operational-task-client.test.ts @@ -21,6 +21,7 @@ function task(generation: number | null | undefined = 2) { const snapshot = { revision: 1, source_session_id: "session", published_at: time, published_by: "agent", ...(generation === undefined ? {} : { execution_generation: generation }) }; return { + execution_scope: "production", project_id: id, task_id: id, task_type: "event_investigation", workflow: "water_quality_complaint", lifecycle_status: "waiting_human", active_run: null, status: "waiting_human", priority: "normal", revision: 1, execution_generation: 2, @@ -42,6 +43,28 @@ function task(generation: number | null | undefined = 2) { afterEach(() => vi.unstubAllGlobals()); describe("operational task contract", () => { + it.each(["event_investigation", "external_work_order", "scheduled_flow"])( + "loads %s summaries and details with the Server execution scope", (taskType) => { + const detail = { ...task(), task_type: taskType, execution_scope: "production" }; + const { + latest_plan: _latestPlan, latest_report: _latestReport, evidence: _evidence, + actions: _actions, permissions: _permissions, ...summary + } = detail; + expect(parseOperationalTaskSummary(summary)).toHaveProperty("execution_scope", "production"); + expect(parseOperationalTask(detail)).toHaveProperty("execution_scope", "production"); + } + ); + + it.each([undefined, null, "", 1])("rejects an invalid execution scope %j", (scope) => { + const detail = { ...task(), execution_scope: scope }; + const { + latest_plan: _latestPlan, latest_report: _latestReport, evidence: _evidence, + actions: _actions, permissions: _permissions, ...summary + } = detail; + expect(() => parseOperationalTaskSummary(summary)).toThrow(); + expect(() => parseOperationalTask(detail)).toThrow(); + }); + describe.each(["event_investigation", "external_work_order", "scheduled_flow"])("%s replay context", (taskType) => { const replayContext = { replay_id: id, knowledge_cutoff: time, batch_sequence: 1 }; @@ -170,6 +193,7 @@ describe("operational task contract", () => { it("keeps unknown fields and invalid generations rejected", () => { expect(() => parseOperationalTask(task(0))).toThrow(); const value = task(); + expect(() => parseOperationalTask({ ...value, unexpected: true })).toThrow(); expect(() => parseOperationalTask({ ...value, latest_plan: { ...value.latest_plan, unexpected: true } })).toThrow(); expect(() => parseOperationalTask({ ...value, latest_report: { ...value.latest_report, unexpected: true } })).toThrow(); expect(() => parseOperationalTask({ ...value, latest_report: { ...value.latest_report, execution_generation: -1 } })).toThrow(); diff --git a/src/features/workbench/api/operational-task-client.ts b/src/features/workbench/api/operational-task-client.ts index 30fb432..fc12eb0 100644 --- a/src/features/workbench/api/operational-task-client.ts +++ b/src/features/workbench/api/operational-task-client.ts @@ -50,6 +50,7 @@ const operationalTaskRunPageSchema = z.object({ next_cursor: z.string().min(1).nullable() }).strict(); const taskSummarySchema = z.object({ + execution_scope: z.string().min(1), handling_context: z.record(z.unknown()).optional(), project_id: z.string().uuid(), task_id: z.string().uuid(), diff --git a/src/features/workbench/components/agent-execution-plan-panel.test.tsx b/src/features/workbench/components/agent-execution-plan-panel.test.tsx index 41772ea..de56996 100644 --- a/src/features/workbench/components/agent-execution-plan-panel.test.tsx +++ b/src/features/workbench/components/agent-execution-plan-panel.test.tsx @@ -62,6 +62,7 @@ describe("execution step cards", () => { }); const taskBase = { + execution_scope: "production", project_id: "11111111-1111-4111-8111-111111111111", task_id: "22222222-2222-4222-8222-222222222222", task_type: "event_investigation", diff --git a/src/features/workbench/hooks/use-operational-tasks.test.ts b/src/features/workbench/hooks/use-operational-tasks.test.ts index e874df0..86994fa 100644 --- a/src/features/workbench/hooks/use-operational-tasks.test.ts +++ b/src/features/workbench/hooks/use-operational-tasks.test.ts @@ -9,6 +9,7 @@ import { function task(taskType: OperationalTask["task_type"]): OperationalTask { return { + execution_scope: "production", project_id: "11111111-1111-4111-8111-111111111111", task_id: "22222222-2222-4222-8222-222222222222", task_type: taskType, diff --git a/tests/browser/dynamic-investigation-flow.e2e.ts b/tests/browser/dynamic-investigation-flow.e2e.ts index 0daa49c..76f8f4f 100644 --- a/tests/browser/dynamic-investigation-flow.e2e.ts +++ b/tests/browser/dynamic-investigation-flow.e2e.ts @@ -61,6 +61,7 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常 ] }; let task: OperationalTask = { + execution_scope: "production", project_id: projectId, task_id: taskId, task_type: "event_investigation", workflow: "water_quality_complaint", lifecycle_status: "waiting_human", active_run: null, status: "waiting_human", priority: "urgent", revision: 2, execution_generation: 1, execution_request_id: "85000000-0000-4000-8000-000000000001", diff --git a/tests/browser/operational-task-approval.e2e.ts b/tests/browser/operational-task-approval.e2e.ts index 2152d44..db8484d 100644 --- a/tests/browser/operational-task-approval.e2e.ts +++ b/tests/browser/operational-task-approval.e2e.ts @@ -66,6 +66,7 @@ test("审批动作展示中文参数和证据,viewer 不可审批,冲突后 execution_detail: null }; let task: OperationalTask = { + execution_scope: "production", project_id: projectId, task_id: taskId, task_type: "event_investigation", diff --git a/tests/browser/operational-task-loading.e2e.ts b/tests/browser/operational-task-loading.e2e.ts index ef0b807..809fa6d 100644 --- a/tests/browser/operational-task-loading.e2e.ts +++ b/tests/browser/operational-task-loading.e2e.ts @@ -3,13 +3,13 @@ import type { OperationalTask, OperationalTaskSummary } from "../../src/features import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; -test("调查和预约任务接收 Server 返回的空回放上下文并展示详情", async ({ page }, testInfo) => { +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, + execution_scope: "production", 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, @@ -65,7 +65,7 @@ test("调查和预约任务接收 Server 返回的空回放上下文并展示详 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 testInfo.attach("investigation-loaded", { body: await panel.screenshot({ path: testInfo.outputPath("investigation-loaded.png") }), contentType: "image/png" }); await panel.getByRole("button", { name: /预约任务/ }).click(); await panel.getByText(appointment.title, { exact: true }).first().click(); @@ -73,5 +73,5 @@ test("调查和预约任务接收 Server 返回的空回放上下文并展示详 await expect(detail).toContainText("核对预约任务详情"); await expect(detail).toContainText("凤阳维修组"); await expect(detail).toContainText("凤阳核验点"); - await testInfo.attach("appointment-loaded", { body: await panel.screenshot(), contentType: "image/png" }); + await testInfo.attach("appointment-loaded", { body: await panel.screenshot({ path: testInfo.outputPath("appointment-loaded.png") }), contentType: "image/png" }); }); diff --git a/tests/browser/peripheral-sample-panel.e2e.ts b/tests/browser/peripheral-sample-panel.e2e.ts index 7e2dfe2..59df4d1 100644 --- a/tests/browser/peripheral-sample-panel.e2e.ts +++ b/tests/browser/peripheral-sample-panel.e2e.ts @@ -1267,6 +1267,7 @@ test.describe("外围系统", () => { if (request.method() === "POST" && url.pathname.endsWith("/operational-tasks")) { const input = request.postDataJSON() as Record; operationalTask = { + execution_scope: "production", project_id: projectId, task_id: "70000000-0000-4000-8000-000000000001", task_type: "event_investigation", workflow: "water_quality_complaint", lifecycle_status: "waiting_human", active_run: null, diff --git a/tests/browser/peripheral-work-order-push.e2e.ts b/tests/browser/peripheral-work-order-push.e2e.ts index 920bf12..fe7b4d1 100644 --- a/tests/browser/peripheral-work-order-push.e2e.ts +++ b/tests/browser/peripheral-work-order-push.e2e.ts @@ -17,6 +17,7 @@ async function expectPushedOrder(page: Page, failFirstDetail = false) { conditions: [], alerts: [], scenarios: [], latest_analysis: null, work_orders: [] }; const task = { + execution_scope: "production", project_id: TEST_PROJECT_ID, task_id: taskId, task_type: "external_work_order", workflow: "external-work-order", lifecycle_status: "open", active_run: null, status: "queued", priority: "normal", revision: 2, execution_generation: 1, diff --git a/tests/browser/support/mock-investigation-presentation.ts b/tests/browser/support/mock-investigation-presentation.ts index a6b8613..b144c22 100644 --- a/tests/browser/support/mock-investigation-presentation.ts +++ b/tests/browser/support/mock-investigation-presentation.ts @@ -11,6 +11,7 @@ export function investigationFixture(index: number, title: string): OperationalT const runId = `93000000-0000-4000-8000-${String(index).padStart(12, "0")}`; const requestId = `94000000-0000-4000-8000-${String(index).padStart(12, "0")}`; return { + execution_scope: "production", 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: "仅供浏览器测试的合成调查",