Skip to content

fix(ci): make the server test diagnose itself and re-arm the vacuous gates - #26

Merged
hyperpolymath merged 7 commits into
mainfrom
fix/ci-green
Sep 21, 2026
Merged

hyperpolymath merged 7 commits into
mainfrom
fix/ci-green

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

What this does

Seven commits on top of 81136ef7, targeting the fork's main.

  1. Makes the server test diagnose itself. test/integration/test_server.jl captured no
    subprocess output, never checked process_exited, and swallowed every exception in a bare
    catch — so @test ready was one silent boolean. It now captures stdout/stderr, reports the
    exit code or the measured elapsed time, and prints the log tail on failure.

  2. Sizes the readiness wait by wall-clock deadline, not iteration count. HTTP.jl 1.11.0
    retries ECONNREFUSED inside a single HTTP.get, so a refused localhost probe costs
    1.8–2.1 s, not ~0. The old 60-iteration loop was ~165 s, not the ~30 s its comment claimed.
    Now retry=false plus READINESS_BUDGET_SECONDS = 300, deliberately above the ~165 s that
    already failed in CI so this is not a tolerance regression dressed up as a fix.

  3. Gives the three vacuous checks teeth without blocking upstream. Five
    continue-on-error: true became
    ${{ github.repository != 'hyperpolymath/MetaManifold-WebUI' }}. A pull_request run
    evaluates github.repository as the base repo, so the gate is advisory for the upstream
    contribution and enforcing on this fork. Re-arming it immediately exposed a live SPDX
    violation, fixed here.

  4. Doc/licence drift: missing AGPL text, dead web/ ignore rules, README corrections.

  5. A prepared, never-run history-rewrite script. It cannot push — the force-push commands
    are printed instructions inside a heredoc.

Verification

Three mutants, each confirming a different branch of the new diagnostic:

mutant report wall clock
forced startup crash exited early: true, exit code: 1, stderr tail captured 4.4 s
alive but never listening still running after <N> s, both logs empty full budget
real run, exact CI flags Server smoke tests | 8 pass 38.6 s

⚠ Honest limit: the ~165 s CI failure was never reproduced locally, so this does not prove 300 s
is enough on the runner. If it still reds, the report will now say why.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm

hyperpolymath and others added 7 commits September 21, 2026 14:30
test_server.jl:44 asserted a bare boolean built by a loop whose `catch` was
empty, so "the server crashed at startup" and "the server is not up yet" were
indistinguishable, the subprocess was never checked with `process_exited`, and
its stdout/stderr went nowhere. Grepping the whole 4,122-line CI log for any
server output returned nothing at all — the single failing assertion carried
no evidence whatsoever.

Now: stdout/stderr are captured to files, the wait loop breaks early on
`process_exited`, and on failure the test logs the exact command, the exit
code (or "still running"), and the last 40 lines of each stream.

Also drops `--compiled-modules=no` from the spawned server's argv. Measured on
julia 1.12.5, `Base.julia_cmd()` propagates both `--compiled-modules=no` and
`--code-coverage=user` (not `-t`), so the child was loading Oxygen, DuckDB and
the pipeline with precompilation disabled inside a ~30s readiness budget.
`--code-coverage=user` is deliberately KEPT: the SIGINT in the `finally` block
exists precisely so the child flushes its coverage data, and stripping the flag
would silently delete the server-route coverage that shutdown path was written
to collect.

This diagnoses the failure; it does not claim to fix it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
Three of this repo's four checks reported success while proving nothing.

1. Repo hygiene could not fail. 67f2faa added `continue-on-error: true` at
   job level AND on all four steps, and turned the commit-convention
   `::error::` into a `::warning::` above an unchanged `exit 1`. Its reason
   was sound — upstream does not gate on SPDX/format/lint/commit-convention —
   but the blanket form also disarmed the check here, where it is the only
   thing watching. Replaced with
   `${{ github.repository != 'hyperpolymath/MetaManifold-WebUI' }}`: a
   `pull_request` run evaluates `github.repository` as the BASE repo, so the
   checks stay advisory for the owner's PR and enforcing on this fork.
   `::error::` restored, since the step does in fact exit 1.

2. Gate triage reported success on pushes having triaged nothing. All three
   substantive steps take <owner>/<repo> <pr-number> and were individually
   gated on `pull_request`, leaving a push to run only a squabbler build and
   a bundled fixture — then report "Gate triage: success". Gated at job level
   instead and the now-redundant per-step guards removed. `!cancelled()` is
   kept, conjoined rather than replaced: without it the job would skip
   whenever `test` fails, which is the exact case gate triage exists for.
   Accepted cost: the push leg was also a squabbler-regression canary, and
   that canary is gone.

3. The cladistic-explorer step was a permanent no-op. `if [ -f
   test/unit/test_clade_cumulus.jl ]` guarded a file that has never existed
   on main (only src/analysis/clade_cumulus.jl), so the step always took the
   else branch and always reported success. Removed. Its sibling
   analysis-config step lost the same `if [ -f … ]` wrapper: the file exists,
   and if it is ever deleted the step should go red rather than quietly pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
Five of the six cheap wins from the recon. Each was verified to still hold
against the current tree before being touched.

README
  - The CI badge pointed at JoshuaJewell/MetaManifold-WebUI, so the front page
    of this fork displayed upstream's build status rather than its own.
  - The Julia badge and the prerequisites line both claimed ">= 1.0" while
    mise.toml, the CI matrix and test_install_pins.jl all pin 1.12.5 exactly.
  - The hero <img> pointed at .github/screenshots/hero.png; that directory has
    never existed, so it rendered as a broken image. Block removed.
  (The clone URL still points upstream deliberately — that is the project's
  canonical home, not drift.)

LICENSES/AGPL-3.0-only.txt
  Three SPDX identifiers are in use — AGPL-3.0-only, MPL-2.0, CC-BY-SA-4.0 —
  but only the latter two had licence texts, so a strict REUSE lint failed
  even though the full AGPL text sits at the repository root. Added from
  LICENSE verbatim.

.gitignore
  Dropped six `!web/dist/...` un-ignore rules left over from 4ea9ed0. No web/
  directory exists anywhere in this repository.

src/MetaManifold.jl
  Included analysis/analysis_config.jl, a six-line shim, instead of the
  1,615-line canonical analysis/AnalysisConfig.jl. Now includes the canonical
  file directly. The shim stays for external callers but is off the load path
  and now says so — including both would define module AnalysisConfig twice.
  Verified: `using MetaManifold` precompiles and loads, AnalysisConfig defined.
  The milestone-3 note describing the old two-hop arrangement is updated.

NOT done, deliberately: `frontend/package.json` test:e2e. Adding Playwright
would mean an unpinned network fetch, lockfile churn and a large non-bun
toolchain, and docs/testing/infrastructure.md already records the e2e lane as
knowingly unprovisioned — "a heavyweight CI provisioning decision reserved for
a later prompt". Overriding a deferred decision is not a cheap win.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
.git is 269.35 MiB of a 282 MB checkout against a ~12 MB working tree, and
`git count-objects -vH` reports prune-packable 0 / garbage 0, so `git gc`
reclaims none of it. This script is the remedy, written out in full and
deliberately NOT executed: it refuses without --i-have-read-the-warnings,
operates on a fresh clone in a scratch directory, and never pushes.

Measuring the pack first corrected the plan in two ways.

  1. data/MiSeq_SOP/ holds 170.80 MiB across 54 blobs and is the single
     largest item — larger than data/VESPA_pool (143.17 MiB) and
     data/Multiplex_pool (71.56 MiB), the only two paths originally slated
     for removal. Stripping just those two would have left the biggest
     offender untouched.

     It also cannot be stripped wholesale. It splits cleanly:
     163.09 MiB in 48 uncompressed *.fastq blobs, none of which are present
     in the working tree, and 7.71 MiB in 6 *.fastq.gz blobs which ARE live
     fixtures, deliberately re-whitelisted at .gitignore:294-304. The strip
     rule is therefore a regex anchored on \.fastq$ so .gz never matches.

  2. "Nothing junk is committed — no node_modules" is true of the working
     tree and false of history: 1,322 node_modules blobs (18.84 MiB) are
     reachable, as is 20.13 MiB under a web/ directory that no longer exists.

Full accounting, aggregate blob bytes across all refs:

     170.80 MiB    54  data/MiSeq_SOP/      (163.09 dead / 7.71 live)
     143.17 MiB     6  data/VESPA_pool/     dead
      71.56 MiB     2  data/Multiplex_pool/ dead
      20.13 MiB    29  web/                 dead
      18.84 MiB  1322  **/node_modules/     dead
      13.67 MiB  1381  everything else      the actual repository

Removing the five dead classes drops ~416.79 MiB of blob content and leaves
~21.4 MiB. That is roughly double what the two-pool plan would have reclaimed.

The script does not push because the push is the dangerous half: this fork's
`main` IS the head branch of JoshuaJewell#6, so a force
push rewrites an open pull request belonging to someone else and invalidates
every existing clone. That stays a human decision, and a coordinated one.

git-filter-repo is not installed on this machine; the script checks for it
and prints the pipx install line rather than falling back to filter-branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
Found by running scripts/check-spdx.sh locally after giving the hygiene gate
teeth again. The file arrived in 7782b65 without a header, and the check that
would have caught it was non-blocking at the time, so it went in unnoticed.

This is the vacuous-gate cost made concrete: the gate was not merely
uninformative, it was already concealing a live violation. check-spdx now
reports OK across 265 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
The readiness loop's own comment claimed a "30 s budget". That number was an
INFERENCE written in the declarative voice of a measurement, and it was wrong.

Measured 2026-09-21 against a dead port, HTTP.jl 1.11.0: a refused connection
on localhost does NOT return immediately. HTTP.jl's default retry layer treats
ECONNREFUSED as recoverable and retries it with backoff INSIDE a single
HTTP.get call, so one probe costs 1.8-2.1 s (0.63 s with retry=false), not ~0.
The real budget was therefore ~165 s, clocked at 2m44.8s against a server that
was alive but never listening. Both naive arithmetics -- "readtimeout x 60 =
90 s" and "sleep x 60 = 30 s" -- were wrong, because the per-probe cost is set
by a library layer the loop's author never sees.

Fixed so the number in the comment is true by construction rather than by
arithmetic:

  * retry=false on the probe. This loop IS the retry; a retry layer nested
    inside a retry loop makes the budget unknowable.
  * a wall-clock deadline instead of an iteration count, so probe cost can
    drift without silently changing what the test waits for.
  * the failure report prints MEASURED elapsed time, not the constant.

The budget is 300 s, deliberately ABOVE the ~165 s the old loop effectively
allowed -- that is the budget which already proved insufficient in CI, so
adopting a smaller number would have been a tolerance regression dressed up as
a fix. It is spent only on the failure path; the loop exits the moment the
server binds.

This also weakens the cold-load hypothesis for the CI red: a server that fails
to bind in 2 3/4 minutes fits "slow cold load" considerably worse than one
failing in 30 s.

Verified by killing mutants, not by a green suite:
  * crash mutant (error() at the top of server.jl) -> report gives the child
    command, exited early: true, exit code 1, and the stderr tail carrying the
    error; loop breaks in 4.4 s via process_exited rather than polling out.
  * hang mutant (alive, never listening) -> "still running after 120.0 s",
    both log files EMPTY handled, SIGINT cleanup still reaps the child.
  * unmutated, exact CI flags -> Server smoke tests | 8 pass | 47.1s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
The script printed a line about the working tree being unchanged and then
compared nothing. An inert assertion is worse than no assertion: it occupies
the place a reader expects a guard to be, so it is read as one.

Record BEFORE_TREE alongside BEFORE_HEAD and compare it to the post-rewrite
HEAD tree. The invariant is exact, not approximate: every path in the strip set
is already absent from HEAD, so the checked-out tree MUST come out
byte-identical. Any difference means a --path or --path-regex caught a LIVE
file, and the run must not be pushed.

That is the specific danger in this strip set. data/MiSeq_SOP/ splits into
163.09 MiB of dead uncompressed *.fastq and 7.71 MiB of LIVE *.fastq.gz
fixtures, deliberately re-whitelisted at .gitignore:294-304. The regex is
anchored '\.fastq$' so .gz cannot match -- but "the regex looks right" is not
evidence, and the tree comparison is.

The script remains unexecuted and still refuses to run without
--i-have-read-the-warnings. git-filter-repo is not installed on this machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 075cf402-9b90-41ec-b7b3-54d0b50c547e

📥 Commits

Reviewing files that changed from the base of the PR and between 81136ef and 4565055.

📒 Files selected for processing (10)
  • .github/workflows/ci.yml
  • .gitignore
  • LICENSES/AGPL-3.0-only.txt
  • README.md
  • config/ci/lint_source.jl
  • docs/milestones/03-analysis-config-v1-milestone3.md
  • scripts/strip-history.sh
  • src/MetaManifold.jl
  • src/analysis/analysis_config.jl
  • test/integration/test_server.jl
 ______________________________________________________
< Review complete: I laughed, I cried, I filed issues. >
 ------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@sonarqubecloud

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath marked this pull request as ready for review September 21, 2026 14:25
@hyperpolymath
hyperpolymath merged commit 440b5fd into main Sep 21, 2026
2 of 4 checks passed
@hyperpolymath
hyperpolymath deleted the fix/ci-green branch September 21, 2026 14:25
hyperpolymath added a commit that referenced this pull request Sep 21, 2026
…gates (#26)

## What this does

Seven commits on top of `cc1ccc65`, targeting the fork's `main`.

1. **Makes the server test diagnose itself.**
`test/integration/test_server.jl` captured no
subprocess output, never checked `process_exited`, and swallowed every
exception in a bare
`catch` — so `@test ready` was one silent boolean. It now captures
stdout/stderr, reports the
exit code or the measured elapsed time, and prints the log tail on
failure.

2. **Sizes the readiness wait by wall-clock deadline, not iteration
count.** HTTP.jl 1.11.0
retries `ECONNREFUSED` *inside* a single `HTTP.get`, so a refused
localhost probe costs
1.8–2.1 s, not ~0. The old 60-iteration loop was ~165 s, not the ~30 s
its comment claimed.
Now `retry=false` plus `READINESS_BUDGET_SECONDS = 300`, deliberately
above the ~165 s that
already failed in CI so this is not a tolerance regression dressed up as
a fix.

3. **Gives the three vacuous checks teeth without blocking upstream.**
Five
   `continue-on-error: true` became
`${{ github.repository != 'hyperpolymath/MetaManifold-WebUI' }}`. A
`pull_request` run
evaluates `github.repository` as the **base** repo, so the gate is
advisory for the upstream
contribution and enforcing on this fork. Re-arming it immediately
exposed a live SPDX
   violation, fixed here.

4. Doc/licence drift: missing AGPL text, dead `web/` ignore rules,
README corrections.

5. A prepared, **never-run** history-rewrite script. It cannot push —
the force-push commands
   are printed instructions inside a heredoc.

## Verification

Three mutants, each confirming a different branch of the new diagnostic:

| mutant | report | wall clock |
|---|---|---|
| forced startup crash | `exited early: true`, `exit code: 1`, stderr
tail captured | 4.4 s |
| alive but never listening | `still running after <N> s`, both logs
empty | full budget |
| real run, exact CI flags | `Server smoke tests \| 8 pass` | 38.6 s |

⚠ Honest limit: the ~165 s CI failure was never reproduced locally, so
this does not prove 300 s
is enough on the runner. If it still reds, the report will now say why.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
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