import { describe, it, expect } from 'vitest'; import { buildMermaid } from '@beehexa/hexasync-template-report-render'; import type { ComponentFlow } from '@beehexa/hexasync-template-compose'; import { withSource } from './withSource'; /** * An IF step's `data` is its next — in every kind of flow (reported 2026-08-06). * * *"The `next` property is a string so it points to the next step key, but the node it draws is not connected, * which is incorrect. The resolve next / if logic should follow * `core.api/hexasync.steps/Execution/NextStepBuilder.cs`."* * * The chart drew `Check Exists → Check Condition` and left the other six nodes floating. Cause: IF/SWITCH * routing via `data` was gated on `newKind`, and this is an OLD-kind pusher — so its three `displayType: IF` * steps fell through to `step.next`, which an IF step does not have. `toFlowStep`'s own comment recorded the * assumption behind the gate: *"harmless for old-kind steps (all undefined)."* * * The runtime named in the report settles it: `IfStepExecutor.ExecuteAsync` passes the step's own `StepData` * into `NextStepBuilder`, so `data: { if, then, else }` IS that step's next whatever flow it sits in. * * Fixture: the reported pusher, trimmed to its routing — * `001-projects/nla-extensions/nla-backorder/partials/pushers/Backorder_BackOrderItemEstimatedDate_Pusher.yaml` */ const reportedFlow: ComponentFlow = { kind: 'pusher', phases: [ { name: 'pushSteps', steps: [ { key: 'CHECK_EXISTS', label: 'Check Exists', rootStep: true, stepType: 'API', next: 'IF_VARIANT_IS_EXISTS', }, { key: 'IF_VARIANT_IS_EXISTS', label: 'Check Condition', rootStep: false, stepType: 'IF', data: { if: '{{a}}', then: 'IF_UPDATE_CONDITIONS', else: 'NO_OP_END', }, }, { key: 'IF_UPDATE_CONDITIONS', label: 'Check Condition', rootStep: false, stepType: 'IF', data: { if: '{{b}}', then: 'UPDATE', else: 'IF_METAFIELD_IS_EXISTS' }, }, { key: 'IF_METAFIELD_IS_EXISTS', label: 'Check Condition', rootStep: false, stepType: 'IF', data: { if: '{{c}}', then: 'CLEAN_METAFIELD', else: 'NO_OP_END' }, }, { key: 'UPDATE', label: 'UPDATE', rootStep: false, stepType: 'API' }, { key: 'CLEAN_METAFIELD', label: 'CLEAN_METAFIELD', rootStep: false, stepType: 'API', }, { key: 'NO_OP_END', label: 'No Op End', rootStep: false, stepType: 'SQL', }, ], }, ], } as ComponentFlow; /** * Drawn from the authored record (Story 4.6): the report now charts through `workerFlow`, which reads the component * rather than the `phases` projection. The assertions below are unchanged — they match on step KEYS inside an edge, so * they hold whatever an id is spelled like, which is exactly why this bug's regression test survives the swap intact. * * Each IF step still routes by its `data`, and now the branch leaves a diamond of its own (`…key_:data:if`) instead * of the step's own box. The property under test is unaffected: an edge's left side still names the deciding step. */ const mermaid = buildMermaid(withSource(reportedFlow)); const edges = mermaid.split('\n').filter((l) => l.includes('-->')); /** An edge whose LEFT side is `from` and right side mentions `to`. */ const connects = (from: string, to: string) => edges.some((line) => { const [left, right] = line.split('-->'); return (left ?? '').includes(from) && (right ?? '').includes(to); }); describe('every branch of the reported pusher is drawn', () => { it('connects the entry step by its plain string `next`', () => { // The one edge the chart already drew. expect(connects('CHECK_EXISTS', 'IF_VARIANT_IS_EXISTS')).toBe(true); }); it('connects each IF step’s `then` and `else` from `data`', () => { expect(connects('IF_VARIANT_IS_EXISTS', 'IF_UPDATE_CONDITIONS')).toBe(true); expect(connects('IF_VARIANT_IS_EXISTS', 'NO_OP_END')).toBe(true); expect(connects('IF_UPDATE_CONDITIONS', 'UPDATE')).toBe(true); expect(connects('IF_UPDATE_CONDITIONS', 'IF_METAFIELD_IS_EXISTS')).toBe( true, ); expect(connects('IF_METAFIELD_IS_EXISTS', 'CLEAN_METAFIELD')).toBe(true); expect(connects('IF_METAFIELD_IS_EXISTS', 'NO_OP_END')).toBe(true); }); it('labels the branches, so a reader knows which is which', () => { expect(mermaid).toContain('YES'); expect(mermaid).toContain('NO'); }); it('leaves no step without an inbound edge except the entry', () => { // The symptom as the property it broke: six of seven nodes had no edge at all. for (const key of [ 'IF_VARIANT_IS_EXISTS', 'IF_UPDATE_CONDITIONS', 'IF_METAFIELD_IS_EXISTS', 'UPDATE', 'CLEAN_METAFIELD', 'NO_OP_END', ]) { const inbound = edges.some((line) => (line.split('-->')[1] ?? '').includes(key), ); expect(inbound, `${key} has no inbound edge`).toBe(true); } }); }); describe('a branching IF step is not an END-POINT', () => { it('is not chained to the next phase', () => { // `isTerminal` read `next` alone, so a `data`-branching IF step looked like an end-point and was chained // to the next phase's entry — an edge the flow does not have, drawn beside the branches it was missing. const twoPhase = { kind: 'pusher', phases: [ { name: 'beforePushSteps', steps: [ { key: 'DECIDE', label: 'Decide', rootStep: true, stepType: 'IF', data: { if: '{{x}}', then: 'A', else: 'B' }, }, { key: 'A', label: 'A', rootStep: false }, { key: 'B', label: 'B', rootStep: false }, ], }, { name: 'pushSteps', steps: [{ key: 'SEND', label: 'Send', rootStep: true }], }, ], } as unknown as ComponentFlow; const chart = buildMermaid(withSource(twoPhase)); const decideToSend = chart .split('\n') .filter((l) => l.includes('-->')) .some((l) => { const [left, right] = l.split('-->'); return ( (left ?? '').includes('DECIDE') && (right ?? '').includes('SEND') ); }); expect( decideToSend, 'the branching IF step was treated as an end-point', ).toBe(false); expect(chart).toContain('SEND'); }); });