Skip to content

feat(testgen): the acceptance gate now rejects tests that pass when the code is broken - #131

Merged
stranske merged 1 commit into
mainfrom
claude/testgen-gate-rejects-hollow-tests
Aug 26, 2026
Merged

feat(testgen): the acceptance gate now rejects tests that pass when the code is broken#131
stranske merged 1 commit into
mainfrom
claude/testgen-gate-rejects-hollow-tests

Conversation

@stranske

Copy link
Copy Markdown
Owner

Dedup finding (CLAUDE.md §0)

Checked src/ and tests/ for hollow, deliberate, break, local_verify, node_verdict; searched the improvement log; read features.py's testgen entry.

testgen_gate.py exists and is exactly the right place — an "assured-acceptance gate for generated pytest tests" with collect/import, baseline non-regression, repeated-run reliability and --min-covered-lines-delta. It had no reference to local_verify, hollowness or node grading. So this extends an existing capability rather than building a new one, and registers nothing new.

What was wrong

The gate's strongest criterion was coverage_delta"covered lines went up by at least N". A test that calls the function and asserts nothing raises covered lines exactly as much as one that pins the result. So the gate guarding generated tests could be satisfied by tests that can never fail.

Proven end-to-end on a fixture of one real and two hollow tests, all three passing normally:

PASS  coverage_delta      covered-lines delta 4 >= required 1
FAIL  no_hollow_nodes     2 test(s) pass against a broken base: …test_hollow_smoke

The old gate accepts that set. The new one names both hollow nodes and refuses.

How

hollow_check() shells out to local_verify.py, which reverts to --base-ref and re-runs the candidates per node: a node that still passes did not notice the break, and is hollow.

It reads node_verdict/node_analysis, not the exit code — per-node grading is advisory by construction in local_verify and deliberately leaves the process result alone. A gate reading the exit code would accept hollow tests while believing it had checked.

NOT EVALUATED is not a pass

This is the load-bearing decision. The probe can be prevented from running — no --base-ref, no local_verify.py, a timeout. In that state the gate has no evidence at all, so ok is False and the detail says NOT EVALUATED: <reason>.

Reporting that as ok would make the strongest check the easiest one to switch off silently — the exact defect class this gate exists to catch, one level up.

Break → revert

break caught by
ok widened to not hollow_nodes (unevaluated passes) two asserts fail
the check renamed out of the list (silently dropped) lookup fails

Byte-identical revert both times.

Also pinned: coverage_delta is asserted green in the same synthetic case where no_hollow_nodes is red — so the selftest states in code why the new check cannot be replaced by the old one.

Lane

testgen_lane.py passes --base-ref/--test-path through when given; its selftest asserts both the absent and present forms. It never guesses a ref — the gate fails closed when one is missing, so omitting it cannot quietly buy a green run.

No un-gating was required

capability_advisor reports both as "matched but a gate blocked invocation", but dispatch_ready is false fleet-wide by construction: it needs status=active plus immutable version lineage, which 0 of 33 capabilities have. That governs automatic router dispatch only. Both modules are CLIs and run today.

One gap worth naming

CLAUDE.md §0 asks for the dedup finding in the capability's ledger notes. capabilities.py's only write path is transition, which requires a lifecycle state change — and extending a capability isn't a promotion, so there is no honest transition to make and no note-only command exists. Recorded in the commit instead.

Verified

458 passed, 0 failed, 0 skipped, 85/85 selftests, 5/5 gates. Floor unchanged — these are selftest cases, which add no collected pytest tests.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 14 minutes.

View limit details

Limit details: You’ve used the included review currently available. Your 80 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 89faf183-803f-4054-889d-48001dbdf1a8

📥 Commits

Reviewing files that changed from the base of the PR and between 0b31f8a and 73edb06.

📒 Files selected for processing (2)
  • src/testgen_gate.py
  • src/testgen_lane.py

Comment @coderabbitai help to get the list of available commands.

…he code is broken

DEDUP (CLAUDE.md §0). Grepped src/ and tests/ for hollow/deliberate/break/local_verify/
node_verdict; searched the improvement log; read features.py's testgen entry. testgen_gate.py
EXISTS and is the right home -- an assured-acceptance gate with collect/import, baseline
non-regression, reliability and --min-covered-lines-delta -- and had NO hollowness notion.
This EXTENDS it; no capability is registered.

