Skip to content

ci(launchpad): run the review-agent unit suites via one CONTROLS row (#575) - #576

Merged
tucktuck101 merged 1 commit into
launchpadfrom
ci/review-agent-unit-suites-575
Aug 24, 2026
Merged

tucktuck101 merged 1 commit into
launchpadfrom
ci/review-agent-unit-suites-575

Conversation

@serina-mcfall

@serina-mcfall serina-mcfall commented Aug 24, 2026 •

Copy link
Copy Markdown

Refs #575

Issue type

Task

Provenance

Field Value
Harness / provider Claude Code (Anthropic)
Model Claude Opus 5
Initiating human @serina-mcfall

What this does

Seven test_*.py suites sit beside run_controls.py and all of them pass, but nothing ran them. This adds one row to CONTROLS and the small control that row points at.

     ("check_fetch_states.py", False),
+    ("check_unit_suites.py", False),
 ]

No workflow file changes — launchpad-review-agent-controls.yml already filters on launchpad/review-agent/**.

Why one row and not a new workflow

Two records rule out a second CI entry point, and this follows them rather than reopening the question:

run_controls.py already runs each entry as subprocess.run([sys.executable, HERE / script]) and treats non-zero as FAIL, so a discovery runner needed no change to the harness.

The control is named check_unit_suites.py, not check_adjudication.py — the #118 plan reserves that name for STEP 10's one-control-per-done-criterion work, and this must not squat it.

Why the gap existed

The escalation dead-ended. #566's review raised it as a High finding recorded against #270; #270 handed it to #118 STEP 10; STEP 10's done-when never mentions test_*.py, and check_adjudication.py does not exist on any branch. Five of the suites already record the gap in their own docstrings.

Verification

The harness picks it up and names it in the summary:

$ python3 run_controls.py
PASS  check_contract.py
...
PASS  check_fetch_states.py
PASS  check_unit_suites.py

14 passed, 0 failed, 0 skipped

Both failure modes proven rather than assumed. A control that cannot fail would reproduce the very defect this closes.

Zero-case discovery — the same code with a pattern matching no files:

FAIL  discovery over test_nothing_matches_*.py collects at least one test case (got 0)

1 failure(s)
exit=1

A genuinely broken assertion — one assertEqual flipped to assertNotEqual in test_verdicts.py, then reverted:

PASS  discovery over test_*.py collects at least one test case (got 176)
FAIL  all 176 test cases pass (exit 1)

FAIL: test_well_formed_pair_validates_clean (test_verdicts.WellFormedPairTests...)
    self.assertNotEqual(verdicts.validate(input_doc, output_doc), [])
AssertionError: [] == []

The guard counts cases, not files: renaming every test_* method to check_* would satisfy a file count while discovery collected zero. That trap is documented in launchpad-agents-tests.yml, whose first version had it.

Cost: 176 cases in 0.78s on this branch (271 on #566's head, which adds suites). No third-party dependency — every import is stdlib or a sibling module.

Confirmed on CI

The controls check on this PR went green, and the log shows the new row running rather than merely a green job — run 32693706845:

PASS  check_fetch_states.py
PASS  check_unit_suites.py

14 passed, 0 failed, 0 skipped

Not verified

  • I did not see the control go red on a CI runner — both failure modes were proven locally, not by pushing a deliberate break to this branch. task: run the review-agent unit suites in CI — one row in CONTROLS, no second workflow #575's done-when asks for that; it is the one criterion this PR has not evidenced on CI.
  • I did not audit the thirteen existing CONTROLS entries for overlap with the test_*.py cases. Some assertions may be duplicated, which would make this additive coverage rather than wholly new coverage.
  • No reviewer has run on this branch. It was opened as a draft for that reason; a ready_for_review event marked it ready at 2026-08-24T05:31:06Z, which was not this agent and is not done by any workflow in the repository. Treat it as awaiting a first review regardless of its draft state.
  • I did not change what any test asserts, so anything the new coverage exposes later is a separate finding.

…575)

Seven test_*.py suites sit beside run_controls.py and all of them pass, but
nothing ran them: the controls workflow's only step is run_controls.py, and
CONTROLS listed thirteen entries, none a test_*.py runner. suite.py is #120's
containment suite, not a discovery run, and the one workflow using unittest
discover is scoped -s launchpad/agents.

Five of the suites record the gap in their own docstrings. The escalation
dead-ended: #566's review raised it against #270, #270's Out of scope handed it
to #118 STEP 10, and STEP 10 is one-control-per-done-criterion over adjudication
behaviour -- it never runs test_*.py.

One row in CONTROLS rather than a second workflow, per #270's own reasoning and
the #118 plan at line 187. The controls workflow already filters on
launchpad/review-agent/**, so no workflow file changes.

check_unit_suites.py counts cases before running them, because unittest discover
exits 0 on an empty suite and that is indistinguishable from a pass. It counts
CASES not files -- renaming every test_* method to check_* would satisfy a file
count while discovering zero cases.

Verified both failure modes rather than assuming them: a pattern matching no
files exits 1 on the guard, and flipping one assertEqual to assertNotEqual in
test_verdicts.py turns the control red and names the failing case. Mutation
reverted. run_controls.py reports 14 passed, 0 failed, 0 skipped.

Refs #575

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
@serina-mcfall serina-mcfall added the by:agent Filed or authored by an AI agent, not a human label Aug 24, 2026
@serina-mcfall
serina-mcfall marked this pull request as ready for review August 24, 2026 05:31

@serina-mcfall serina-mcfall left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at ef914626b — no blocker. One Medium, one Low.

Agent-authored under @serina-mcfall's instruction. Per launchpad/AGENTS.md:165 — "Draft everything. Approve nothing." — this approves nothing.

Independence, stated up front: this PR is authored under the same identity I am acting as (serina-mcfall, 05:26 today, a different session). I did not write it, and everything below was re-derived rather than taken from the docstring — but this is not an independent review in the org sense, and it should not be the only one.

It is a real check, and I proved that rather than reading it

Three mutations, all caught:

$ python3 check_unit_suites.py                        # as-is
PASS  discovery over test_*.py collects at least one test case (got 176)
PASS  all 176 test cases pass (exit 0)

# rename every `def test_` to `def check_` across all seven suites
# — the filename trap the docstring names:
FAIL  discovery over test_*.py collects at least one test case (got 0)

# flip one assertEqual to assertNotEqual:
PASS  discovery ... (got 176)
FAIL  all 176 test cases pass (exit 1)
FAIL: test_well_formed_document_validates_clean (test_findings...)

The two-step split does what the docstring claims. Counting cases, not files, is the part that matters: the filename mutation leaves seven test_*.py files on disk and a file-counting guard would have reported seven suites while discovery collected zero. That trap is real and the comment at :45-49 is right that launchpad-agents-tests.yml's first version had it.

The integration is sound too, which is where a control like this usually fails. run_controls.py:56-61 runs each control as a subprocess and appends to failed on non-zero; :70-71 returns 1. And :72-75 returns 2 when a control was skipped, with "A skipped control has not run. Do not report a clean gate." — so this row cannot be satisfied by absence.

Every factual claim in the docstring reproduces:

Claim Check
Seven suites in the directory git ls-tree | grep -c '/test_[^/]*\.py$' → 7 ✓
CONTROLS listed thirteen entries, none a test_*.py runner 13 on launchpad, none a test module ✓
The one workflow using discover is scoped to launchpad/agents discover("launchpad/agents", pattern="test_*.py") ✓
plans/2026-08-13-issue-118-adjudication.md:187 — "STEP 10 appends one row; no second workflow is added" verbatim ✓

Medium — nothing in the chain has a timeout, and the job has none either

$ grep -c timeout check_unit_suites.py run_controls.py .github/workflows/launchpad-review-agent-controls.yml
check_unit_suites.py:0
run_controls.py:1        # ← line 40, and it is on the `gh api rate_limit` probe, not on control invocation
launchpad-review-agent-controls.yml:0

So: check_unit_suites.py:68's subprocess.run has no timeout, run_controls.py:56's control invocation has no timeout, and the workflow job has no timeout-minutes. A single hanging test case runs to GitHub's default job limit — six hours — with no signal beyond a stalled job.

The missing timeouts in run_controls.py and the workflow are pre-existing and not this PR's to fix. What is this PR's is that it is the row that makes a hang reachable: the other twelve controls do bounded, self-contained work, and this one shells out to an arbitrary and growing test suite. A future test that opens a socket, waits on a lock, or patches subprocess.run incompletely turns a red build into a six-hour stall.

Cheap and local: timeout= on the subprocess.run at :68, treating TimeoutExpired as a FAIL with the elapsed budget in the label. security_audit_secrets_check.py already has that shape (_TIMEOUT_SECONDS, except subprocess.TimeoutExpired), so the pattern is in the repo. A timeout-minutes on the job is worth raising separately since it protects all thirteen.

Medium rather than Blocker: the suites are unit tests that mock their I/O, so nothing hangs today. This is about what the row makes possible, not what it does now.

Low — the failure tail can cut the first failure

:77-78 prints proc.stdout[-2000:] and proc.stderr[-4000:]. unittest writes the per-failure tracebacks first and the summary last, so tail-truncation reliably keeps the count and the last failures while dropping the earliest — and the earliest is usually the one to debug. With 176 cases and a handful failing, 4,000 characters is enough today; it is the wrong end to keep if a change ever breaks a dozen.

Not worth its own round trip. If the file is open for the timeout fix, printing the head of stderr alongside the tail (or raising the tail budget) costs a line.

What I would keep exactly as it is

The docstring is the strongest part, and unusually so for a CI control. It records why the escalation dead-ended — #566's review pointed at #270, #270's "Out of scope" handed it to #118 STEP 10, and STEP 10 is one-control-per-done-criterion over adjudication behaviour and never runs test_*.py. That is the reason seven passing suites went unrun, and it is the thing a future reader needs in order not to re-create the gap. "A suite nothing runs is a claim, not a check: it cannot fail, so it cannot protect anything" is the right sentence to have written down.

Two design calls I would not change: one CONTROLS row rather than a second workflow, with the plan-doc line cited rather than asserted; and a subprocess rather than an in-process TextTestRunner, for the two stated reasons — the suites patch process-global state, and a failure reproduces verbatim as the command a contributor runs by hand. Both are argued rather than assumed.

Also correct, and easy to get wrong: an ImportError counts as one case because the loader substitutes _FailedTest, so it passes step 1 and fails step 2. The comment at :51-53 names that explicitly, and it is the right split — "nothing to run" and "does not pass" are different failures.


Reviewed at head ef914626b against origin/launchpad. The three mutations, the four docstring claims and the run_controls.py exit path were all run by me directly. Not verified: behaviour on a genuinely hanging test — I reasoned that from the absent timeout rather than inducing a stall, and did not run this under the real workflow.

🤖 Review drafted by Claude Code (claude-opus-5) for @serina-mcfall.

@tucktuck101 tucktuck101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approved

@tucktuck101
tucktuck101 merged commit 4632045 into launchpad Aug 24, 2026
32 checks passed
@serina-mcfall
serina-mcfall deleted the ci/review-agent-unit-suites-575 branch August 31, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

by:agent Filed or authored by an AI agent, not a human

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants