import { signal, computed, effect } from "@preact/signals-core"; import { DirectedGraph } from "graphology"; const IDLE: NodeStatus = "idle"; const WAITING: NodeStatus = "waiting"; const READY: NodeStatus = "ready"; const RUNNING: NodeStatus = "running"; const COMPLETED: NodeStatus = "completed"; const FAILED: NodeStatus = "failed"; const ABORTED: NodeStatus = "aborted"; const SKIPPED: NodeStatus = "skipped"; type NodeStatus = | "idle" | "waiting" | "ready" | "running" | "completed" | "failed" | "aborted" | "skipped"; export interface TestReactiveRoot { statusMap: Map>>; preconditions: Map>>; blockedByFailure: Map>>; dispose: () => void; } export function createTestReactiveRoot(graph: DirectedGraph): TestReactiveRoot { const statusMap = new Map>>(); const preconditions = new Map>>(); const blockedByFailure = new Map>>(); const disposers: (() => void)[] = []; for (const node of graph.nodes()) { const predecessors = graph.inNeighbors(node); const status = signal(IDLE); const preconditionsComputed = computed(() => { return predecessors.every((pred: string) => { const predStatus = statusMap.get(pred); return ( predStatus !== undefined && (predStatus.value === COMPLETED || predStatus.value === SKIPPED) ); }); }); const blockedByFailureComputed = computed(() => { return predecessors.some((pred: string) => { const predStatus = statusMap.get(pred); return ( predStatus !== undefined && (predStatus.value === FAILED || predStatus.value === ABORTED) ); }); }); const blockerDisposer = effect(() => { if (blockedByFailureComputed.value) { if (status.value === IDLE || status.value === WAITING) { status.value = ABORTED; } } }); disposers.push(blockerDisposer); statusMap.set(node, status); preconditions.set(node, preconditionsComputed); blockedByFailure.set(node, blockedByFailureComputed); } return { statusMap, preconditions, blockedByFailure, dispose: () => { for (const disposer of disposers) { disposer(); } statusMap.clear(); preconditions.clear(); blockedByFailure.clear(); }, }; } export function assertStatus( root: TestReactiveRoot, nodeId: string, expected: NodeStatus, ): void { const status = root.statusMap.get(nodeId); if (!status) { throw new Error(`Node ${nodeId} not found in statusMap`); } if (status.value !== expected) { throw new Error( `Node ${nodeId} status: expected ${expected}, got ${status.value}`, ); } } export function assertPreconditions( root: TestReactiveRoot, nodeId: string, expected: boolean, ): void { const preconditions = root.preconditions.get(nodeId); if (!preconditions) { throw new Error(`Node ${nodeId} not found in preconditions`); } if (preconditions.value !== expected) { throw new Error( `Node ${nodeId} preconditions: expected ${expected}, got ${preconditions.value}`, ); } } export function assertBlockedByFailure( root: TestReactiveRoot, nodeId: string, expected: boolean, ): void { const blocked = root.blockedByFailure.get(nodeId); if (!blocked) { throw new Error(`Node ${nodeId} not found in blockedByFailure`); } if (blocked.value !== expected) { throw new Error( `Node ${nodeId} blockedByFailure: expected ${expected}, got ${blocked.value}`, ); } } export type { NodeStatus };