import { describe, it, expect } from "vitest"; import { h, createHostRoot } from "@alkdev/ujsx"; import { Operation, Sequential, Parallel, Conditional, Map } from "../../src/component/index.js"; import { ReactiveHostConfig } from "../../src/host/reactive.js"; import type { WorkflowNode, ReactiveContext } from "../../src/host/reactive.js"; function renderTemplate(template: ReturnType): ReactiveContext { const root = createHostRoot(ReactiveHostConfig, null); root.render(template); return root.ctx; } describe("ReactiveHostConfig", () => { describe("createRootContext", () => { it("creates fresh ReactiveContext with empty maps", () => { const ctx = ReactiveHostConfig.createRootContext(null); expect(ctx.nodes.size).toBe(0); expect(ctx.statusSignals.size).toBe(0); expect(ctx.parentMap.size).toBe(0); expect(ctx.siblingMap.size).toBe(0); }); it("accepts options with registry", () => { const registry = { resolve: (name: string) => name }; const ctx = ReactiveHostConfig.createRootContext(null, { registry }); expect(ctx.operationRegistry).toBe(registry); }); }); describe("createInstance for operation", () => { it("creates WorkflowNode with correct key and type", () => { const template = h(Sequential, {}, h(Operation, { name: "classify" }), ); const ctx = renderTemplate(template); const node = ctx.nodes.get("classify"); expect(node).toBeDefined(); expect(node!.key).toBe("classify"); expect(node!.type).toBe("operation"); expect(node!.operationId).toBe("classify"); }); it("creates WorkflowNode with idle status signal", () => { const template = h(Sequential, {}, h(Operation, { name: "classify" }), ); const ctx = renderTemplate(template); const node = ctx.nodes.get("classify")!; expect(node.status.value).toBe("idle"); }); it("registers node in context maps", () => { const template = h(Sequential, {}, h(Operation, { name: "classify" }), ); const ctx = renderTemplate(template); expect(ctx.statusSignals.has("classify")).toBe(true); expect(ctx.preconditions.has("classify")).toBe(true); expect(ctx.blockedByFailure.has("classify")).toBe(true); }); it("creates output signal for operation nodes", () => { const template = h(Sequential, {}, h(Operation, { name: "classify" }), ); const ctx = renderTemplate(template); const node = ctx.nodes.get("classify")!; expect(node.output).toBeDefined(); expect(node.output!.value).toBeUndefined(); }); }); describe("createInstance for structural containers", () => { it("creates WorkflowNode tracking children for Sequential", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); const seqNode = ctx.nodes.get("__sequential_0"); expect(seqNode).toBeDefined(); expect(seqNode!.type).toBe("sequential"); expect(seqNode!.children.length).toBe(2); }); it("creates WorkflowNode for Parallel with children", () => { const template = h(Parallel, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); const parNode = ctx.nodes.get("__parallel_0"); expect(parNode).toBeDefined(); expect(parNode!.type).toBe("parallel"); expect(parNode!.children.length).toBe(2); }); it("creates WorkflowNode for Conditional with children", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Conditional, { test: "A" }, h(Operation, { name: "B" }), ), ); const ctx = renderTemplate(template); const condNode = ctx.nodes.get("__conditional_1"); expect(condNode).toBeDefined(); expect(condNode!.type).toBe("conditional"); }); it("structural containers do not have operationId", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), ); const ctx = renderTemplate(template); const seqNode = ctx.nodes.get("__sequential_0")!; expect(seqNode.operationId).toBeUndefined(); }); }); describe("signal initial states", () => { it("root operation has preconditions met (no predecessors)", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); const nodeA = ctx.nodes.get("A")!; expect(nodeA.preconditions.value).toBe(true); }); it("second operation in sequential has unmet preconditions", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); const nodeB = ctx.nodes.get("B")!; expect(nodeB.preconditions.value).toBe(false); }); it("parallel children have preconditions met (no inter-child deps)", () => { const template = h(Parallel, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); expect(ctx.nodes.get("A")!.preconditions.value).toBe(true); expect(ctx.nodes.get("B")!.preconditions.value).toBe(true); }); it("all initial blockedByFailure are false", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); expect(ctx.nodes.get("A")!.blockedByFailure.value).toBe(false); expect(ctx.nodes.get("B")!.blockedByFailure.value).toBe(false); }); }); describe("precondition transition on predecessor completion", () => { it("sequential: completing predecessor updates dependent preconditions", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); const nodeA = ctx.nodes.get("A")!; const nodeB = ctx.nodes.get("B")!; expect(nodeB.preconditions.value).toBe(false); nodeA.status.value = "completed"; expect(nodeB.preconditions.value).toBe(true); }); it("sequential: skipped predecessor satisfies preconditions", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); const nodeA = ctx.nodes.get("A")!; nodeA.status.value = "skipped"; expect(ctx.nodes.get("B")!.preconditions.value).toBe(true); }); it("sequential chain: A→B→C, completing A then B", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), h(Operation, { name: "C" }), ); const ctx = renderTemplate(template); expect(ctx.nodes.get("B")!.preconditions.value).toBe(false); expect(ctx.nodes.get("C")!.preconditions.value).toBe(false); ctx.nodes.get("A")!.status.value = "completed"; expect(ctx.nodes.get("B")!.preconditions.value).toBe(true); expect(ctx.nodes.get("C")!.preconditions.value).toBe(false); ctx.nodes.get("B")!.status.value = "completed"; expect(ctx.nodes.get("C")!.preconditions.value).toBe(true); }); it("parallel children preconditions independent of each other", () => { const template = h(Sequential, {}, h(Operation, { name: "pre" }), h(Parallel, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ), ); const ctx = renderTemplate(template); expect(ctx.nodes.get("A")!.preconditions.value).toBe(false); expect(ctx.nodes.get("B")!.preconditions.value).toBe(false); ctx.nodes.get("pre")!.status.value = "completed"; expect(ctx.nodes.get("A")!.preconditions.value).toBe(true); expect(ctx.nodes.get("B")!.preconditions.value).toBe(true); }); }); describe("failure propagation", () => { it("sequential: failed predecessor causes blockedByFailure to be true", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); ctx.nodes.get("A")!.status.value = "failed"; expect(ctx.nodes.get("B")!.blockedByFailure.value).toBe(true); }); it("sequential: aborted predecessor causes blockedByFailure to be true", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); ctx.nodes.get("A")!.status.value = "aborted"; expect(ctx.nodes.get("B")!.blockedByFailure.value).toBe(true); }); it("parallel siblings are independent for failure propagation", () => { const template = h(Parallel, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); ctx.nodes.get("A")!.status.value = "failed"; expect(ctx.nodes.get("B")!.blockedByFailure.value).toBe(false); }); it("failed predecessor does not satisfy preconditions", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); ctx.nodes.get("A")!.status.value = "failed"; expect(ctx.nodes.get("B")!.preconditions.value).toBe(false); }); }); describe("appendChild", () => { it("appends children to parent's children array", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); const seqNode = ctx.nodes.get("__sequential_0")!; expect(seqNode.children.length).toBe(2); expect(seqNode.children[0]!.key).toBe("A"); expect(seqNode.children[1]!.key).toBe("B"); }); it("does not duplicate children", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), ); const ctx = renderTemplate(template); const seqNode = ctx.nodes.get("__sequential_0")!; const childNode = ctx.nodes.get("A")!; ReactiveHostConfig.appendChild(seqNode, childNode, ctx); expect(seqNode.children.length).toBe(1); }); }); describe("removeChild", () => { it("removes child from parent's children array", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); const seqNode = ctx.nodes.get("__sequential_0")!; const childB = ctx.nodes.get("B")!; ReactiveHostConfig.removeChild(seqNode, childB, ctx); expect(seqNode.children.length).toBe(1); expect(seqNode.children[0]!.key).toBe("A"); }); it("preconditions auto-reevaluate after removeChild", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), h(Operation, { name: "C" }), ); const ctx = renderTemplate(template); const seqNode = ctx.nodes.get("__sequential_0")!; const childB = ctx.nodes.get("B")!; ctx.nodes.get("A")!.status.value = "completed"; expect(childB.preconditions.value).toBe(true); ReactiveHostConfig.removeChild(seqNode, childB, ctx); const childC = ctx.nodes.get("C")!; expect(childC.preconditions.value).toBe(true); }); }); describe("parentMap and siblingMap", () => { it("registers parent-child relationships in parentMap", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); expect(ctx.parentMap.get("A")).toBe("__sequential_0"); expect(ctx.parentMap.get("B")).toBe("__sequential_0"); }); it("registers siblings in siblingMap", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); const siblings = ctx.siblingMap.get("__sequential_0"); expect(siblings).toEqual(["A", "B"]); }); it("nested structures register correct parent", () => { const template = h(Sequential, {}, h(Operation, { name: "pre" }), h(Parallel, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ), ); const ctx = renderTemplate(template); expect(ctx.parentMap.get("pre")).toBe("__sequential_0"); expect(ctx.parentMap.get("A")).toBe("__parallel_1"); expect(ctx.parentMap.get("B")).toBe("__parallel_1"); const seqSiblings = ctx.siblingMap.get("__sequential_0"); expect(seqSiblings).toEqual(["pre", "__parallel_1"]); const parSiblings = ctx.siblingMap.get("__parallel_1"); expect(parSiblings).toEqual(["A", "B"]); }); }); describe("Conditional as error boundary", () => { it("conditional node is created with correct type", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Conditional, { test: "A" }, h(Operation, { name: "B" }), ), ); const ctx = renderTemplate(template); const condNode = ctx.nodes.get("__conditional_1"); expect(condNode).toBeDefined(); expect(condNode!.type).toBe("conditional"); }); it("conditional child has preconditions met (no inter-child ordering)", () => { const template = h(Conditional, { test: "A" }, h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); expect(ctx.nodes.get("B")!.preconditions.value).toBe(true); }); }); describe("full template rendering", () => { it("renders a complete pipeline template", () => { const template = h(Sequential, {}, h(Operation, { name: "architect" }), h(Operation, { name: "reviewer" }), h(Parallel, {}, h(Operation, { name: "decomposer" }), h(Operation, { name: "specialist" }), ), h(Operation, { name: "synthesizer" }), ); const ctx = renderTemplate(template); expect(ctx.nodes.get("architect")).toBeDefined(); expect(ctx.nodes.get("reviewer")).toBeDefined(); expect(ctx.nodes.get("decomposer")).toBeDefined(); expect(ctx.nodes.get("specialist")).toBeDefined(); expect(ctx.nodes.get("synthesizer")).toBeDefined(); expect(ctx.nodes.get("architect")!.preconditions.value).toBe(true); expect(ctx.nodes.get("reviewer")!.preconditions.value).toBe(false); ctx.nodes.get("architect")!.status.value = "completed"; expect(ctx.nodes.get("reviewer")!.preconditions.value).toBe(true); }); it("deeply nested template registers all nodes", () => { const template = h(Sequential, {}, h(Parallel, {}, h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ), h(Operation, { name: "C" }), ), ); const ctx = renderTemplate(template); expect(ctx.nodes.has("A")).toBe(true); expect(ctx.nodes.has("B")).toBe(true); expect(ctx.nodes.has("C")).toBe(true); }); }); describe("shared signal references", () => { it("WorkflowNode.status and statusSignals point to same signal", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), ); const ctx = renderTemplate(template); const node = ctx.nodes.get("A")!; const statusFromMap = ctx.statusSignals.get("A")!; expect(node.status).toBe(statusFromMap); statusFromMap.value = "running"; expect(node.status.value).toBe("running"); node.status.value = "completed"; expect(statusFromMap.value).toBe("completed"); }); }); });