Skip to content

test(dryrun): make the router-stage unknown-handler pins able to fail, and populate meta_ops (BACKLOG #1688, #1692) - #1228

Merged
wshallwshall merged 2 commits into
mainfrom
claude/b1688-1692-dryrun-pins-and-meta-ops
Sep 17, 2026
Merged

wshallwshall merged 2 commits into
mainfrom
claude/b1688-1692-dryrun-pins-and-meta-ops

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Two rows that both land in tests/test_dryrun.py, in separate regions (BACKLOG #1688, #1692).

BACKLOG #1688: which pins could not fail, and which can now

Three pins guard the router-stage fail-closed in route_only -- the raise ValueError that refuses a Router naming a handler the registry does not hold. Only one of them could fail if that raise went away.

Pin Before After
test_dryrun.py::test_route_only_unknown_handler_raises could fail unchanged
test_dryrun.py::test_router_to_unknown_handler_is_error could NOT fail can fail
test_wiring_engine.py::test_inbound_unknown_handler_dead_letters_at_ingress could NOT fail can fail

The dry-run pin asserted ERROR and "ghost" in result.error. Neither is unique to the router stage. Delete the raise and the message reaches transform_one, whose registry.handlers[hname] raises KeyError('ghost'); dry_run's catch-all renders that as router/handler error: 'ghost' -- still ERROR, still carrying ghost. It now asserts returned unknown handler 'ghost', which only the router stage emits.

The runner pin asserted ERROR and that no file was delivered, and nothing about the stage. Delete the raise and a routed row IS committed for the ghost handler; the transform worker's own missing-handler branch (pinned separately in test_adr0071_dispatch_wiring.py) dead-letters it -- the same terminal disposition and the same absent file. It now reads the queue and pins the whole footprint: exactly one dead ingress row, no routed row, and a stored error naming the guard. The whole footprint rather than a filtered probe, because a bare "no routed row" query cannot tell a fail-closed apart from a message that never got past the listener.

Negative control, run locally

route_only's raise ValueError(...) replaced by pass, then the three pins run:

FAILED tests/test_dryrun.py::test_route_only_unknown_handler_raises
FAILED tests/test_dryrun.py::test_router_to_unknown_handler_is_error
FAILED tests/test_wiring_engine.py::test_inbound_unknown_handler_dead_letters_at_ingress
3 failed in 1.05s

The runner pin's failure is the predicted shape, not a generic red:

assert [('routed', 'dead', 'ghost')] == [('ingress', 'dead', None)]
WARNING messagefoundry.pipeline.wiring_runner: transform worker 'file_in':
        handler 'ghost' ... is missing; dead-lettering

The guard was then restored and the three pins re-run green.

#1688 is tests-only. When the raise fires, its text already flows verbatim into result.error, so no engine change was needed. The workbook forecast listing pipeline/dryrun.py for this row was wrong.

BACKLOG #1692: meta_ops

Neither dry_run path passed meta_ops=outcome.meta_ops to its DryRunResult, so the field was empty whatever a Handler declared, and the CLI output dict carried no meta_ops key. A Handler's SetMeta was invisible to the dryrun CLI and the Test Bench while the SetState beside it came through.

Both construction sites now pass it, and the CLI emits meta_ops next to state_ops.

PHI gating. A SetMeta key and value are both message-derived in the general case -- a Handler is free to build either from a field -- so both are redacted unless --show-phi is passed, matching state_ops member for member. MetaOpPreview's docstring asserted that gate throughout the window in which nothing reached it; it now states what is gated and records that window.

Tests pin both construction sites separately (the HL7 path and the raw non-HL7 _dry_run_raw path, so fixing one does not cover the other) and both arms of the CLI gate. The redaction test asserts the fixture actually declared a SetMeta before asserting the absences, so it cannot pass vacuously.

#1690's recorded block on this row is discharged -- "declined" shipped in PR 1169 -- so no serialization work was needed.

Checks

Check Result
ruff format --check . pass, 1308 files
ruff check . pass
mypy messagefoundry see below
pytest, 6 files 286 passed, 1 skipped

Test files run: test_dryrun.py, test_cli.py, test_wiring_engine.py, test_adr0071_dispatch_wiring.py, test_ingress_guard_parity.py, test_checks.py. Enumerated as the two files this branch edits, the two the changed code is consumed by, plus the two the dispatch brief named as neighbouring pins on the same fail-closed.

The full local suite was not run -- it does not finish in one turn on this box.

mypy: 22 errors, all pre-existing in this worktree's venv and none in a file this branch touches. They are import-not-found and downstream no-any-return in auth/webauthn.py, transports/dicom.py, parsing/dicom/_deps.py and parsing/fhir/_deps.py, caused by the optional extras (fhir, dicom, x12, xml, webauthn, vault) being absent from the local interpreter. CI installs them. pipeline/dryrun.py and __main__.py are clean.

Hosted legs a reviewer must read

Every leg, since nothing here ran against the optional extras locally. Specifically:

  • the full pytest matrix with [dev,harness,fhir,dicom,x12,xml,webauthn,vault] installed -- every extras-gated module removed itself at collection time locally, so those counts are absent from the numbers above;
  • mypy strict on a runner that has the extras, which is the only place the 22 errors above can be confirmed as environmental;
  • windows-service-smoke, which never runs locally.

Open question

scripts/hooks/context-budget.ps1 was already modified in this worktree at creation, from a clean origin/main checkout -- rewritten to a 20-line disabled stub citing an owner decision dated today. Nothing in this branch touches it and it is deliberately not staged, so it is absent from both commits. Flagging it because something is rewriting that file into fresh worktrees, and a session that ran git add -A would have carried it into an unrelated PR.

wshallwshall added 2 commits September 16, 2026 17:21
… (BACKLOG #1688)

Two of the three M-7 pins stayed green with route_only's fail-closed deleted,
because a downstream path reaches the same ERROR.

The dry-run pin matched only the ghost name. With the guard gone, transform_one's
registry.handlers[hname] raises KeyError a stage later and dry_run renders that as
"router/handler error: 'ghost'" -- still ERROR, still carrying the name. It now
asserts the router-stage wording instead.

The runner pin asserted ERROR and no delivered file, and nothing about the stage.
With the guard gone a routed row IS committed for the ghost handler and the
transform worker's own missing-handler branch dead-letters it -- same disposition,
same absent file. It now reads the queue and pins the whole footprint: one dead
ingress row, no routed row, and a stored error naming the guard.

Negative control: with the raise replaced by pass, all three pins fail.
…HI gate (BACKLOG #1692)

Neither dry_run path passed meta_ops=outcome.meta_ops to its DryRunResult, so the
field was empty whatever a Handler declared, and the CLI output dict had no
meta_ops key at all. A Handler's SetMeta was therefore invisible to the dryrun CLI
and the Test Bench, while the SetState beside it came through.

Both construction sites now pass it, and the CLI emits meta_ops next to state_ops.
A SetMeta key and value are both message-derived in the general case, so both are
redacted unless --show-phi is passed, matching a state write exactly.

MetaOpPreview's docstring claimed that gate throughout the window in which nothing
reached it; it now says what is gated and records that.

Tests pin both construction sites (the HL7 path and the raw non-HL7 path fail
independently) and both arms of the CLI gate.
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

LANDER INSPECTION -- labelled self-review, not a peer review

Posted under the korus LANDER.md line 364 obligation. I did not author this change.

The vacuous-pin claim is correct, and verifiable from the source without running anything

route_only raises ValueError(f"router {ic.router!r} returned unknown handler {hname!r}"). Delete
it and transform_one's first statement is a bare registry.handlers[hname], whose docstring says
outright "The caller guarantees hname is registered." So a deleted guard yields KeyError('ghost'),
dry_run's catch-all renders "router/handler error: 'ghost'", and the old assertion --
"ghost" in result.error -- is satisfied by the failure it was meant to exclude.

The runner half reproduces the same way through a different route: without the guard, routing commits
a routed row, the transform worker's _prepare_routed returns missing_handler=True, and
dead_letter_now produces the same terminal ERROR and the same absent output file. Both old pins
green with the guard gone.

The replacements are shape-exact, and that is the difference

  • dry-run: "returned unknown handler 'ghost'" in result.error -- the guard's own wording, which the
    KeyError path cannot produce.
  • runner: staged == [(INGRESS, DEAD, None)] against [('routed','dead','ghost')] with the guard
    gone, plus the error text. Two independent channels, a stage footprint and a message.

The third pin calls route_only directly under pytest.raises and was never vacuous. The PR says so
and leaves it alone, which is the right classification.

The production half is a separate, real bug

meta_ops was defined on DryRunResult, populated on RouteOutcome, and passed at neither
construction site
-- so the field was always empty. The MetaOpPreview docstring meanwhile claimed
the CLI gated value behind --show-phi, describing a gate over something that never arrived.

That is this session's defect class in a third register: not a test that cannot fail, but a control
described in prose over a value that was never populated.
The PR fixes both and says so.

The __main__.py addition mirrors the adjacent state_ops block member for member, and redacts key
and value alike rather than only the value -- correct, since a Handler may build either from a field.

Verdict

Verdict: merge.

@wshallwshall
wshallwshall added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 9fbbc39 Sep 17, 2026
42 checks passed
@wshallwshall
wshallwshall deleted the claude/b1688-1692-dryrun-pins-and-meta-ops branch September 17, 2026 01:20
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