WHAT WAS WRONG. The gate's strongest criterion was `coverage_delta`. A test that calls the
function and asserts nothing raises covered lines exactly as much as one that pins the
result, so the gate guarding GENERATED tests could be satisfied by tests that can never
fail. Proven end-to-end on a fixture of one real and two hollow tests, all three passing
normally:

    PASS  coverage_delta      covered-lines delta 4 >= required 1
    FAIL  no_hollow_nodes     2 test(s) pass against a broken base: ...test_hollow_smoke

The old gate accepts that set. The selftest now asserts BOTH facts in one case, so the
reason this check exists cannot be lost to a later tidy-up.

BUILT ON #124's CONVENTION, not beside it. That PR drained this module to "misuse is not a
bad-test verdict": every check carries `could_not_measure`, and a blind check names the
misuse AND the remedy. The hollowness probe is the same shape -- it can be prevented from
running (no --base-ref, no local_verify.py, a timeout) -- so it returns `measured: False`
with a reason and a remedy, and `_hollow_check_row` renders the same COULD NOT MEASURE
wording. A blind probe never passes, for the reason #124 gives for coverage_delta: this is
the strongest check here, so letting "could not run" read as ok would make it the easiest
one to switch off silently. Break -> revert: widening `ok` to `not nodes` fails two asserts;
reverted byte-identical.

It reads node_verdict/node_analysis, NOT the exit code -- per-node grading is advisory by
construction in local_verify and deliberately leaves the process result alone, so a gate
reading the exit code would accept hollow tests while believing it had checked.

testgen_lane.py forwards --base-ref/--test-path when given and never guesses one: the gate
fails closed without it, so an omitted ref surfaces as a failed check rather than as a
silently weaker gate. Its selftest asserts both the absent and present forms.

No un-gating was needed. capability_advisor reports these as "matched but a gate blocked
invocation", but dispatch_ready is false fleet-wide by construction (status=active plus
immutable version lineage, which 0 of 33 capabilities have). That governs automatic ROUTER
dispatch; both modules are CLIs and run today.

VERIFY: 457 passed, 1 failed, 0 skipped, 85/85 selftests, 5/5 gates. The one failure is
PRE-EXISTING ON CLEAN MAIN and machine-local, confirmed by stashing this branch and
re-running: test_model_tier_resolution.test_capacity_gate_is_seat_level_not_gemini_special
unpacks three values from capacity.compute(), whose docstring says "(state, reason[, meta])"
-- meta optional. `_shed(agent)` is `(SHED_DIR / agent).exists()`, a file OUTSIDE
$ORCH_STATE_DIR, and this machine has a real codex 429 shed flag, so compute() takes the
two-value early return. main's CI is green because a runner has no shed file. The test
should neutralise _shed rather than depend on the host; filed separately, not bundled here.

Floor unchanged: these are selftest cases, which add no collected pytest tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@stranske
stranske force-pushed the claude/testgen-gate-rejects-hollow-tests branch from e69f697 to 73edb06 Compare August 26, 2026 01:00
@agents-workflows-bot

Copy link
Copy Markdown
Contributor

Workflow source needed

PR #131 needs either a linked GitHub issue or one valid non-issue Workflow Source before PR metadata automation can manage it safely.

Please do one of:

  • Add <!-- meta:issue:123 --> or a normal Closes #123 / Related to #123 line.
  • Check one Workflow Source option in the PR body.
  • Add a hidden marker such as <!-- workflow-source:local_request -->, <!-- workflow-source:manual_remote -->, <!-- workflow-source:review_followup -->, <!-- workflow-source:sync_campaign -->, or <!-- workflow-source:dependabot -->.
  • Add a workflow source label such as workflow:source-direct-pr, workflow:source-local-request, workflow:source-review-followup, workflow:source-sync, or workflow:no-automation.

Once a valid source is present, this warning will not be reposted.

@github-actions

Copy link
Copy Markdown
Contributor

Workflow state fingerprint for Keepalive Loop Reporter. Do not edit.

@stranske-keepalive

Copy link
Copy Markdown

Automated Status Summary

Head SHA: 8a150ef
Latest Runs: ⏳ pending — Gate
Required: core tests (3.12): ⏳ pending, core tests (3.13): ⏳ pending, docker smoke: ⏳ pending, gate: ⏳ pending

Workflow / Job Result Logs
(no jobs reported) ⏳ pending

Coverage Overview

  • Coverage history entries: 1

Coverage Trend

Metric Value
Current 33.60%
Baseline ⚠️ not configured (absent)
Delta n/a — nothing to compare against
Minimum 70.00%
Status ❌ Below minimum

