test(server): #434 isolate Bun.serve integration suites in aggregate runs#438
Draft
vansin wants to merge 1 commit into
Draft
test(server): #434 isolate Bun.serve integration suites in aggregate runs#438vansin wants to merge 1 commit into
vansin wants to merge 1 commit into
Conversation
…runs Baseline `bun test src/` on d418862 reported 515 pass / 8 fail / 1518 expects. The 8 failures were api-host-supervisors-fallback fetch()s against an unbound port — one Bun.serve singleton wins the port when two integration suites import ./index.js in the same process, and downstream tests race a random 18000-slot picked parent-side (TOCTOU). Fix: minimal production seam + a Bun-authored aggregate runner that runs each real-server suite in its own child. ## Production seam (src/index.ts) - Extract the sole `Bun.serve({...})` call into `export function bootServer(opts?: { port?: number; hostname?: string })` that returns the real `Bun.Server`. `opts.port ?? PORT` and `opts.hostname ?? HOST` use `??` on purpose — `||` would swallow a legitimate `0`. - Wrap all post-serve top-level side effects (retention sweeper, stale sweeper, shutdown handlers, banner, vault check) in `if (import.meta.main) { ... }`. Production `bun run src/index.ts` still boots identically; the banner now prints `${server.hostname}:${server.port}` (effective bind, so an OS-ephemeral bind shows the assigned port, not `PORT`). - Zero reformatting of the ~1850-line Bun.serve object literal — reviewable diff stays surgical. - Verified: `bun run src/index.ts` boots with correct banner and effective port; `bootServer({ port: 0 })` from a test binds an OS-assigned ephemeral port and returns it via `server.port`. ## Test refactor (2 files, isolated_server kind) - `src/api-host-supervisors-fallback.test.ts`: drop `PORT = 18000 + Math.random() * 1000` and the `process.env.PORT` mutation; call `bootServer({ port: 0, hostname: "127.0.0.1" })` in `beforeAll`; finalize `BASE` from `server.port`; call `server.stop(true)` in `afterAll`; cleanup includes the DB dir (sweeps WAL + SHM in one shot). - `src/uploads-http.test.ts`: same pattern. Individual results (running each isolated suite in its own Bun child): src/api-host-supervisors-fallback.test.ts → 8/0/30 pass src/uploads-http.test.ts → 10/0/26 pass The 8 previously-red tests are now green because the assertions finally reach — no fetch() dials an unbound port anymore. ## Aggregate runner (scripts/test-runner.ts + scripts/test-manifest.ts) - Bun-authored (no Node runtime dependency). Serial by construction — no `--concurrency=N` flag ships in this PR (determinism first; parallelism gated on evidence). - CLI: `--suite=<manifest-path>` (exact match only) and `--verbose`. No glob patterns — the manifest is the source of truth. - Manifest is exhaustive: `ISOLATED_SERVER_SUITES` + `SHARED_UNIT_SUITES` cover every `*.test.ts` under `server/src/` and `server/scripts/`. On start the runner does a set-equality check against the actual filesystem — a missing or extra entry hard-fails BEFORE any suite runs. Duplicate check first, then disk vs manifest. - Per-child env: explicit allowlist of parent keys (PATH, TMPDIR, TERM, LANG, LC_ALL, LC_CTYPE, CI, GITHUB_ACTIONS, SHELL, USER, LOGNAME). Everything else dropped. `DATABASE_URL` is `delete env. DATABASE_URL`'d both after the allowlist copy and after suite-specific overrides — belt+braces defense on top of the #435 db-adapter guard. - Per-child `HOME`, `COMMHUB_DB`, `COMMHUB_UPLOADS_DIR`, `TMPDIR`: fresh `mkdtempSync` per suite. On exit the runner rms the whole dir (sweeps `db`, `db-wal`, `db-shm` at once). - `NODE_ENV=test` forced for every child. - Exit code is the gate. Pass/fail/expect counters are parsed only for the aggregate summary — a green count-line can never green-wash a non-zero child exit. - stdout/stderr continuously drained (no buffered deadlock). `--verbose` streams live; otherwise the runner prints the tail on failure. - SIGINT/SIGTERM handlers forward to every live child then clean up temp dirs before propagating exit. ## Runner self-test (scripts/test-runner-self.test.ts) - Rule 9 / #435 defense-in-depth: proves the runner's env-shaping produces `DATABASE_URL=undefined` in a Bun child even when the parent process has a leaked fake-prod DATABASE_URL. Spawns a real Bun -e child, asks it to print `process.env.DATABASE_URL || "UNSET"`, asserts "UNSET". - Also includes the clean-parent baseline (no leaked DATABASE_URL → child still UNSET) and a smoke-test that `NODE_ENV="test"` is set. ## package.json - `"test": "bun run scripts/test-runner.ts"` — canonical gate. - `"test:raw": "…echo warning to stderr… && bun test src/"` — retained as diagnostic-only. Running it prints a loud warning that this path reproduces the module-singleton conflicts documented in #434 and is not a CI gate. ## Reproducibility (3× consecutive canonical runs) Every run identical: total pass: 526 total fail: 0 total expects: 1555 [isolated_server] src/api-host-supervisors-fallback.test.ts exit=0 8/0/30 [isolated_server] src/uploads-http.test.ts exit=0 10/0/26 [shared_unit] (28 files) exit=0 508/0/1499 = baseline 515 pass + 8 tests newly-reaching (which were previously counted as fails) + 3 new runner-self tests = 526 pass. 1518 expects + 7 runner-self + additional expects from newly-reaching assertions = 1555. Zero fail, zero ConnectionRefused, zero DB guard trip, zero port collision. Runner also proven to: - reject an unregistered `stray.test.ts` on disk with a hard fail BEFORE any test runs - reject a duplicate manifest entry the same way ## Scope - `server/src/index.ts` — bootServer factory + `import.meta.main` guard - `server/src/api-host-supervisors-fallback.test.ts` — use seam - `server/src/uploads-http.test.ts` — use seam - `server/scripts/test-runner.ts` — new (Bun-authored) - `server/scripts/test-manifest.ts` — new - `server/scripts/test-runner-self.test.ts` — new - `server/package.json` — canonical test → runner; test:raw diagnostic - `docs/tests/aggregate-runner.md` — new SOP doc Zero touches to `db-adapter.ts` — #435 stays on its own branch/PR. #436 (`security/435-…`) is not cherry-picked; docs may link #435/#436 but do not assume they have merged. Refs: #434 (this), #435 (DATABASE_URL guard, related), #428 (RFC).
vansin
commented
Jul 12, 2026
vansin
left a comment
Contributor
Author
There was a problem hiding this comment.
Independent checkpoint: FAIL — keep Draft; do not Ready/merge.
The green baseline is real: I reproduced canonical 526 pass / 0 fail / 1555 expects, including a parent fake-prod DATABASE_URL. OS-ephemeral port binding, normal child failure propagation, and manifest mismatch rejection work.
Blocking findings at 6260213:
- P0 — published CLI no longer starts the server.
server/bin/commhub.tsonly dynamically importsindex.js; the newif (import.meta.main)inindex.tsis therefore false on the officialcommhub-serverpath. Real run: DB initializes, process stays alive, but health is connection-refused and strace records 0 bind/0 listen. Introduce one explicit production lifecycle called by both direct-index and bin entrypoints, and add a real bin→health smoke. - P0 —
test:rawstill inheritsDATABASE_URL. With a fake PG URL it selects PostgreSQL ([commhub] database: PostgreSQL); this machine only avoided a connection becausepgis absent. Diagnostic mode is not a DB-safety bypass. Raw must also unconditionally removeDATABASE_URL, forceNODE_ENV=test, and use a throwawayCOMMHUB_DB. - P1 — signal handling can green-wash/interminably leak. The handler only forwards. A child that exits 0 on SIGTERM yields runner exit 0 with
0/0/0; a child that ignores the signal leaves runner, child, andanet-434-*temp dir alive. Add first-signal state, stop scheduling, force overall nonzero even if child exits 0, bounded grace→kill, and finally cleanup; lock both exit-0 and ignore-signal counterexamples. - P1 — env self-test tests a copy, not the runner. It duplicates allowlist/env construction. Deliberately regressing the real runner still leaves the self-test green. Exercise a nested real runner/probe or one import-safe shared implementation.
- P1 — #435 integration currently fails closed. Overlaying verified #435 HEAD
30e811amakes the manifest rejectsrc/db-adapter-guard.test.tsbefore tests run. Do not weaken set equality; rebase after #435 and register that suite, then rerun 3×. - P1/P2 — lifecycle/fixture ownership is incomplete. Top-level console replacement plus timers at
index.ts:102and:503remain outside the lifecycle, so import-only never exits andbootServer().stop()cannot clean them. The two real-server tests also statically open DB before setting their test-localSERVER_DB, so that path is unused and cleanup targets the wrong directory; safety currently comes from runner child env.
Please deliver one corrective commit with raw evidence for the counterexamples above. No merge/deploy authorization is implied.
This was referenced Jul 12, 2026
s2agi
pushed a commit
that referenced
this pull request
Jul 12, 2026
This was referenced Jul 12, 2026
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.
Fixes #434 — clears the baseline 8 aggregate failures (
api-host-supervisors-fallback) that were caused by module-singleton / port / DB conflicts underbun test src/.Draft — do not merge. Reviewers: 通信龙 (lead) + 副指挥 (coordinator, independent verification). Author does not self-approve.
Independent of
#436/security/435-…. This branch does not touchserver/src/db-adapter.tsand does not cherry-pick#435— docs may cross-reference#435but do not assume it has merged.What changed
Minimal production seam + Bun-authored aggregate runner + 2-suite refactor.
Production seam —
server/src/index.tsBun.serve({...})call intoexport function bootServer(opts?: { port?: number; hostname?: string })returning the realBun.Server.opts.port ?? PORTandopts.hostname ?? HOSTuse??on purpose —||would swallow a legitimate0(副指挥 硬点 1).if (import.meta.main) { … }. Productionbun run src/index.tsstill boots identically.${server.hostname}:${server.port}— effective bind. Kernel-assigned ports show through cleanly.Bun.serveobject literal — the diff stays surgical (reviewable diffguarantee).Sanity:
bun run src/index.tsboots with correct banner and effective port;bootServer({ port: 0 })from a test binds an OS-assigned ephemeral port and returns it viaserver.port.Test refactor (2 files,
isolated_serverkind)src/api-host-supervisors-fallback.test.ts— dropPORT = 18000 + Math.random() * 1000+process.env.PORT; callbootServer({ port: 0, hostname: "127.0.0.1" })inbeforeAll; finalizeBASEfromserver.port; callserver.stop(true)inafterAll; sweep DB dir (WAL + SHM together).src/uploads-http.test.ts— same pattern.Individual results:
api-host-supervisors-fallback.test.tsuploads-http.test.tsAggregate runner —
server/scripts/test-runner.ts+test-manifest.ts--concurrency=Nflag ships here (副指挥 硬点 3).--suite=<manifest-path>(exact match only) and--verbose. No glob patterns — manifest is source of truth (硬点 4).ISOLATED_SERVER_SUITES+SHARED_UNIT_SUITEScover every*.test.tsunderserver/src/andserver/scripts/. On start the runner:src/stray.test.tson disk gives❌ manifest / disk mismatch (issue #434 rule 4): unregistered on disk: src/stray-manifest-canary.test.tsand exit 2.PATH,TMPDIR,TERM,LANG,LC_ALL,LC_CTYPE,CI,GITHUB_ACTIONS,SHELL,USER,LOGNAME). Everything else dropped.DATABASE_URLdeleted twice (after allowlist copy, after suite overrides) — belt+braces on top of#435's guard.mkdtempSyncforHOME,COMMHUB_DB,COMMHUB_UPLOADS_DIR,TMPDIR. On exit,rmSyncthe whole dir (sweepsdb,db-wal,db-shmin one shot).NODE_ENV=testforced.stdout/stderrcontinuously drained (no buffered deadlock);--verbosestreams live, otherwise a tail is printed on failure.Runner self-test —
scripts/test-runner-self.test.tsDATABASE_URLon the parent, asserts the child seesDATABASE_URL=UNSET. Proves the runner's env-shaping keeps the invariant regardless of what the shell / CI has leaked (硬点 9 / security(test): reject inherited DATABASE_URL before adapter construction #435 defense-in-depth).NODE_ENV="test"smoke.package.json"test": "bun run scripts/test-runner.ts"— canonical gate."test:raw": "…stderr warning… && bun test src/"— diagnostic-only. Running it prints:docs/tests/aggregate-runner.mdShort operator SOP: canonical command, manifest rule,
test:rawwarning, reproducibility check, cross-refs to#435/#436.Reproducibility — 3× consecutive canonical runs
Every run identical:
526 pass = 515 baseline + 8 tests newly-reaching (previously counted as fails) + 3 runner-self tests.1555 expects = 1518 baseline + 30 + 26 + 7 runner-self + additional expects from newly-reaching assertions in api-host-supervisors-fallback. Zero fail, zero ConnectionRefused, zero DB guard trip, zero port collision.Acceptance checklist (issue #434)
mkdtempSync; test refactor usesserver.port)Alignment with the 10 hardline constraints
origin/maind418862; not cherry-picked from security(test): reject inherited DATABASE_URL before adapter construction #435.db-adapter.tsuntouched.testscript runs the Bun-authored runner; explicitdelete env.DATABASE_URL; independentCOMMHUB_DB/HOME/TMPDIRper child.test:rawdiagnostic-only + loud stderr warning.port: 0inside the child viabootServer({ port: 0 }); parent never picks a random slot.mkdtempDB per child;rmSync recursivecleansdb/db-wal/db-shm.concurrency=1); 3× identical; zero fail.DATABASE_URL; runner-self test proves child sees UNSET even when parent leaks fake-prod.Scope constraints
#435/#436untouched. This branch never editsdb-adapter.ts.