Skip to content

fix(cbtop): the --ci gate could not fail, and --iterations 0 forged a green report - #2432

Closed
noahgift wants to merge 1 commit into
mainfrom
fix/cbtop-ci-gate-cannot-fail
Closed

fix(cbtop): the --ci gate could not fail, and --iterations 0 forged a green report#2432
noahgift wants to merge 1 commit into
mainfrom
fix/cbtop-ci-gate-cannot-fail

Conversation

@noahgift

Copy link
Copy Markdown
Contributor

apr cbtop --headless --simulated --ci printed Status: FAIL | CI: red and 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 --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 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:

--iterations 100 -> rc=5  cbtop: FAIL - Brick score 96 < threshold 100
--iterations 0   -> rc=0  cbtop: PASS - Brick score 100 >= threshold 100
                          Falsification: 7/7 passed
                          Status: PASS | CI: green

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. The report even claimed 986 tok/s alongside p50_us: 0.00 and p99_us: 0.00.

Root cause

file:line defect
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 — 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…"). 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: 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):

$ apr 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 and would report every brick as a
       perfect 100/A from zero samples
(stdout: 0 bytes — no report is produced at all)

$ apr cbtop --headless --simulated --ci --iterations 50
rc=5
  Status: FAIL | CI: red
cbtop: FAIL - report status FAIL (CI: red), falsification 2/7 passed

$ apr cbtop --headless --simulated --json --iterations 20
report: 2026-08-10T17:43:33Z
now:    2026-08-10T17:43:33Z

$ apr cbtop --json
rc=2  error: the following required arguments were not provided: --headless
$ apr cbtop --output r.json
rc=2  error: the following required arguments were not provided: --headless

$ apr cbtop --headless --simulated --iterations 1     # smallest honest run
rc=0
$ apr cbtop --headless --simulated --json --iterations 5
rc=0  parsed ok, status= FAIL

The exit status now tracks the printed verdict rather than ignoring it: across 10 consecutive --ci runs the report read Status: FAIL | CI: red and 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_set built a report with status: "FAIL" / ci_result: "red" and asserted check_ci_thresholds returned 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_format asserted ts.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 by test_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:

    ✅ RmsNorm      100 (A) - 0.0µs / 1.5µs (0.00x)
    ✅ QkvBrick     100 (A) - 0.0µs / 6.0µs (0.00x)
    ... (all seven bricks 0.0µs, score 100)
  Falsification: 7/7 passed
  Status: PASS | CI: green

panicked at cbtop_falsification_summary_hardware.rs:519: cbtop accepted --iterations 0: ()
panicked at parsing.rs:270: cbtop accepted --iterations 0 at parse time
test result: FAILED. 17 passed; 2 failed

ci_result check removed:

panicked at cbtop_falsification_summary_hardware.rs:492:
  cbtop --ci returned pass for a report it had already judged FAIL/red
panicked at cbtop_headless_report.rs:284:
  assertion failed: !check_ci_thresholds(&red, &config)
test result: FAILED. 49 passed; 2 failed

hardcoded date restored — note the second failure, which proves both headless paths now share the one helper rather than carrying two literals:

panicked at cbtop_chrono_timestamp_get.rs:23:
  timestamp 2026-01-12T17:39:12Z carries neither 2026-08-10 nor 2026-08-10 — the real UTC date
panicked at cbtop_chrono_timestamp_get.rs:41:
  report timestamp 2026-01-12T17:39:12Z carries neither 2026-08-10 nor 2026-08-10
test result: FAILED. 0 passed; 2 failed

requires = "headless" removed:

panicked at parsing.rs:291: cbtop --json was accepted without --headless
test result: FAILED. 0 passed; 1 failed

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-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. That target has printed ✅ CI validation passed for 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, so main is unaffected; the Makefile now carries a note explaining why it is red.

Gates

cargo fmt --all -- --check rc=0 · cargo clippy -p apr-cli --lib -- -D warnings rc=0 · cargo test -p apr-cli --lib 6662 passed, 0 failed · cargo test -p aprender-contracts --lib 1420 passed, 0 failed · pv validate contracts/apr-cli-operations-v1.yaml rc=0.

Contract apr-cli-operations-v1 gains FALSIFY-OPS-008/009/010, each naming the test that falsifies it.

A build note for whoever picks this up: the workspace CARGO_TARGET_DIR is shared across worktrees, and a concurrent agent's build made apr-cli fail with a spurious can't find crate for provable_contracts. Everything above was built and measured with a private CARGO_TARGET_DIR; nothing in the tree was wrong.

Fixes #2397
Audit epic: #2373

… 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>
@noahgift

Copy link
Copy Markdown
Contributor Author

Temporarily closing to stop CI contention — the branch is untouched and this will be reopened, nothing is lost.

The shared clean-room runner host is at 91% disk with a load average over 100, and workspace-test has started timing out at 75 minutes on unrelated PRs (#2385) purely from contention. This audit has 23 PRs open at once and drafts still trigger full CI in this repo, so parking them as drafts did not reduce load.

Reopening in batches as the merge queue drains. The work is complete and reviewed; only the CI scheduling is being paced.

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.

apr cbtop --iterations 0 fabricates a perfect green report; --ci exits 0 on a red report

1 participant