// Self-contained workflow smoke test. No DB needed — exercises the pure // state machine + dependency engine against the user's 5-stage pipeline: // // Inputfile (NONE) → Internal Approval (FORMAL, gate) // ├─ Approved // │ └─ Delivery // └─ Declined (rework edge → Inputfile) // // Run: npx tsx scripts/test-workflow-logic.ts // Each step prints PASS / FAIL; non-zero exit on any failure. import { canTransition } from "@/lib/pipeline/stage-machine"; import { canStageStart, getStageIdsToUnblock, getBranchStagesToSkipOnApprove, getBranchOutcomeOnReject, } from "@/lib/pipeline/dependency-engine"; type StageStatus = | "BLOCKED" | "NOT_STARTED" | "IN_PROGRESS" | "IN_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "DELIVERED" | "SKIPPED"; interface Stage { id: string; status: StageStatus; order: number; template: { id: string; slug: string; isCriticalGate: boolean; isOptional: boolean; dependsOn: { prerequisiteId: string }[]; }; stageDefinition: { id: string; branchKind?: "NONE" | "APPROVED" | "DECLINED" | null; dependsOn: { prerequisiteId: string }[]; } | null; } // ─── Pipeline builder ───────────────────────────────────────────────────── // Mirrors the user's pipeline graph from the screenshot. Forward edges // are encoded in stageDefinition.dependsOn (V2 dynamic). Rework edges // (Declined → Inputfile, Delivery → Inputfile) aren't exercised by the // engine — those live in PipelineStageRework and route through // stage-transition-service.applyRework, which we test separately. function buildPipeline(opts: { tagBranches?: boolean } = {}): Stage[] { const stage = ( slug: string, order: number, deps: string[], extra: Partial> & { branchKind?: "NONE" | "APPROVED" | "DECLINED"; } = {} ): Stage => ({ id: `ds_${slug}`, status: "BLOCKED", order, template: { id: `tpl_${slug}`, slug, isCriticalGate: extra.isCriticalGate ?? false, isOptional: extra.isOptional ?? false, dependsOn: [], }, stageDefinition: { id: `def_${slug}`, branchKind: extra.branchKind ?? "NONE", dependsOn: deps.map((d) => ({ prerequisiteId: `def_${d}` })), }, }); return [ stage("inputfile", 1, []), stage("internal-approval", 2, ["inputfile"], { isCriticalGate: true }), stage("approved", 3, ["internal-approval"], { branchKind: opts.tagBranches ? "APPROVED" : "NONE", }), stage("declined", 3, ["internal-approval"], { branchKind: opts.tagBranches ? "DECLINED" : "NONE", }), stage("delivery", 4, ["approved"]), ]; } // ─── Test harness ───────────────────────────────────────────────────────── let passed = 0; let failed = 0; const failures: string[] = []; function assert(name: string, cond: boolean, detail?: string) { if (cond) { passed++; console.log(` ✓ ${name}`); } else { failed++; failures.push(`${name}${detail ? ` — ${detail}` : ""}`); console.log(` ✗ ${name}${detail ? ` — ${detail}` : ""}`); } } function section(name: string) { console.log(`\n— ${name} —`); } // Simulate the auto-advance + canStageStart flow that stage-service // would execute end-to-end on the user's pipeline. function simulateTerminalTransition( stages: Stage[], stageId: string, newStatus: StageStatus ): Stage[] { const updated = stages.map((s) => s.id === stageId ? { ...s, status: newStatus } : s ); const just = updated.find((s) => s.id === stageId)!; const unblockIds = getStageIdsToUnblock(just, updated); return updated.map((s) => unblockIds.includes(s.id) ? { ...s, status: "IN_PROGRESS" } : s ); } // ─── Run ────────────────────────────────────────────────────────────────── section("State machine — NONE stage Mark Complete shortcut"); { const r = canTransition("IN_PROGRESS", "APPROVED"); assert( "IN_PROGRESS → APPROVED is allowed (Mark Complete on NONE stage)", r.allowed, r.reason ); } { const r = canTransition("IN_PROGRESS", "IN_REVIEW"); assert( "IN_PROGRESS → IN_REVIEW still allowed (FORMAL/SIMPLE flow)", r.allowed ); } { const r = canTransition("IN_REVIEW", "APPROVED"); assert("IN_REVIEW → APPROVED allowed (FORMAL approve)", r.allowed); } { const r = canTransition("IN_REVIEW", "CHANGES_REQUESTED"); assert("IN_REVIEW → CHANGES_REQUESTED allowed (request changes)", r.allowed); } { const r = canTransition("BLOCKED", "APPROVED"); assert( "BLOCKED → APPROVED rejected (must finish prereq first)", !r.allowed ); } section("Dependency engine — order-based deps drive the cascade"); let p = buildPipeline(); // Initial state: producer hasn't done anything yet. Inputfile is the only // stage without prereqs. { const inputfile = p.find((s) => s.template.slug === "inputfile")!; assert( "Inputfile has no prerequisites (it's stage 1)", inputfile.stageDefinition!.dependsOn.length === 0 ); // Producer manually starts it → IN_PROGRESS p = p.map((s) => s.id === inputfile.id ? { ...s, status: "IN_PROGRESS" as StageStatus } : s ); assert("Producer clicks Start Work → Inputfile IN_PROGRESS", true); } // Mark Complete on Inputfile → IN_PROGRESS → APPROVED → triggers cascade section("Mark Complete on Inputfile (the failing case from the screenshot)"); { const inputfile = p.find((s) => s.template.slug === "inputfile")!; const r = canTransition(inputfile.status, "APPROVED"); assert( "Mark Complete (IN_PROGRESS → APPROVED) is now allowed", r.allowed, r.reason ); p = simulateTerminalTransition(p, inputfile.id, "APPROVED"); const ia = p.find((s) => s.template.slug === "internal-approval")!; assert( "Internal Approval auto-advances to IN_PROGRESS", ia.status === "IN_PROGRESS", `actual: ${ia.status}` ); // Downstream-of-downstream stays BLOCKED at this point. const approved = p.find((s) => s.template.slug === "approved")!; const declined = p.find((s) => s.template.slug === "declined")!; assert( "Approved stays BLOCKED (waiting on Internal Approval)", approved.status === "BLOCKED" ); assert( "Declined stays BLOCKED (waiting on Internal Approval)", declined.status === "BLOCKED" ); } section("Approve & advance on Internal Approval (FORMAL stage)"); { const ia = p.find((s) => s.template.slug === "internal-approval")!; // FORMAL stage goes IN_PROGRESS → IN_REVIEW (Submit) → APPROVED. We // simulate the final APPROVED step the StageReviewPanel triggers. p = p.map((s) => s.id === ia.id ? { ...s, status: "IN_REVIEW" as StageStatus } : s ); assert( "IN_PROGRESS → IN_REVIEW (Submit for review) allowed", canTransition("IN_PROGRESS", "IN_REVIEW").allowed ); // Now the reviewer clicks Approve & advance. p = simulateTerminalTransition(p, ia.id, "APPROVED"); // Both downstream branches (Approved + Declined) depend on Internal // Approval — both should unblock. const approved = p.find((s) => s.template.slug === "approved")!; const declined = p.find((s) => s.template.slug === "declined")!; assert( "Approved auto-advances to IN_PROGRESS", approved.status === "IN_PROGRESS", `actual: ${approved.status}` ); assert( "Declined ALSO auto-advances to IN_PROGRESS (shared prereq)", declined.status === "IN_PROGRESS", `actual: ${declined.status}` ); // Delivery should still be BLOCKED — it depends on Approved, not on // Internal Approval. const delivery = p.find((s) => s.template.slug === "delivery")!; assert( "Delivery still BLOCKED (depends on Approved, not Internal Approval)", delivery.status === "BLOCKED", `actual: ${delivery.status}` ); } section("Producer marks Approved complete → Delivery cascades"); { const approved = p.find((s) => s.template.slug === "approved")!; p = simulateTerminalTransition(p, approved.id, "APPROVED"); const delivery = p.find((s) => s.template.slug === "delivery")!; assert( "Delivery auto-advances to IN_PROGRESS once Approved completes", delivery.status === "IN_PROGRESS", `actual: ${delivery.status}` ); } section("Edge: canStageStart guards work via the engine"); { // Reset to fresh pipeline so we can ask "can Delivery start right now?" // before any prereqs are met. const fresh = buildPipeline(); const delivery = fresh.find((s) => s.template.slug === "delivery")!; const r = canStageStart(delivery, fresh); assert( "Fresh Delivery: cannot start — Approved is not terminal", !r.allowed, r.reason ); } { const fresh = buildPipeline(); const inputfile = fresh.find((s) => s.template.slug === "inputfile")!; const r = canStageStart(inputfile, fresh); assert( "Fresh Inputfile: can start — no prerequisites", r.allowed, r.reason ); } section("Edge: order-fallback deps when no V2 edges are drawn"); { // Build a pipeline with NO stageDefinition deps to verify the // order-fallback path. (Producers building a quick pipeline who don't // draw forward arrows still get a sensible left-to-right cascade.) const fallback = buildPipeline().map((s) => ({ ...s, stageDefinition: { id: s.stageDefinition!.id, dependsOn: [] }, })); const inputfile = fallback.find((s) => s.template.slug === "inputfile")!; const ia = fallback.find((s) => s.template.slug === "internal-approval")!; inputfile.status = "IN_PROGRESS"; const unblock = getStageIdsToUnblock( { ...inputfile, status: "APPROVED" }, fallback.map((s) => (s.id === inputfile.id ? { ...s, status: "APPROVED" as StageStatus } : s)) ); assert( "No V2 deps: order fallback unblocks the next-order stage", unblock.includes(ia.id), `unblock = ${unblock.join(", ")}` ); } // ─── Generic shape coverage ────────────────────────────────────────────── // The engine must work for any pipeline shape, not just the user's // canonical 5-stage flow. These build minimal pipelines for each shape // pattern and verify the cascade behaves correctly. function pipelineFromGraph( edges: Array<[from: string, to: string]>, nodeOrder?: Record ): Stage[] { const nodes = new Set(); edges.forEach(([a, b]) => { nodes.add(a); nodes.add(b); }); const byName = new Map(); Array.from(nodes).forEach((n, i) => byName.set(n, nodeOrder?.[n] ?? i + 1)); return Array.from(nodes).map((n) => ({ id: `ds_${n}`, status: "BLOCKED" as StageStatus, order: byName.get(n)!, template: { id: `tpl_${n}`, slug: n, isCriticalGate: false, isOptional: false, dependsOn: [], }, stageDefinition: { id: `def_${n}`, dependsOn: edges .filter(([, to]) => to === n) .map(([from]) => ({ prerequisiteId: `def_${from}` })), }, })); } function complete(stages: Stage[], slug: string): Stage[] { const stage = stages.find((s) => s.template.slug === slug); if (!stage) throw new Error(`No stage ${slug}`); return simulateTerminalTransition(stages, stage.id, "APPROVED"); } function statusOf(stages: Stage[], slug: string): StageStatus { return stages.find((s) => s.template.slug === slug)!.status; } section("Shape: linear A → B → C → D"); { let g = pipelineFromGraph([ ["A", "B"], ["B", "C"], ["C", "D"], ]); // Producer manually starts the head. g = g.map((s) => s.template.slug === "A" ? { ...s, status: "IN_PROGRESS" as StageStatus } : s ); g = complete(g, "A"); assert("Linear: A done → B opens", statusOf(g, "B") === "IN_PROGRESS"); assert("Linear: C stays BLOCKED", statusOf(g, "C") === "BLOCKED"); assert("Linear: D stays BLOCKED", statusOf(g, "D") === "BLOCKED"); g = complete(g, "B"); assert("Linear: B done → C opens", statusOf(g, "C") === "IN_PROGRESS"); assert("Linear: D still BLOCKED", statusOf(g, "D") === "BLOCKED"); g = complete(g, "C"); assert("Linear: C done → D opens", statusOf(g, "D") === "IN_PROGRESS"); } section("Shape: fan-out A → {B, C, D}"); { let g = pipelineFromGraph([ ["A", "B"], ["A", "C"], ["A", "D"], ]); g = g.map((s) => s.template.slug === "A" ? { ...s, status: "IN_PROGRESS" as StageStatus } : s ); g = complete(g, "A"); assert("Fan-out: B opens", statusOf(g, "B") === "IN_PROGRESS"); assert("Fan-out: C opens", statusOf(g, "C") === "IN_PROGRESS"); assert("Fan-out: D opens", statusOf(g, "D") === "IN_PROGRESS"); } section("Shape: fan-in {A, B, C} → D"); { let g = pipelineFromGraph( [ ["A", "D"], ["B", "D"], ["C", "D"], ], { A: 1, B: 1, C: 1, D: 2 } ); g = g.map((s) => ["A", "B", "C"].includes(s.template.slug) ? { ...s, status: "IN_PROGRESS" as StageStatus } : s ); g = complete(g, "A"); assert("Fan-in: D BLOCKED with only A done", statusOf(g, "D") === "BLOCKED"); g = complete(g, "B"); assert("Fan-in: D BLOCKED with A+B done", statusOf(g, "D") === "BLOCKED"); g = complete(g, "C"); assert( "Fan-in: D opens only after all three predecessors done", statusOf(g, "D") === "IN_PROGRESS" ); } section("Shape: diamond A → {B, C} → D"); { let g = pipelineFromGraph( [ ["A", "B"], ["A", "C"], ["B", "D"], ["C", "D"], ], { A: 1, B: 2, C: 2, D: 3 } ); g = g.map((s) => s.template.slug === "A" ? { ...s, status: "IN_PROGRESS" as StageStatus } : s ); g = complete(g, "A"); assert("Diamond: A done → B opens", statusOf(g, "B") === "IN_PROGRESS"); assert("Diamond: A done → C opens", statusOf(g, "C") === "IN_PROGRESS"); assert("Diamond: D still BLOCKED", statusOf(g, "D") === "BLOCKED"); g = complete(g, "B"); assert( "Diamond: only B done → D still BLOCKED (also waits for C)", statusOf(g, "D") === "BLOCKED" ); g = complete(g, "C"); assert( "Diamond: B+C done → D opens", statusOf(g, "D") === "IN_PROGRESS" ); } section("Shape: long chain (10 stages)"); { const slugs = ["s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "s10"]; const edges: Array<[string, string]> = []; for (let i = 0; i < slugs.length - 1; i++) { edges.push([slugs[i], slugs[i + 1]]); } let g = pipelineFromGraph(edges); g = g.map((s) => s.template.slug === "s1" ? { ...s, status: "IN_PROGRESS" as StageStatus } : s ); for (let i = 0; i < slugs.length - 1; i++) { g = complete(g, slugs[i]); assert( `Chain step ${i + 1}: ${slugs[i + 1]} opens after ${slugs[i]} completes`, statusOf(g, slugs[i + 1]) === "IN_PROGRESS" ); } } // ─── Rework reset planner (mirrors applyRework's where-clause logic) ───── // applyRework resets stages where `order > targetOrder && order <= // currentOrder && status NOT IN (SKIPPED, NOT_STARTED)` to NOT_STARTED. // This pure function reproduces that math so we can verify it from // arbitrary pipeline + rework configurations without spinning up a DB. function planReworkReset( stages: Stage[], sourceSlug: string, targetSlug: string ): string[] { const source = stages.find((s) => s.template.slug === sourceSlug)!; const target = stages.find((s) => s.template.slug === targetSlug)!; if (target.order >= source.order) { throw new Error("Target must have lower order than source for rework"); } return stages .filter( (s) => s.order > target.order && s.order <= source.order && s.status !== "SKIPPED" && s.status !== "NOT_STARTED" ) .map((s) => s.template.slug) .sort(); } section("Rework: linear pipeline, rework from far end back to head"); { let g = pipelineFromGraph([ ["A", "B"], ["B", "C"], ["C", "D"], ]); // Walk the whole pipeline to APPROVED, then simulate rework D → A. g = g.map((s) => s.template.slug === "A" ? { ...s, status: "IN_PROGRESS" as StageStatus } : s ); g = complete(g, "A"); g = complete(g, "B"); g = complete(g, "C"); g = complete(g, "D"); assert("Rework setup: all four stages APPROVED", statusOf(g, "A") === "APPROVED" && statusOf(g, "D") === "APPROVED"); const toReset = planReworkReset(g, "D", "A"); assert( "Rework D → A: B, C, D reset; A untouched", toReset.join(",") === "B,C,D", `actual: ${toReset.join(",")}` ); } section("Rework: SKIPPED stages stay SKIPPED across rework"); { let g = pipelineFromGraph([ ["A", "B"], ["B", "C"], ["C", "D"], ]); // Simulate: A done, B skipped, C done, D done. g = g.map((s) => { if (s.template.slug === "A") return { ...s, status: "APPROVED" as StageStatus }; if (s.template.slug === "B") return { ...s, status: "SKIPPED" as StageStatus }; if (s.template.slug === "C") return { ...s, status: "APPROVED" as StageStatus }; if (s.template.slug === "D") return { ...s, status: "APPROVED" as StageStatus }; return s; }); const toReset = planReworkReset(g, "D", "A"); assert( "Rework D → A skipping B: only C, D reset; B stays SKIPPED", toReset.join(",") === "C,D", `actual: ${toReset.join(",")}` ); } section("Rework: short rework D → C only resets D"); { let g = pipelineFromGraph([ ["A", "B"], ["B", "C"], ["C", "D"], ]); // All four APPROVED, then short rework from D back to C. g = g.map((s) => ({ ...s, status: "APPROVED" as StageStatus })); const toReset = planReworkReset(g, "D", "C"); assert( "Rework D → C: only D resets (single-step back)", toReset.join(",") === "D", `actual: ${toReset.join(",")}` ); } section("Rework: user's pipeline — Declined → Inputfile resets everything between"); { // Match the user's screenshot exactly. const stages: Stage[] = [ { name: "inputfile", order: 1 }, { name: "internal-approval", order: 2 }, { name: "approved", order: 3 }, { name: "declined", order: 3 }, { name: "delivery", order: 4 }, ].map(({ name, order }) => ({ id: `ds_${name}`, status: "APPROVED" as StageStatus, // pretend they've all completed order, template: { id: `tpl_${name}`, slug: name, isCriticalGate: name === "internal-approval", isOptional: false, dependsOn: [], }, stageDefinition: { id: `def_${name}`, dependsOn: [] }, })); const toReset = planReworkReset(stages, "declined", "inputfile"); assert( "User pipeline rework Declined → Inputfile: internal-approval + approved + declined all reset; inputfile spared", !toReset.includes("inputfile") && toReset.includes("internal-approval") && toReset.includes("declined"), `actual: ${toReset.join(",")}` ); } // ─── Outcome-branch routing ─────────────────────────────────────────────── // User's pipeline with branch tags: Approved+Declined are downstream of // the FORMAL parent, but only one fires per decision. The other goes // SKIPPED (visually grayed out, blocked from use). section("Branch routing — APPROVE picks the Approved path, skips Declined"); { // Setup: parent (internal-approval) just hit APPROVED. let pipeline = buildPipeline({ tagBranches: true }); pipeline = pipeline.map((s) => s.template.slug === "inputfile" ? { ...s, status: "APPROVED" as StageStatus } : s ); pipeline = pipeline.map((s) => s.template.slug === "internal-approval" ? { ...s, status: "APPROVED" as StageStatus } : s ); const parent = pipeline.find((s) => s.template.slug === "internal-approval")!; const unblock = getStageIdsToUnblock(parent, pipeline); const skip = getBranchStagesToSkipOnApprove(parent, pipeline); // Only Approved branch opens; Declined branch goes SKIPPED. const unblockSlugs = pipeline .filter((s) => unblock.includes(s.id)) .map((s) => s.template.slug) .sort(); const skipSlugs = pipeline .filter((s) => skip.includes(s.id)) .map((s) => s.template.slug) .sort(); assert( "APPROVED path: only 'approved' branch unblocks", unblockSlugs.length === 1 && unblockSlugs[0] === "approved", `unblock=[${unblockSlugs.join(",")}]` ); assert( "APPROVED path: 'declined' branch is auto-SKIPPED", skipSlugs.length === 1 && skipSlugs[0] === "declined", `skip=[${skipSlugs.join(",")}]` ); // Sanity: without branch tags, BOTH children would unblock (old behaviour). const untaggedPipeline = buildPipeline({ tagBranches: false }).map((s) => s.template.slug === "inputfile" || s.template.slug === "internal-approval" ? { ...s, status: "APPROVED" as StageStatus } : s ); const untaggedParent = untaggedPipeline.find( (s) => s.template.slug === "internal-approval" )!; const untaggedUnblock = getStageIdsToUnblock( untaggedParent, untaggedPipeline ).map((id) => untaggedPipeline.find((s) => s.id === id)!.template.slug); assert( "Untagged pipeline opens BOTH children (regression check)", untaggedUnblock.includes("approved") && untaggedUnblock.includes("declined"), `untaggedUnblock=[${untaggedUnblock.sort().join(",")}]` ); } section("Branch routing — REJECT stamps Declined, skips Approved"); { let pipeline = buildPipeline({ tagBranches: true }); pipeline = pipeline.map((s) => s.template.slug === "inputfile" ? { ...s, status: "APPROVED" as StageStatus } : s ); // Parent is in flight — Request Changes about to fire. pipeline = pipeline.map((s) => s.template.slug === "internal-approval" ? { ...s, status: "IN_REVIEW" as StageStatus } : s ); const parent = pipeline.find((s) => s.template.slug === "internal-approval")!; const { idsToStampApproved, idsToSkip } = getBranchOutcomeOnReject( parent, pipeline ); const stampedSlugs = pipeline .filter((s) => idsToStampApproved.includes(s.id)) .map((s) => s.template.slug) .sort(); const skippedSlugs = pipeline .filter((s) => idsToSkip.includes(s.id)) .map((s) => s.template.slug) .sort(); assert( "REJECT path: 'declined' branch is auto-stamped APPROVED", stampedSlugs.length === 1 && stampedSlugs[0] === "declined", `stamped=[${stampedSlugs.join(",")}]` ); assert( "REJECT path: 'approved' branch is auto-SKIPPED", skippedSlugs.length === 1 && skippedSlugs[0] === "approved", `skipped=[${skippedSlugs.join(",")}]` ); } section("Branch routing — generalizes to N-way splits"); { // 3-way split: parent → { approved-a, approved-b, declined }. // Two APPROVED branches both open on approve; one DECLINED is skipped. const stages: Stage[] = [ { id: "ds_parent", status: "APPROVED", order: 1, template: { id: "tpl_parent", slug: "parent", isCriticalGate: true, isOptional: false, dependsOn: [], }, stageDefinition: { id: "def_parent", branchKind: "NONE", dependsOn: [], }, }, ...["approved-a", "approved-b"].map((slug) => ({ id: `ds_${slug}`, status: "BLOCKED", order: 2, template: { id: `tpl_${slug}`, slug, isCriticalGate: false, isOptional: false, dependsOn: [], }, stageDefinition: { id: `def_${slug}`, branchKind: "APPROVED", dependsOn: [{ prerequisiteId: "def_parent" }], }, })), { id: "ds_declined", status: "BLOCKED", order: 2, template: { id: "tpl_declined", slug: "declined", isCriticalGate: false, isOptional: false, dependsOn: [], }, stageDefinition: { id: "def_declined", branchKind: "DECLINED", dependsOn: [{ prerequisiteId: "def_parent" }], }, }, ]; const parent = stages.find((s) => s.template.slug === "parent")!; const unblock = getStageIdsToUnblock(parent, stages) .map((id) => stages.find((s) => s.id === id)!.template.slug) .sort(); const skip = getBranchStagesToSkipOnApprove(parent, stages) .map((id) => stages.find((s) => s.id === id)!.template.slug) .sort(); assert( "3-way: BOTH APPROVED branches open on approve", unblock.length === 2 && unblock.includes("approved-a") && unblock.includes("approved-b"), `unblock=[${unblock.join(",")}]` ); assert( "3-way: DECLINED branch is skipped on approve", skip.length === 1 && skip[0] === "declined", `skip=[${skip.join(",")}]` ); } section("Branch routing — already-skipped branches are idempotent"); { let pipeline = buildPipeline({ tagBranches: true }); pipeline = pipeline.map((s) => s.template.slug === "declined" ? { ...s, status: "SKIPPED" as StageStatus } : s ); pipeline = pipeline.map((s) => s.template.slug === "inputfile" ? { ...s, status: "APPROVED" as StageStatus } : s ); pipeline = pipeline.map((s) => s.template.slug === "internal-approval" ? { ...s, status: "APPROVED" as StageStatus } : s ); const parent = pipeline.find((s) => s.template.slug === "internal-approval")!; const skip = getBranchStagesToSkipOnApprove(parent, pipeline); assert( "Already-SKIPPED branch is not re-targeted", skip.length === 0, `skip=[${skip.join(",")}]` ); } section("State machine — additional shortcuts"); { // Shortcuts added in 46cd8f8 to declutter producer workflow. for (const [from, to, expect] of [ ["IN_PROGRESS", "SKIPPED", true], ["CHANGES_REQUESTED", "APPROVED", true], ["BLOCKED", "IN_PROGRESS", true], // when prereqs are now satisfied ["DELIVERED", "APPROVED", true], // downgrade if mistakenly delivered ["SKIPPED", "IN_PROGRESS", true], // unskip directly ["BLOCKED", "APPROVED", false], // still blocked — can't skip work ["NOT_STARTED", "APPROVED", false], // must start first ] as const) { const r = canTransition(from, to); assert( `${from} → ${to} is ${expect ? "allowed" : "rejected"}`, r.allowed === expect, r.reason ); } } // ─── Summary ────────────────────────────────────────────────────────────── console.log("\n──────────────────────────────────────────────────"); console.log( `Result: ${passed} passed, ${failed} failed (${passed + failed} total)` ); if (failed > 0) { console.log("\nFailures:"); failures.forEach((f) => console.log(` • ${f}`)); process.exit(1); } console.log("✓ all workflow logic assertions passed"); process.exit(0);