Skip to content

fix(seed): the Hydra Triage flow was inert — the step is the edge, not the node - #91

Merged
rubenvdlinde merged 1 commit into
developmentfrom
fix/seeded-triage-flow-is-inert
Jul 31, 2026
Merged

fix(seed): the Hydra Triage flow was inert — the step is the edge, not the node#91
rubenvdlinde merged 1 commit into
developmentfrom
fix/seeded-triage-flow-is-inert

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

The seeded flow ran nothing

Every type and config in SeedHydraTriageFlow sat on nodes[]. OpenRegister's engine never reads those: FlowEngine::stepFor() resolves a transition to the matching entry in edges[], and RegistryStepDispatcher::dispatch() takes type/config from that edge. A node is a Petri-net place and carries no behaviour; a step with no type passes its items through untouched.

So the flow imported cleanly, walked all three edges, and reported completed having called no agent and taken no branch. No error, no warning, no log line.

Measured on a live instance, the same graph both ways:

where type/config sat result
on nodes[] status: completed, items untouched, hermiq never called
on edges[] the agent step ran, and its parsed answer landed on the item

Why the tests didn't catch it

Every assertion read nodes[]. They were checking decoration.

  • testNoNodeCarriesExecutableConfig() is new and is now the load-bearing test.
  • testEveryEndpointAndRouterOutputNamesAReachablePlace() also tightens: a router's outputs must be places the routing edge itself reaches, because FlowEngine::advanceItems() only distributes items to the firing transition's own to list. An output naming anything else drops every item routed to it, silently.

Second defect, same family

The agent step named the agent by its display name. AgentMapper::findByUuid() matches the uuid column, so it resolved to nothing — swallowed into an empty answer before #89, a failed step after it.

The two seeds still stay independent by name rather than by a hard-coded uuid; the name is now resolved to a uuid at seed time, and left empty when the agent is absent. An empty agentId is refused at both validate and execute time, where a name is not — so the flow announces its missing half instead of carrying an identifier that silently resolves to nothing.

Verification

  • 1343 unit tests green (8 in this file, 43 assertions), phpcs and phpstan clean on lib.
  • The edge-form behaviour was verified against the live engine while porting hydra's applier flow (ConductionNL/hydra#435), which hit the identical defect in its own definitions.

…t the node

Every `type` and `config` in the seeded flow sat on `nodes[]`. OpenRegister's
engine never reads those: `FlowEngine::stepFor()` resolves a transition to the
matching entry in `edges[]`, and `RegistryStepDispatcher::dispatch()` takes
`type`/`config` from that edge. A node is a Petri-net place and carries no
behaviour, and a step with no `type` passes its items through untouched.

So the flow imported cleanly, walked all three edges and reported `completed`
having called no agent and taken no branch — no error, no warning, no log line.
Measured on a live instance 2026-07-31, the same graph both ways: node form
completes with the items untouched and hermiq never called; edge form runs the
agent step and lands its parsed answer on the item.

Every assertion in the test suite read `nodes[]`, which is exactly why they all
passed. `testNoNodeCarriesExecutableConfig()` is the one that was missing, and
is now the load-bearing test. `testEveryEndpointAndRouterOutputNamesAReachablePlace()`
also tightens: a router's outputs must be places the ROUTING EDGE reaches,
because `advanceItems()` only distributes to that transition's own `to` list —
an output naming anything else drops its items silently.

Second defect, same family: the agent step named the agent by its display NAME.
`AgentMapper::findByUuid()` matches the `uuid` column, so it resolved to
nothing. The two seeds still stay independent by name rather than by a
hard-coded uuid — the name is now resolved to a uuid at seed time, and left
empty when the agent is absent, because an empty agentId is refused at both
validate and execute time where a name is not.

1343 unit tests green; phpcs and phpstan clean.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ d9bddda

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 117/117
npm ✅ 657/657
PHPUnit
Newman
Playwright ⏭️

Coverage: 85.3% (93/109 statements)


Quality workflow — 2026-07-31 12:38 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 64b6b15 into development Jul 31, 2026
33 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/seeded-triage-flow-is-inert branch July 31, 2026 12:40
rubenvdlinde added a commit to ConductionNL/openregister that referenced this pull request Jul 31, 2026
…it as a no-op (#2227)

* fix(flow): refuse a node that carries step config instead of running it as a no-op

The step is the EDGE. `FlowEngine::stepFor()` resolves a transition to the
matching entry in `edges[]`, and `RegistryStepDispatcher::dispatch()` reads
`type` and `config` off that edge. A node is a Petri-net place and carries no
behaviour.

A `type` on a node is therefore not merely redundant — it is the whole
behaviour of the flow, put where nothing looks. The engine accepted it: every
transition became a pass-through (dispatch() returns items untouched when
`type` is empty) and the run reported COMPLETED. No error, no warning, nothing
in the trace, and an output key simply absent — indistinguishable from a flow
whose steps genuinely had nothing to do.

Three graphs in the fleet were authored this way and none of them failed
anywhere: hydra's dispatch flow (shipped as a completed task), hydra's applier
flow, and hermiq's seeded Hydra Triage flow — whose unit tests asserted on
`nodes[].type` and therefore all passed. Node-shaped authoring is the natural
mistake, because that is how a graph editor presents a flow, and because this
class's own docblock calls itself the translation layer between what users
author and what executes.

`extractPlaces()` already refuses a duplicate id for the same reason ("the
graph would run but not be the graph the user drew"). This is the same kind of
authoring error with a larger blast radius, so it is refused in the same place,
with a message naming the node and where the step belongs.

Presentational keys are untouched: position, label and styling are what a
canvas legitimately stores on a node, and a test pins that they still build.

Fixes #2226. The three affected graphs are corrected in ConductionNL/hydra#435
and ConductionNL/hermiq#91; all three are `enabled: false`, so nothing in
flight is broken by this becoming an error.

15,542 unit tests green (3 new); phpcs, phpstan and psalm clean.

* style: group the `failIfExists` param tag with the rest

Pre-existing phpcs failure on development, not introduced here:
`SaveObject::handleObjectCreation()`'s docblock had `@param bool $failIfExists`
separated from the other param tags by a blank line and misaligned, which trips
"Parameter tags must be grouped together" and the type-padding sniff.

Caught because CI runs phpcs over the whole tree while a local run scoped to the
changed file does not.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant