feat: add retry semantics tests with requestIdToNodeKey reverse map
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user