Close #210 — rig_https_banner.py can now go red on a truncated body - #227
Merged
Conversation
The rig's PASS covered only the banner. Body completeness rode on `total >= 125_000` — a stale literal about a user-editable Wikipedia article — and the `ok` it computed was assigned and never consulted by the verdict. A run reaching 73,720 B of ~125,703 B printed `WARNING: body stalled` and `PASS`, exit 0. Being the only rig that executes `do_https_get`, that is why #211's silent truncation went unnoticed. The fix is not a better literal. The response frames its own length and the C64 has already parsed that framing, so `tools/http_body_checks.py` asks the same question the client asks, off the same DMA-read parser state: Content-Length vs the 24-bit consumed count, or the arrival of the terminal chunk. Neither framing present is INCONCLUSIVE, exit 78, never promoted to a pass. Two of four arms match `http_recv_timeout_verdict` and two diverge on purpose; the module docstring has the table and is the authority. The 6502 chunked arm rejects unconditionally and never reads `http_chunk_state`, which is right for code that only runs when the tick budget expired — a chunked response that got there is short by construction, and the parser's own success exit (`@cb_done` -> `sink_finish_done`, C=0) never passes through the verdict at all — but useless to a rig reading at an arbitrary moment, which needs a positive signal. The 6502 unframed arm returns success because it must return something. What is NOT a divergence is the Content-Length-before-chunked precedence, and that is now pinned against `src/http.s`: reversing those two arms in the 6502 previously left every host-side test green. `test_symbols_...` was also grepping `src/http.s` for names when five of the seven are declared in `src/data.s`; it reads the `.res` widths now. The exit-code decision lives in `decide_exit`, not in the rig. While it was five lines at the bottom of the rig, the only thing testing it was an AST guard, and five one-token mutants kept that guard green — `any(` -> `all(`, `return EXIT_FAIL` -> `EXIT_PASS`, a `[:2]` slice and two dead conjuncts — two of which restore #210 exactly. Name-taint cannot see polarity, list membership, or which constant is returned. As a module function it is executed by the suite instead; the rig calls it BY KEYWORD, because a positional settled/status transposition would hand the still-growing gate a verdict that is never inconclusive and revive the cry-wolf failure while passing every guard. That is a trade, not a closed seam: smaller in consequence, marginally larger in surface. Five untestable decision mutants became one untestable binding mutant, which keywords remove. `read_state`'s slicing, the span arithmetic, the `last_moved` tracking and the `grace` computation are unchanged and still untested — `tools/uci/` rigs need hardware and this one is not importable without the sibling harness. Two things the hardware runs forced: * `check_fetch_settled`. A 300 s budget expired at 591,417 B of a 754,413 B body that was still growing, and the completeness verdict said TRUNCATED while describing the rig's own budget. Still-growing at the deadline is inconclusive. It is not in `checks`, so it can only turn FAIL -> 78, never anything -> PASS. * TURBO_MHZ. The rig inherited whatever clock the previous lane left and got away with it because the banner verdict is clock-independent. A completeness verdict is not: the device was at 1 MHz and the fetch never left "tcp connected". Red-green, all captured: * The suite against master's rig — the real pre-fix artifact — reddens on `the rig never calls decide_exit`. That assertion is ordered ahead of the stale-literal one on purpose: with the literal first, the pre-fix red never reached the substantive guard. * `tools/mutate_http_body_checks.py`: 27/27, `KNOWN_EQUIVALENT` empty. Includes the five exit-code mutants, six more written independently against `decide_exit` by a second review, two that mutate `src/http.s`, and two against the rig's delegation. * M15 behaviourally, on the real numbers: shipped `decide_exit` exits 1 on a 162,996 B shortfall, the mutant exits 0. * Hardware, U64E @ 48 MHz, uci-onchip HTTPS_HOST=en.wikipedia.org HTTPS_PATH=/wiki/Commodore_64, PRG 44e6c0dd…, FETCH_TIMEOUT=900: one PASS at 754,413 B == Content-Length and one FAIL frozen at 299,123 B, on the same artifact. #211 reproduces on the shipped path; treat neither run as the norm. CLAUDE.md records both. The exact tuple the failing run produced is an assertion in the suite. No PRG bytes change: no .s, .cfg or Makefile input is touched, and all seven build variants were rebuilt to prove it. Closes #210. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Closes #210.
The rig's PASS covered only the banner. Completeness rode on
total >= 125_000— a stale literal about a user-editable Wikipedia article — and theokit computed was assigned and then never consulted by the verdict. A run reaching 73,720 B printedWARNING: body stalledandPASS, exit 0. Being the only rig that executesdo_https_get, that is why #211's silent truncation went unnoticed.The fix is not a better literal
The response frames its own length and the C64 has already parsed that framing into DMA-readable BSS, so
tools/http_body_checks.pyasks the same question the client asks, off the same parser state — Content-Length against the 24-bit consumed count, or the arrival of the terminal chunk. Neither framing present is INCONCLUSIVE (exit 78), never promoted to a pass. No PRG change was needed for any of it.Two of four arms match
http_recv_timeout_verdict; two diverge deliberatelyThe 6502 chunked arm rejects unconditionally and never reads
http_chunk_state— right for code that only runs when the tick budget expired, since the parser's own success exit (@cb_done→sink_finish_done, C=0) never reaches the verdict at all, but useless to a rig reading at an arbitrary moment. The unframed arm returns success because the PRG must return something.What is not a divergence is the Content-Length-before-chunked precedence, and that is now pinned against
src/http.s. Reversing those arms in the 6502 previously left every host-side test green — the old symbol check greppedsrc/http.sfor name substrings while five of the seven symbols are declared insrc/data.s. The pin is labelled for what it is: source shape, not behaviour, and weaker evidence than the link-time asserts this repo uses elsewhere. Review confirmed that label is exact — two mutants that change PRG behaviour while preserving the shape survive it, precisely inside the stated limit.The exit-code decision moved into the module
While it was five lines in the rig, an AST taint guard was the only thing testing it — and that guard proves a verdict name reaches an
ifcontaining areturn, nothing about polarity, list membership, or which constant is returned. Five one-token mutants kept it green, two of which restore #210 exactly: exit 0 on a body just reported as truncated.decide_exitis now a pure function in the module, executed by the suite rather than inspected. The rig calls it by keyword, because a positionalsettled/statustransposition would revive the cry-wolf failure while passing every guard.This is a trade, not a closed seam: smaller in consequence, marginally larger in surface. The decision left the seam and is executed; five untestable decision mutants were traded for one untestable binding mutant, which keywords remove.
read_state's slicing, thespan_lo/span_hiarithmetic,last_movedandgraceare unchanged and still untested — the runner says so.Red-green
the rig never calls decide_exit. That assertion is deliberately ordered ahead of the stale-literal one: with the literal first, the pre-fix red never reached the substantive guard, which is part of why the weak taint check went unnoticed.tools/mutate_http_body_checks.py: 27/27,KNOWN_EQUIVALENTempty — the five exit-code mutants, six written independently againstdecide_exitby a second reviewer and folded in (an independent set that lands is worth more than the same set re-run, and only stays worth something if it lives in the file), two that mutatesrc/http.s, and two against the rig's delegation.decide_exitexits 1 on a 162,996 B shortfall;any(→all(exits 0.pytest163 passed;test_pytest_boundary.pyandtest_runner_coverage.pygreen.The mutation runner stages
src/http.sandsrc/data.sinto a tempdir and never writes the repo — verified empirically with a cleangit statusafter a full run.Hardware
U64E @ 48 MHz, uci-onchip,
HTTPS_HOST=en.wikipedia.org HTTPS_PATH=/wiki/Commodore_64, PRG44e6c0dd605db65682131d6621fc9ad7b8b03cb0d22b0b241691abd3c35ed32a,FETCH_TIMEOUT=900— two runs of that one artifact:One of each: treat neither as the norm, and do not quote the PASS on its own. #211 reproduces on the shipped path, and the stall is abrupt — the counter freezes and stays frozen for hundreds of seconds, which is what #219's fast-expiry path predicts (errored socket →
net_pollis a 6-cycleRTS). The old rig would have printedWARNING: body stalled at 299,123 BandPASS, exit 0.An earlier 300 s run is what produced
check_fetch_settled: it reported TRUNCATED while describing the rig's own budget. A still-growing body at the deadline is inconclusive now, andsettledis not inchecks— it can only turn FAIL → 78, never anything → PASS.No PRG bytes change — no
.s,.cfgor Makefile input is touched; all seven build variants rebuilt to prove it, uci-onchip reproducing971b074f65c77eb53b84c9f8f10074bbff81105953e6a04c69533289937977dc.Scope
#211's cause is untouched — this makes the truncation observable, not absent.
boot.sstill ignores the carry, so the menu path renders a truncated body normally; wiring the UI is separate work. A pass is a framing-agreement statement, not byte fidelity: nothing here observes what the server sent.Owed work
This rig still does not call
preflight_reu(), unlike the five other crypto-path rigs. Pre-existing, but this PR promotes it to a completeness oracle with a 900 s budget, so a comb PRG on a REU-disabled device now costs far more before failing. PR #225 lists it inKNOWN_UNPREPPEDnaming this lane as owner; the follow-up is to wireprepare_device(...)afterenable_ucionce both land, asserting 48 MHz to match this rig's new default.Throughput follow-up filed as #226: a truncated run polls a frozen counter for the rest of its budget.
🤖 Generated with Claude Code