No baseline was read (absent), so the delta above is not a measurement. Status reflects only the --minimum floor. Write config/coverage-baseline.json with a line or coverage percentage to enable the comparison; that file is deliberately not synced from Workflows, so each repo owns its own.

Top Coverage Hotspots (lowest coverage)

File Coverage Missing
src/capability_effectiveness.py 0.0% 154
src/capability_firing_monitor.py 0.0% 192
src/capability_matcher_proposals.py 0.0% 111
src/capability_opportunity.py 0.0% 143
src/capability_propensity.py 0.0% 1958
src/capability_task_proposals.py 0.0% 195
src/ccusage_reconcile.py 0.0% 286
src/codemod_lane.py 0.0% 351
src/evidence_acquisition.py 0.0% 103
src/exploration_collection.py 0.0% 331
src/feature_scan.py 0.0% 118
src/frontend_verify.py 0.0% 255
src/improvement_log.py 0.0% 248
src/issue_readiness.py 0.0% 507
src/keepalive_evidence.py 0.0% 378

Low Coverage Files (<50.0%)

File Coverage Missing
src/capability_effectiveness.py 0.0% 154
src/capability_firing_monitor.py 0.0% 192
src/capability_matcher_proposals.py 0.0% 111
src/capability_opportunity.py 0.0% 143
src/capability_propensity.py 0.0% 1958
src/capability_task_proposals.py 0.0% 195
src/ccusage_reconcile.py 0.0% 286
src/codemod_lane.py 0.0% 351
src/evidence_acquisition.py 0.0% 103
src/exploration_collection.py 0.0% 331
src/feature_scan.py 0.0% 118
src/frontend_verify.py 0.0% 255
src/improvement_log.py 0.0% 248
src/issue_readiness.py 0.0% 507
src/keepalive_evidence.py 0.0% 378

Updated automatically; will refresh on subsequent CI/Docker completions.


Keepalive checklist

Scope

No scope information available

Tasks

  • No tasks defined

Acceptance criteria

  • No acceptance criteria defined

@github-actions

Copy link
Copy Markdown
Contributor

Workflow state fingerprint for Agents Gate Followups. Do not edit.

@stranske

Copy link
Copy Markdown
Owner Author

Merging with one red check, and stating why rather than waving it through.

The failure is Merge automerge-labelled agent PRs, which died on:

API rate limit exceeded for user ID 23046322
Max retries (5) reached for rate limit. Token: unknown.

That is the fleet automation's own token, not this PR. The job's purpose is to merge OTHER automerge-labelled PRs; it neither builds nor tests anything here, and it would have failed identically on an empty commit.

Every substantive check is green: verify.py (real pytest counts + selftests + capability gates), Gate / gate, python ci 3.12 and 3.13, lint-ruff, lint-format, typecheck-mypy.

Local verification: 457 passed, 1 failed, 85/85 selftests, 5/5 gates. That one local failure is pre-existing on clean main and machine-local — test_capacity_gate_is_seat_level_not_gemini_special unpacks three values from capacity.compute(), whose docstring says (state, reason[, meta]), and _shed() reads (SHED_DIR / agent).exists() — a file outside $ORCH_STATE_DIR. This machine has a real codex 429 shed flag, so compute() takes the two-value early return. CI is green because a runner has no shed file. Confirmed by stashing this branch and re-running on clean main.

@stranske
stranske merged commit deb31f3 into main Aug 26, 2026
57 of 58 checks passed
@stranske
stranske deleted the claude/testgen-gate-rejects-hollow-tests branch August 26, 2026 03:28
stranske pushed a commit that referenced this pull request Aug 29, 2026
…D, not by uncovered lines

DEDUP (CLAUDE.md §0). Checked src/ for priorit/rank/hotspot/target — only capability_targets.py,
which ranks CAPABILITIES not test targets. Grepped the tree for hotspot/prioriti/rank_. Searched
the improvement log for "escaped defect" and "test priority": both reported a real absence with
the log read in full. Nearest existing thing is coverage_trend.compute_top_files, which ranks by
LOWEST COVERAGE — precisely the ordering this exists not to lead with. Not present; building new.
`capability_admission.py --preflight` clears with zero blocking failures; caller, heartbeat and
fixture remain obligations, and the fixture is discharged here.

WHY NOT MOST-UNCOVERED-FIRST. That maximises percentage-per-PR and points agents at the largest
uncovered files, which are the glue modules where a meaningful test is hardest to write. It is the
ordering most likely to produce hollow tests, so uncovered mass is the LAST tier, not the first.

THREE TIERS, lexicographic:
  1. ESCAPED DEFECTS — a file that later needed a fix is a file whose tests missed something. The
     only tier reporting observed failure of the TESTS rather than a property of the code.
  2. CHURN — where regressions actually arrive.
  3. UNCOVERED MASS — how far the metric moves. Last, deliberately.
All three multiplied by (1 - hollow_rate), so a module where agents keep producing tests that pass
against a broken base sinks however much uncovered code it has. That term only became measurable
when testgen_gate grew no_hollow_nodes in #131.

LEXICOGRAPHIC, NOT A WEIGHTED SUM, and the first version was the latter. Multiplying the tiers
apart (1e6/1e3/1) and summing holds only while the lower tiers stay small: at 1,000,000 uncovered
statements tier 3 exactly equals one escaped defect and the ordering inverts. A scoring function
whose correctness depends on inputs staying under a magic threshold is a defect waiting for a big
repository. Two of the new tests caught it during review; the tuple holds at any magnitude and the
tests now assert at 10**9. No blended "score" is reported at all — one number formed from three
incomparable tiers invites exactly the trade-off the tuple forbids.

TIER 1 IS A GIT PROXY TODAY AND SAYS SO. The Brain has the better signal in outcomes.durability,
where broke_later means merged, CI green, broke afterwards. Measured across 4,665 outcome rows:
durable 2842, abandoned 1300, pending 517, reverted 4, reworked 2, broke_later ZERO —
durability_sweep assigns "reopened, reverted, or durable" and never broke_later, while
pattern_miner.TERMINAL_FAILURE_DURABILITY consumes it. A consumer for a label nothing produces.
Six escaped-defect rows cannot order a queue, so tier 1 reads git history, which needs no
instrumentation and exists in every repo. brain_signal_status() reports which source is live and
returns "unknown" rather than zero when the store cannot be read, so the day the Brain signal
becomes usable is visible rather than assumed.

RANKING IS NOT TRAINING. A fix commit touching a file is decent evidence for ORDERING work and
poor evidence for a learner: code changes for many reasons. This module ranks; it never writes
durability labels. A broke_later producer must clear a higher bar and is deliberately separate.

A SECOND DEFECT OF MY OWN, caught while validating on real data: the CLI treated a missing
--coverage-json as an empty report, so tier 3 rendered as a column of zeros that reads as
"everything is covered" rather than "nothing was read" — the same could-not-measure-as-measured-
zero shape this module's own notes describe. It now names the file and the failure. Found when a
scratch path was cleaned up between runs and the ranking carried cheerfully on.

VALIDATED ON REAL DATA. stranske/Trend_Model_Project, its real Gate coverage payload and its real
history: multi_period/engine.py ranks first on 9 fix commits, 28 churn, 335 uncovered — while
3_Results.py, which has the MOST uncovered statements at 759, ranks third. That inversion is the
design working.

Break -> revert on both invariants: restoring the weighted sum fails the two tier-ordering tests;
removing the hollow discount fails the two hollow tests. Byte-identical revert, 13 pass.

Tests are pytest, not selftest cases, on purpose: local_verify grades per pytest NODE, so a
selftest is one node and its assertions are invisible to hollow detection. A module that orders
test-writing work should have its own tests gradeable by the gate judging that work. The module
keeps a --selftest as well (88/88), exercising the CLI as it ships including live git parsing.

Verified: 515 passed, 0 failed, 0 skipped, 88/88 selftests, 5/5 gates. Floor 502 -> 515.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
stranske added a commit that referenced this pull request Aug 29, 2026
…D, not by uncovered lines (#153)

* feat(testgen): rank test-writing work by where testing actually FAILED, not by uncovered lines

DEDUP (CLAUDE.md §0). Checked src/ for priorit/rank/hotspot/target — only capability_targets.py,
which ranks CAPABILITIES not test targets. Grepped the tree for hotspot/prioriti/rank_. Searched
the improvement log for "escaped defect" and "test priority": both reported a real absence with
the log read in full. Nearest existing thing is coverage_trend.compute_top_files, which ranks by
LOWEST COVERAGE — precisely the ordering this exists not to lead with. Not present; building new.
`capability_admission.py --preflight` clears with zero blocking failures; caller, heartbeat and
fixture remain obligations, and the fixture is discharged here.

WHY NOT MOST-UNCOVERED-FIRST. That maximises percentage-per-PR and points agents at the largest
uncovered files, which are the glue modules where a meaningful test is hardest to write. It is the
ordering most likely to produce hollow tests, so uncovered mass is the LAST tier, not the first.

THREE TIERS, lexicographic:
  1. ESCAPED DEFECTS — a file that later needed a fix is a file whose tests missed something. The
     only tier reporting observed failure of the TESTS rather than a property of the code.
  2. CHURN — where regressions actually arrive.
  3. UNCOVERED MASS — how far the metric moves. Last, deliberately.
All three multiplied by (1 - hollow_rate), so a module where agents keep producing tests that pass
against a broken base sinks however much uncovered code it has. That term only became measurable
when testgen_gate grew no_hollow_nodes in #131.

LEXICOGRAPHIC, NOT A WEIGHTED SUM, and the first version was the latter. Multiplying the tiers
apart (1e6/1e3/1) and summing holds only while the lower tiers stay small: at 1,000,000 uncovered
statements tier 3 exactly equals one escaped defect and the ordering inverts. A scoring function
whose correctness depends on inputs staying under a magic threshold is a defect waiting for a big
repository. Two of the new tests caught it during review; the tuple holds at any magnitude and the
tests now assert at 10**9. No blended "score" is reported at all — one number formed from three
incomparable tiers invites exactly the trade-off the tuple forbids.

TIER 1 IS A GIT PROXY TODAY AND SAYS SO. The Brain has the better signal in outcomes.durability,
where broke_later means merged, CI green, broke afterwards. Measured across 4,665 outcome rows:
durable 2842, abandoned 1300, pending 517, reverted 4, reworked 2, broke_later ZERO —
durability_sweep assigns "reopened, reverted, or durable" and never broke_later, while
pattern_miner.TERMINAL_FAILURE_DURABILITY consumes it. A consumer for a label nothing produces.
Six escaped-defect rows cannot order a queue, so tier 1 reads git history, which needs no
instrumentation and exists in every repo. brain_signal_status() reports which source is live and
returns "unknown" rather than zero when the store cannot be read, so the day the Brain signal
becomes usable is visible rather than assumed.

RANKING IS NOT TRAINING. A fix commit touching a file is decent evidence for ORDERING work and
poor evidence for a learner: code changes for many reasons. This module ranks; it never writes
durability labels. A broke_later producer must clear a higher bar and is deliberately separate.

A SECOND DEFECT OF MY OWN, caught while validating on real data: the CLI treated a missing
--coverage-json as an empty report, so tier 3 rendered as a column of zeros that reads as
"everything is covered" rather than "nothing was read" — the same could-not-measure-as-measured-
zero shape this module's own notes describe. It now names the file and the failure. Found when a
scratch path was cleaned up between runs and the ranking carried cheerfully on.

VALIDATED ON REAL DATA. stranske/Trend_Model_Project, its real Gate coverage payload and its real
history: multi_period/engine.py ranks first on 9 fix commits, 28 churn, 335 uncovered — while
3_Results.py, which has the MOST uncovered statements at 759, ranks third. That inversion is the
design working.

Break -> revert on both invariants: restoring the weighted sum fails the two tier-ordering tests;
removing the hollow discount fails the two hollow tests. Byte-identical revert, 13 pass.

Tests are pytest, not selftest cases, on purpose: local_verify grades per pytest NODE, so a
selftest is one node and its assertions are invisible to hollow detection. A module that orders
test-writing work should have its own tests gradeable by the gate judging that work. The module
keeps a --selftest as well (88/88), exercising the CLI as it ships including live git parsing.

Verified: 515 passed, 0 failed, 0 skipped, 88/88 selftests, 5/5 gates. Floor 502 -> 515.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(floor): resolve against #152 as the union, re-measured on the merge result

#152 landed underneath this branch and added a NEW floor key, mirror_skipped_max, bounding the
exec mirror by its own skip ceiling rather than the runner's. Resolved as the UNION per the rule
this file states: main's structure kept in full including that key, this branch's rationale
appended rather than either note replacing the other, and the count RE-MEASURED on the merge
result rather than carried forward from either side.

515 from verify.py's own run after the rebase, which confirms #152 added no collected tests. That
makes 502 + 13 correct -- but measured, not assumed. Taking a side would have been silently right
this time, and it is what put the floor 8 below reality on an earlier occasion.

Verified on the merge result: 515 passed, 0 failed, 0 skipped, 88/88 selftests, 5/5 gates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Tim Stranske <tim@stranskemo.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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