From 000a1e04c54b4c78f063f359a74815ed018374cf Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Thu, 21 May 2026 22:17:11 +0000 Subject: [PATCH] feat: add retry semantics tests with requestIdToNodeKey reverse map --- test/reactive/workflow.test.ts | 213 +++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) diff --git a/test/reactive/workflow.test.ts b/test/reactive/workflow.test.ts index f02ab08..a7fc159 100644 --- a/test/reactive/workflow.test.ts +++ b/test/reactive/workflow.test.ts @@ -528,6 +528,219 @@ describe("WorkflowReactiveRoot", () => { }); }); + describe("retry semantics", () => { + it("nodeKeyToRequestId overwrites previous requestId — projection tracks latest attempt", () => { + const graph = makeSimpleGraph(); + const root = new WorkflowReactiveRoot(graph); + + root.setRequestId("a", "req-1"); + expect(root.nodeKeyToRequestId.get("a")).toBe("req-1"); + + root.setRequestId("a", "req-2"); + expect(root.nodeKeyToRequestId.get("a")).toBe("req-2"); + expect(root.requestIdToNodeKey.get("req-2")).toBe("a"); + + root.dispose(); + }); + + it("retry sequence: error then requested then responded → status is completed", () => { + const graph = makeSimpleGraph(); + const root = new WorkflowReactiveRoot(graph); + + root.setRequestId("a", "req-1"); + root.append({ + type: "call.requested", + requestId: "req-1", + operationId: "a", + input: null, + timestamp: "2026-01-01T00:00:00Z", + }); + root.append({ + type: "call.error", + requestId: "req-1", + error: { code: "ERR", message: "failed" }, + timestamp: "2026-01-01T00:00:01Z", + }); + expect(root.getStatus("a")).toBe("failed"); + + root.setRequestId("a", "req-2"); + root.append({ + type: "call.requested", + requestId: "req-2", + operationId: "a", + input: null, + timestamp: "2026-01-01T00:00:02Z", + }); + expect(root.getStatus("a")).toBe("running"); + + root.append({ + type: "call.responded", + requestId: "req-2", + output: "ok", + timestamp: "2026-01-01T00:00:03Z", + }); + expect(root.getStatus("a")).toBe("completed"); + + root.dispose(); + }); + + it("getResult reflects latest attempt after retry", () => { + const graph = makeSimpleGraph(); + const root = new WorkflowReactiveRoot(graph); + + root.setRequestId("a", "req-1"); + root.append({ + type: "call.requested", + requestId: "req-1", + operationId: "a", + input: null, + timestamp: "2026-01-01T00:00:00Z", + }); + root.append({ + type: "call.error", + requestId: "req-1", + error: { code: "ERR", message: "failed" }, + timestamp: "2026-01-01T00:00:01Z", + }); + + root.setRequestId("a", "req-2"); + root.append({ + type: "call.requested", + requestId: "req-2", + operationId: "a", + input: null, + timestamp: "2026-01-01T00:00:02Z", + }); + root.append({ + type: "call.responded", + requestId: "req-2", + output: { retry: true }, + timestamp: "2026-01-01T00:00:03Z", + }); + + const result = root.getResult("a"); + expect(result).toBeDefined(); + expect(result!.status).toBe("completed"); + expect(result!.output).toEqual({ retry: true }); + + root.dispose(); + }); + + it("event log preserves full history across retries", () => { + const graph = makeSimpleGraph(); + const root = new WorkflowReactiveRoot(graph); + + root.setRequestId("a", "req-1"); + root.append({ + type: "call.requested", + requestId: "req-1", + operationId: "a", + input: null, + timestamp: "2026-01-01T00:00:00Z", + }); + root.append({ + type: "call.error", + requestId: "req-1", + error: { code: "ERR", message: "failed" }, + timestamp: "2026-01-01T00:00:01Z", + }); + + root.setRequestId("a", "req-2"); + root.append({ + type: "call.requested", + requestId: "req-2", + operationId: "a", + input: null, + timestamp: "2026-01-01T00:00:02Z", + }); + root.append({ + type: "call.responded", + requestId: "req-2", + output: "ok", + timestamp: "2026-01-01T00:00:03Z", + }); + + const events = root.getEvents("a"); + expect(events.length).toBe(4); + expect(events[0]!.type).toBe("call.requested"); + expect(events[0]!.requestId).toBe("req-1"); + expect(events[1]!.type).toBe("call.error"); + expect(events[1]!.requestId).toBe("req-1"); + expect(events[2]!.type).toBe("call.requested"); + expect(events[2]!.requestId).toBe("req-2"); + expect(events[3]!.type).toBe("call.responded"); + expect(events[3]!.requestId).toBe("req-2"); + + root.dispose(); + }); + + it("retry clears failed status so downstream preconditions can be met", () => { + const graph = makeSimpleGraph(); + const root = new WorkflowReactiveRoot(graph); + + root.setRequestId("a", "req-1"); + root.setRequestId("b", "req-b"); + root.append({ + type: "call.requested", + requestId: "req-1", + operationId: "a", + input: null, + timestamp: "2026-01-01T00:00:00Z", + }); + root.append({ + type: "call.error", + requestId: "req-1", + error: { code: "ERR", message: "failed" }, + timestamp: "2026-01-01T00:00:01Z", + }); + expect(root.blockedByFailure.get("b")!.value).toBe(true); + + root.setRequestId("a", "req-2"); + root.append({ + type: "call.requested", + requestId: "req-2", + operationId: "a", + input: null, + timestamp: "2026-01-01T00:00:02Z", + }); + root.append({ + type: "call.responded", + requestId: "req-2", + output: "ok", + timestamp: "2026-01-01T00:00:03Z", + }); + expect(root.getStatus("a")).toBe("completed"); + expect(root.blockedByFailure.get("b")!.value).toBe(false); + + root.dispose(); + }); + + it("requestIdToNodeKey preserves all requestIds including previous attempts", () => { + const graph = makeSimpleGraph(); + const root = new WorkflowReactiveRoot(graph); + + root.setRequestId("a", "req-1"); + root.setRequestId("a", "req-2"); + + expect(root.requestIdToNodeKey.get("req-1")).toBe("a"); + expect(root.requestIdToNodeKey.get("req-2")).toBe("a"); + + root.dispose(); + }); + + it("dispose clears requestIdToNodeKey", () => { + const graph = makeSimpleGraph(); + const root = new WorkflowReactiveRoot(graph); + + root.setRequestId("a", "req-1"); + root.setRequestId("a", "req-2"); + expect(root.requestIdToNodeKey.size).toBe(2); + + root.dispose(); + expect(root.requestIdToNodeKey.size).toBe(0); + }); + }); + describe("abort cascade", () => { it("failed node causes downstream dependents to abort (continue-running default)", () => { const graph = makeSimpleGraph();