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", () => {
|
describe("abort cascade", () => {
|
||||||
it("failed node causes downstream dependents to abort (continue-running default)", () => {
|
it("failed node causes downstream dependents to abort (continue-running default)", () => {
|
||||||
const graph = makeSimpleGraph();
|
const graph = makeSimpleGraph();
|
||||||
|
|||||||
Reference in New Issue
Block a user