fix(cbtop): the --ci gate could not fail, and --iterations 0 forged a green report - #2432
Closed
noahgift wants to merge 1 commit into
Closed
fix(cbtop): the --ci gate could not fail, and --iterations 0 forged a green report#2432noahgift wants to merge 1 commit into
noahgift wants to merge 1 commit into
Conversation
… green report
`apr cbtop --headless --simulated --ci` printed "Status: FAIL | CI: red" and
exited 0. A pipeline step that exists to fail a build could not fail one unless
the caller also remembered to pass an explicit --throughput or --brick-score
number; the report's own verdict was never consulted.
Worse, `--iterations 0` was accepted. With zero measurement iterations every
brick keeps zero samples, so its measured time is 0.0µs, its gap factor 0.00x
and its score a perfect 100/A. The shipped 0.63.0 binary answered the same
threshold pair two ways:
--iterations 100 -> rc=5, "FAIL - Brick score 96 < threshold 100"
--iterations 0 -> rc=0, "PASS - Brick score 100 >= threshold 100"
"Falsification: 7/7 passed Status: PASS | CI: green"
with the measurement count as the only variable. Appending `--iterations 0` to
any cbtop gate made it unconditionally green, and the fabricated numbers were
valid JSON, so a machine consumer could not tell either.
Root causes:
crates/apr-cli/src/commands/cbtop_report_tui.rs:10 — check_ci_thresholds()
only ever set passed=false from config.throughput_threshold /
config.brick_score_threshold. It never read report.status or
report.ci_result. Both headless paths (gguf.rs:40, cbtop_measure_batch.rs:395)
route through this one function, so both were blind.
crates/apr-cli/src/extended_commands.rs:455 — `iterations` had no value
parser, so 0 reached the measurement loop. `--warmup abc` was already
rejected, so the parser validated the field's type but not its domain.
crates/apr-cli/src/commands/cbtop_get_cpu_memory.rs:95 and
cbtop_measure_batch.rs:457 — both spliced a live time-of-day into a
string-literal date ("2026-01-11T..." and "2026-01-12T..."), so every report
ever written, including files persisted with --output for CI provenance, was
stamped seven months stale, and one run could stamp two different days.
crates/apr-cli/src/extended_commands.rs:437,440 — --json and --output both
document "requires --headless" but carried no clap constraint. The flag was
silently dropped and cbtop entered the interactive TUI, so an interactive
user who asked for JSON got a full-screen UI and CI got a raw-mode errno
that said nothing about the actual mistake.
After, against a release binary built from this tree (apr 0.63.0 8cc3aaf):
cbtop --headless --simulated --ci --iterations 0 --brick-score 100 --throughput 900
rc=2 error: invalid value '0' for '--iterations <ITERATIONS>':
must be at least 1 - a zero-iteration run measures nothing ...
cbtop --headless --simulated --ci --iterations 50
rc=5 Status: FAIL | CI: red
cbtop: FAIL - report status FAIL (CI: red), falsification 2/7 passed
cbtop --headless --simulated --json
report: 2026-08-10T17:43:33Z now: 2026-08-10T17:43:33Z
cbtop --json rc=2 error: the following required arguments were not provided: --headless
cbtop --output r.json rc=2 error: the following required arguments were not provided: --headless
cbtop --headless --simulated --iterations 1 rc=0 (smallest honest run still served)
Two pre-existing tests had encoded the defects and went red, correctly:
test_ci_threshold_only_throughput_set built a report with status "FAIL" /
ci_result "red" and asserted check_ci_thresholds returned true. Its fixture
is now green so it isolates what it is actually about (with only a throughput
threshold set, the failing brick score is not consulted), plus a companion
assertion that the same report with a red verdict fails.
test_chrono_timestamp_format asserted ts.starts_with("2026-01-12T"), which
locked the hardcoded date in place. It now brackets the call with a
before/after UTC date so a midnight rollover cannot flake it.
Mutation check - each fix reverted in turn with the tests left in place:
iterations guard removed:
test_run_rejects_zero_iterations panicked "cbtop accepted --iterations 0"
test_parse_cbtop_rejects_zero_iterations panicked "cbtop accepted --iterations 0 at parse time"
and the test output printed the forged report it prevents:
RmsNorm 100 (A) - 0.0us / 1.5us (0.00x) ... Falsification: 7/7 passed
Status: PASS | CI: green
ci_result check removed:
test_ci_gate_fails_on_red_report_without_explicit_thresholds panicked
"cbtop --ci returned pass for a report it had already judged FAIL/red"
test_ci_threshold_only_throughput_set panicked at !check_ci_thresholds(&red, &config)
hardcoded date restored:
test_chrono_timestamp_is_current_utc_date panicked
"timestamp 2026-01-12T17:39:12Z carries neither 2026-08-10 nor 2026-08-10"
test_simulated_report_timestamp_is_current_utc_date panicked likewise,
proving both headless paths now share the one helper
requires="headless" removed:
test_parse_cbtop_json_requires_headless panicked
"cbtop --json was accepted without --headless"
Restored, all 6662 apr-cli lib tests pass.
One consequence worth stating plainly: `make showcase-ci` now exits 5. The
--simulated pipeline jitters each brick +/-20% around its budget, so roughly
half land over budget and the report is genuinely red. It printed "CI
validation passed" for as long as it has existed only because the exit path
ignored the verdict. The target is not referenced by any workflow, so main is
unaffected; the Makefile now says so.
Refs #2397
Audit epic: #2373
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Author
|
Temporarily closing to stop CI contention — the branch is untouched and this will be reopened, nothing is lost. The shared Reopening in batches as the merge queue drains. The work is complete and reviewed; only the CI scheduling is being paced. |
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.
apr cbtop --headless --simulated --ciprintedStatus: FAIL | CI: redand exited 0. A pipeline step whose entire reason to exist is failing a build could not fail one — unless the caller also remembered to pass an explicit--throughputor--brick-scorenumber. The report's own verdict was never consulted.Worse,
--iterations 0was accepted. With zero measurement iterations every brick keeps zero samples, so its measured time is 0.0µs, its gap factor 0.00x, and its score rounds to a perfect 100/A. The shipped 0.63.0 binary answered the same threshold pair two ways, with the measurement count as the only variable:Appending
--iterations 0to any cbtop gate made it unconditionally green, and the fabricated numbers were valid JSON, so a machine consumer could not tell either. The report even claimed 986 tok/s alongsidep50_us: 0.00andp99_us: 0.00.Root cause
crates/apr-cli/src/commands/cbtop_report_tui.rs:10check_ci_thresholds()only ever setpassed = falsefromconfig.throughput_threshold/config.brick_score_threshold. It never readreport.statusorreport.ci_result. Both headless paths (gguf.rs:40,cbtop_measure_batch.rs:395) route through this one function, so both were blind.crates/apr-cli/src/extended_commands.rs:455iterationshad no value parser, so0reached the measurement loop.--warmup abcwas already rejected — the parser validated the field's type but not its domain.crates/apr-cli/src/commands/cbtop_get_cpu_memory.rs:95andcbtop_measure_batch.rs:457"2026-01-11T…"and"2026-01-12T…"). Every report ever written — including files persisted with--outputfor CI provenance — was stamped seven months stale, and one run could stamp two different days.crates/apr-cli/src/extended_commands.rs:437,440--jsonand--outputboth document "requires --headless" but carried no clap constraint. The flag was silently dropped and cbtop entered the interactive TUI: an interactive user who asked for JSON got a full-screen UI, and CI got a raw-mode errno that said nothing about the actual mistake.After
Measured against a release binary built from this tree,
apr 0.63.0 (8cc3aafeb):The exit status now tracks the printed verdict rather than ignoring it: across 10 consecutive
--ciruns the report readStatus: FAIL | CI: redand the exit code was 5 every time. The green direction is covered by unit test, since the simulated pipeline essentially never produces an all-under-budget report.Two pre-existing tests had encoded the defects
Both went red and were fixed, as the brief anticipates:
test_ci_threshold_only_throughput_setbuilt a report withstatus: "FAIL"/ci_result: "red"and assertedcheck_ci_thresholdsreturned true. Its fixture is now green so the test isolates what it is actually about — with only a throughput threshold set, the failing brick score is not consulted — plus a companion assertion that the same report with a red verdict fails.test_chrono_timestamp_formatassertedts.starts_with("2026-01-12T"), locking the hardcoded date in place. It now brackets the call with a before/after UTC date, so a midnight rollover cannot flake it.I also dropped a candidate test that asserted the fabricated report was perfect (
assert_eq!(report.status, "PASS")for a zero-sample pipeline) — that is the same defect-locking pattern, just written by me. It is replaced bytest_run_accepts_one_iteration, which proves the guard discriminates on the value instead of rejecting every run.Mutation check
Each fix reverted in turn, tests left in place.
iterations guard removed — and the failure output prints the forged report the guard prevents:
ci_result check removed:
hardcoded date restored — note the second failure, which proves both headless paths now share the one helper rather than carrying two literals:
requires = "headless"removed:Restored, the working tree diff is byte-identical to the pre-mutation snapshot and all 6662 apr-cli lib tests pass.
One consequence worth stating plainly
make showcase-cinow exits 5. The--simulatedpipeline jitters each brick ±20% around its budget, so roughly half land over budget and the report is genuinely red. That target has printed✅ CI validation passedfor as long as it has existed only because the exit path ignored the verdict — it is the same defect, one layer up. It is not referenced by any workflow, somainis unaffected; the Makefile now carries a note explaining why it is red.Gates
cargo fmt --all -- --checkrc=0 ·cargo clippy -p apr-cli --lib -- -D warningsrc=0 ·cargo test -p apr-cli --lib6662 passed, 0 failed ·cargo test -p aprender-contracts --lib1420 passed, 0 failed ·pv validate contracts/apr-cli-operations-v1.yamlrc=0.Contract
apr-cli-operations-v1gains FALSIFY-OPS-008/009/010, each naming the test that falsifies it.A build note for whoever picks this up: the workspace
CARGO_TARGET_DIRis shared across worktrees, and a concurrent agent's build madeapr-clifail with a spuriouscan't find crate for provable_contracts. Everything above was built and measured with a privateCARGO_TARGET_DIR; nothing in the tree was wrong.Fixes #2397
Audit epic: #2373