import { afterEach, describe, expect, it } from "bun:test"; import { bindOperationalTaskCredential, humanTaskEvidenceCommandSchema, OperationalTaskClient, proposedTaskActionSchema, releaseOperationalTaskCredential, } from "../../src/product/operational-task-client.js"; const originalFetch = globalThis.fetch; const taskId = "50000000-0000-4000-8000-000000000001"; const runId = "55000000-0000-4000-8000-000000000001"; const executionRequestId = "60000000-0000-4000-8000-000000000001"; const run = { run_id: runId, task_id: taskId, generation: 1, execution_request_id: executionRequestId, status: "running", revision: 2, agent_session_id: "session-1", lease_expires_at: "2026-09-23T01:01:00Z", heartbeat_at: "2026-09-23T01:00:00Z", started_at: "2026-09-23T01:00:00Z", finished_at: null, failure_code: null, failure_detail: null, dispatch_attempts: 0, next_dispatch_at: "2026-09-23T01:00:00Z", created_at: "2026-09-23T01:00:00Z", updated_at: "2026-09-23T01:00:00Z", }; const task = { task_id: taskId, task_type: "event_investigation", workflow: "water_quality_complaint", lifecycle_status: "open", active_run: run, status: "running", revision: 2, execution_request_id: executionRequestId, title: "水质投诉调查", summary: "调查投诉", }; afterEach(() => { globalThis.fetch = originalFetch; releaseOperationalTaskCredential("runtime-1"); }); describe("OperationalTaskClient", () => { it("requests a selected report while retaining the current task snapshot", async () => { let requestedUrl = ""; globalThis.fetch = (async (input: Parameters[0]) => { requestedUrl = String(input); return Response.json({ task: { ...task, execution_generation: 2, latest_plan: null, latest_report: { revision: 8, execution_generation: 2 }, evidence: [], actions: [], permissions: {} }, approved_data_scopes: [], selected_report: { revision: 3, execution_generation: 1, report: { title: "上一轮报告" } } }); }) as unknown as typeof fetch; bindOperationalTaskCredential("runtime-1", { authorization: "Bearer transient-secret", projectId: "project-1" }); const client = new OperationalTaskClient("http://server.test"); const context = await client.getAgentContext("runtime-1", taskId, 3); expect(new URL(requestedUrl).searchParams.get("report_revision")).toBe("3"); expect(context.selected_report?.execution_generation).toBe(1); expect(context.task.execution_generation).toBe(2); }); it("matches proposed action payloads to the generated Server contract", () => { const common = { request_id: "60000000-0000-4000-8000-000000000001", kind: "create_work_order" as const, reason: "需要现场采样", evidence_refs: [], node_id: "field_sampling", }; expect(proposedTaskActionSchema.safeParse({ ...common, payload: { title: "现场采样", description: "采集二次供水出水和末端水样", work_order_type: "water_quality_sampling", priority: "urgent", location: "联调演示小区", }, }).success).toBe(true); expect(proposedTaskActionSchema.safeParse({ ...common, payload: { title: "现场采样", summary: "字段名不属于后端合同", priority: "high", }, }).success).toBe(false); expect(proposedTaskActionSchema.safeParse({ ...common, kind: "issue_command", payload: { title: "现场采样", description: "错误动作类型不得复用工单载荷", work_order_type: "water_quality_sampling", }, }).success).toBe(false); }); it("accepts only explicit human input on the collaboration evidence path", () => { const evidence = { request_id: "60000000-0000-4000-8000-000000000001", evidence: { evidence_id: "human-confirmation-1", kind: "human_input", summary: "现场人员确认已完成采样。", source_label: "调度员补充", observed_at: "2026-09-23T01:00:00Z", }, }; expect(humanTaskEvidenceCommandSchema.safeParse(evidence).success).toBe(true); expect(humanTaskEvidenceCommandSchema.safeParse({ ...evidence, evidence: { ...evidence.evidence, kind: "system_data" }, }).success).toBe(false); }); it("uses an in-memory credential and converts plans to the server contract", async () => { let request: Request | undefined; globalThis.fetch = (async (input, init) => { request = new Request(input, init); if (request.url.endsWith(`/runs/${runId}/claim`)) return Response.json(run); return Response.json({ task, plan_revision: 1, report_revision: null, }); }) as typeof fetch; bindOperationalTaskCredential("runtime-1", { authorization: "Bearer transient-secret", projectId: "project-1", }); const client = new OperationalTaskClient("http://server.test"); await client.claimTask("runtime-1", taskId, runId, { requestId: crypto.randomUUID(), expectedRevision: 1, sourceSessionId: "session-1", }); await client.publishArtifacts("runtime-1", taskId, { requestId: executionRequestId, sourceSessionId: "session-1", executionPlan: { title: "调查", reason: "更新", nodes: [{ id: "READ", title: "读取", owner: "agent", status: "completed", detail: "完成", evidence: ["evidence-1"] }], edges: [], }, }); expect(request?.headers.get("authorization")).toBe("Bearer transient-secret"); expect(request?.headers.get("x-project-id")).toBe("project-1"); const body = await request?.json() as Record; expect(body).toMatchObject({ request_id: "60000000-0000-4000-8000-000000000001", source_session_id: "session-1", run_id: runId, plan: { schema: "execution-plan@1", nodes: [{ evidence_refs: ["evidence-1"] }], }, }); expect(JSON.stringify(body)).not.toContain("transient-secret"); }); it("fails closed without a current product credential", async () => { const client = new OperationalTaskClient("http://server.test"); expect(client.getAgentContext("runtime-1", "50000000-0000-4000-8000-000000000001")) .rejects.toThrow("current product credential"); }); it("preserves the Server problem code for lease authority failures", async () => { let call = 0; globalThis.fetch = (async (_input, _init) => { call += 1; if (call === 1) return Response.json(run); return Response.json({ type: "https://tjwater.example/problems/lease-expired", title: "Conflict", status: 409, detail: "Agent lease has expired", code: "LEASE_EXPIRED", }, { status: 409 }); }) as typeof fetch; bindOperationalTaskCredential("runtime-1", { authorization: "Bearer transient-secret", projectId: "project-1", }); const client = new OperationalTaskClient("http://server.test"); await client.claimTask("runtime-1", taskId, runId, { requestId: crypto.randomUUID(), expectedRevision: 1, sourceSessionId: "session-1", }); expect(client.publishArtifacts( "runtime-1", taskId, { requestId: "60000000-0000-4000-8000-000000000001", sourceSessionId: "session-1", report: { title: "失败", stage: "final", outcome: "concluded", stop_reason: null, data_gaps: [], current_conclusion: "租约已失效", conclusion_boundary: "未继续写回", evidence_refs: [], recommendations: [], pending_items: [], }, }, )).rejects.toMatchObject({ status: 409, code: "LEASE_EXPIRED", message: "Agent lease has expired", }); }); it("returns the persisted bounded data snapshot with its evidence id", async () => { const urls: string[] = []; globalThis.fetch = (async (input) => { const url = String(input); urls.push(url); return urls.length === 1 ? Response.json({ evidence_id: "scada_readings:sha256:abc", kind: "system_data", summary: "scada_readings 数据快照", source_label: "scada_readings", source_ref: "scada_readings:sha256:abc", observed_at: "2026-09-23T01:00:00Z", }) : Response.json({ source: "scada_readings", source_ref: "scada_readings:sha256:abc", queried_at: "2026-09-23T01:00:00Z", origin: "system_data", data: { items: [{ value: 0.42 }] }, truncated: false, }); }) as typeof fetch; bindOperationalTaskCredential("runtime-1", { authorization: "Bearer transient-secret", projectId: "project-1", }); const client = new OperationalTaskClient("http://server.test"); const result = await client.queryData("runtime-1", "50000000-0000-4000-8000-000000000001", { source: "scada_readings", device_ids: ["device-1"], start: "2026-09-23T00:00:00Z", end: "2026-09-23T01:00:00Z", }); expect(result).toMatchObject({ evidence: { evidence_id: "scada_readings:sha256:abc" }, snapshot: { origin: "system_data", truncated: false, data: { items: [{ value: 0.42 }] } }, }); expect(urls[1]).toContain("/evidence/scada_readings%3Asha256%3Aabc/snapshot"); }); it("keeps the current task revision for heartbeats after an action-only response", async () => { const requestBodies: Record[] = []; let call = 0; globalThis.fetch = (async (_input, init) => { if (init?.body) requestBodies.push(JSON.parse(String(init.body)) as Record); call += 1; if (call === 1) return Response.json(run); if (call === 2) return Response.json({ action_id: "70000000-0000-4000-8000-000000000001", approval_status: "pending", execution_status: "not_submitted", }); return Response.json({ ...run, revision: 3 }); }) as typeof fetch; bindOperationalTaskCredential("runtime-1", { authorization: "Bearer transient-secret", projectId: "project-1", }); const client = new OperationalTaskClient("http://server.test"); await client.claimTask("runtime-1", taskId, runId, { requestId: "60000000-0000-4000-8000-000000000002", expectedRevision: 1, sourceSessionId: "session-1", }); await client.proposeAction("runtime-1", "50000000-0000-4000-8000-000000000001", { request_id: "60000000-0000-4000-8000-000000000003", kind: "create_work_order", reason: "需要现场采样", payload: { title: "现场采样", description: "采集水样", work_order_type: "water_quality_sampling", priority: "urgent", }, evidence_refs: [], }); await client.heartbeatTask( "runtime-1", taskId, runId, "session-1", ); expect(requestBodies[2]).toMatchObject({ expected_revision: 2, source_session_id: "session-1", }); }); it("serializes artifact publication and heartbeat revision updates", async () => { const requestBodies: Record[] = []; let releaseArtifact: (() => void) | undefined; const artifactGate = new Promise((resolve) => { releaseArtifact = resolve; }); let call = 0; globalThis.fetch = (async (_input, init) => { if (init?.body) requestBodies.push(JSON.parse(String(init.body)) as Record); call += 1; if (call === 1) return Response.json(run); if (call === 2) { await artifactGate; return Response.json({ task: { ...task, active_run: { ...run, revision: 3 } }, plan_revision: 1, report_revision: null, }); } return Response.json({ ...run, revision: 4 }); }) as typeof fetch; bindOperationalTaskCredential("runtime-1", { authorization: "Bearer transient-secret", projectId: "project-1", }); const client = new OperationalTaskClient("http://server.test"); await client.claimTask("runtime-1", taskId, runId, { requestId: executionRequestId, expectedRevision: 1, sourceSessionId: "session-1", }); const publication = client.publishArtifacts("runtime-1", taskId, { requestId: "60000000-0000-4000-8000-000000000002", sourceSessionId: "session-1", executionPlan: { title: "调查计划", reason: "核实告警", nodes: [], edges: [], }, }); const heartbeat = client.heartbeatTask("runtime-1", taskId, runId, "session-1"); releaseArtifact?.(); await Promise.all([publication, heartbeat]); expect(requestBodies[2]).toMatchObject({ expected_revision: 3 }); }); });