ledger: spell the scenario corpus's enums the way the server reads them - #472
Merged
Conversation
morph#444 gave every ledger enum a `glz::meta`/`glz::enumerate`, so glaze decodes and encodes them by enumerator name. The scenario corpus still spelled them as their underlying integers, so a real `ladder_ledger_server` refused every payload carrying one with `expected_quote` and every reply assertion written against the integer form was wrong in the same way. Measured on `8bd4132b`: `--rung ledger` was `15 FAIL, 1 ok` -- only `store-list-and-undo-an-entry.scenario`, written after #444, passed -- and the whole corpus `73 files, 58 ok, 15 FAIL`. The rewrite is per-action, not a substitution, because `kind=` names two different enums: `OpenAccount.kind` is `AccountKind` and `SubmitReport.kind` is `ReportKind`, and they collide ten lines apart in `submit-a-report-and-poll-it.scenario`. 274 sites in 15 files: 101 `key=<int>` send sites, 154 escaped reply assertions (`\"kind\":0` -> `\"kind\":\"Asset\"`), 18 `field <enum> <op> <int>` comparisons, and one enum inside the inline `legs=[...]` JSON (`zero-sum-holds-per-currency.scenario`'s `"foreignCurrency":0`, which a send-site grep for `currency=` does not see). Eight prose blocks that documented the integer wire form now document the enumerator names, including the note in `store-list-and-undo-an-entry.scenario` that said its siblings were still broken. Six of the 18 comparisons were the dangerous ones. `Runner.compare` (`morph_scenario.py:952`) is plain Python `!=`, so `expect ok field status != 2` reads `"Pending" != 2` once `status` decodes as a string -- permanently true, measuring nothing, and green. All six (`submit-a-report-and-poll-it.scenario` 70/74/81/85/100 and `open-account-transact-report-close.scenario:117`) are now `!= "Failed"`. The two `== 0` sites in `two-books-are-isolated.scenario` are `== "Pending"`. Neither a green run nor a mutation run detects that class. Seeding one dead `status != 2` back into `submit-a-report-and-poll-it.scenario` and running it against a live server gave `19 steps, 34 assertions, no failures` and `28 mutants, 28 caught, 0 survived` -- mutation perturbs the scenario, not the server, and both mutants of a dead line die anyway. A static scan is the check that catches it, and it did: $ grep -nE '(^|[^A-Za-z])(kind|currency|foreignCurrency|trigger|action|status)(\\")?"?[[:space:]]*(==|!=|=|:)[[:space:]]*-?[0-9]' *.scenario submit-a-report-and-poll-it.scenario:100:expect ok field status != 2 With the seed removed that scan returns nothing, as do the three narrower ones in the issue. That the surviving comparisons are live rather than vacuous was shown directly: flipping line 100 to `!= "Pending"` fails with `reply: ok body={"status":"Pending"}`. Verified: `--rung ledger --twice` green on both passes against one database; the whole corpus `73 files, 73 ok, 0 FAIL` (bookmarks 14, kanban 21, pastebin 12, polls 10 unchanged); `--rung ledger --mutate` 0 survivors; `scenario_coverage.py` exit 0 (`ledger actions 19/19 dispatched, workflows 16/16`); `test_morph_scenario.py` 132 tests, OK. The missing CI gate that let this go unnoticed for five days is morph#462, and stays out of scope here. Fixes #460 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C6uB9zxdSG8qp3VNAAyFvF
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #460.
morph#444 gave every ledger enum a
glz::meta/glz::enumerate, so glaze decodes and encodes them by enumerator name. The scenario corpus still spelled them as their underlying integers, so a realladder_ledger_serverrefused every payload carrying one withexpected_quote, and every reply assertion written against the integer form was wrong in the same way. This rewrites the corpus; #444 is correct and is untouched.Measured before / after
Re-measured on the merge base
8bd4132b(post-#461), not taken from the issue. The issue's master column is stale in exactly the way it predicted it would be; its "master + #461" column is arithmetic, and it turns out to be right.f6a4e464)8bd4132b, post-#461)--rung ledgerkey=<int>send sitesfield <enum> <op> <int>The one file that already passed is
store-list-and-undo-an-entry.scenario, added by #461 and written in the enumerator-name form. Baseline failure, for the record:Prose is 8 blocks, not 7. The issue's list of seven omits
store-list-and-undo-an-entry.scenario:52-55, whose note reads "Every other file in this directory still sends the pre-#444 integers and is refused by the running server for it" — true when written, false after this PR, so it is corrected too.The rewrite is per-action
kind=names two enums.OpenAccount.kindisAccountKind;SubmitReport.kindisReportKind. They collide ten lines apart insubmit-a-report-and-poll-it.scenario(:56→"Asset",:67→"MonthlyStatement"), so a globalkind=0 → "Asset"is wrong on the second. The rewrite resolves every site against the action of thedoline it sits on or follows, and refuses to convert a site whose (action, field) pair it does not know — it reported zero such sites, which is how the 274 conversions are accounted for: 101 + 154 + 18 + 1.The inline-JSON site is the trap a send-site grep misses:
zero-sum-holds-per-currency.scenario:84'slegs=[{…,"foreignCurrency":0},…], now"foreignCurrency":"USD".The six vacuity-risk comparisons
Runner.compare(scripts/scenario/morph_scenario.py:952) is plain Python!=on the parsed JSON value. Oncestatusdecodes as a string,expect ok field status != 2reads"Pending" != 2— trivially, permanently true. The file goes green while the check it exists for has stopped existing. All six areReportStatus, all six asserted on aGetReportStatusreply, all six now compare against a quoted enumerator name:submit-a-report-and-poll-it.scenario:70field status != 2field status != "Failed"submit-a-report-and-poll-it.scenario:74field status != 2field status != "Failed"submit-a-report-and-poll-it.scenario:81field status != 2field status != "Failed"submit-a-report-and-poll-it.scenario:85field status != 2field status != "Failed"submit-a-report-and-poll-it.scenario:100field status != 2field status != "Failed"open-account-transact-report-close.scenario:117field status != 2field status != "Failed"(The 12
==sites are safe by construction —"Asset" == 0is false, so they fail loudly.two-books-are-isolated.scenario:106,109wentstatus == 0→status == "Pending".)Shown live, not asserted. Flipping
:100to!= "Pending"and running it against a real server fails, which is only possible if the comparison reaches the real value:That reply body is also the direct confirmation that
ReportStatusis a string on the wire now.The static scan, and why
--mutateis not the controlmutate_scenario.pycannot detect the vacuous class, and this was measured here rather than taken on trust. Seeding one deadstatus != 2back intosubmit-a-report-and-poll-it.scenario, against a live seeded server:Zero survivors on a file with a provably dead assertion. It is structural: mutation perturbs the scenario, not the server, and both mutants of a dead line die anyway — the kind flip dies on the reply's kind, and
!=→==dies because"Pending" == 2is false.The scan is what caught it. Seeded, caught, removed:
Why this scan rather than the issue's three. It is one regex covering all four syntaxes at once —
kind=0,"foreignCurrency":0,\"kind\":0,field status != 2— where the issue's scan 1 requires a literal=and so is blind to the inline-JSON site (trap 1), which the issue has to cover by naming that one line by hand. It also picks up prose stating the integer form near one of those tokens, which is how three comment lines (submit-a-report-and-poll-it.scenario:29,33,open-account-transact-report-close.scenario:111) that no acceptance grep in the issue mentions were found and fixed. Checked as a superset: on the pre-change tree it matches 277 lines — the 274 code sites plus those 3 prose lines — in exactly the 15 files that needed rewriting, and zero lines instore-list-and-undo-an-entry.scenario.All four scans, on the branch as it stands:
Verification
Release, GCC 16.2.1, own build dir configured
-DMORPH_BUILD_LADDER=ON -DMORPH_LADDER_RUNGS=all -DMORPH_BUILD_NET=ON -DMORPH_BUILD_QT=ON.--mutateis reported as necessary but not sufficient — see the section above for why it is not the control for the defect this issue is about.What is not verified here
scenario_coverage.pyreportsledger actions 19/19andtotal 73/73, not the18/18and72/72in the issue. Both numbers moved on master between the issue being written and this branch's merge base; nothing in this PR changes an action name, and the gate exits 0 either way.test_morph_scenario.pywas not edited. Its pins are on file names and counts; this PR changes file contents only, and the file set is unchanged, so no pin needed touching.scripts/scenario/scenarios/ledger/**is modified — no tooling script, noexamples/**.🤖 Generated with Claude Code