Skip to content

ci: run the Node app test suites in the required diagnostics gate - #1124

Open
mdheller wants to merge 2 commits into
mainfrom
ci/wire-apps-tests
Open

ci: run the Node app test suites in the required diagnostics gate#1124
mdheller wants to merge 2 commits into
mainfrom
ci/wire-apps-tests

Conversation

@mdheller

Copy link
Copy Markdown
Member

What this does

The apps/* suites that use the Node built-in test runner (node --test) were never executed by CI. The only workflow that referenced them is images.yml (it builds their containers), and the Dockerfiles run npm install, never npm test. So the health-twin PHI-path exposure / auth-gate / consult-cap tests — and the sibling apps' suites — ran on nobody's PR.

This wires them into the already-required gate.

The protection finding (why placement was the whole problem)

So the only non-colliding, actually-blocking route is a matrix leg inside an already-required job. This adds node-app-suites to the existing app-test-diagnostics job (already in diagnostics-gate.needs). Coverage rides the single required context; no new job; the gate and its needs: are untouched.

merge-tree (this branch × contested PRs): both CLEAN.

One leg, all apps, hard timeout

.github/app-ci/run-node-app-tests.sh (kept out of tools/): discovers every apps/* with a node --test script and runs them in one leg (Actions is spend-capped), each under a hard per-app timeout so a hang is RED (exit 124), never a silently-cancelled job. It fails on zero matches and refuses to pass if everything is quarantined.

Sibling inventory (measured locally, direct exit codes)

app test script status
health-twin node --test PASS (5) — now runs
hellgraph-service node --test PASS (94) — now runs
lifecycle-warden node --test PASS (27) — now runs
commons-search node --test HANG (124) — quarantined
search-gateway node --test HANG (124) — quarantined
socioprophet-web vitest run other harness — reported, not run here (covered by build-web; wants its own vitest job)
academy-board / portfolio-agent / synapse-bridge none no test script

The two hangs (root-caused, not hidden)

commons-search and search-gateway src/server.test.ts: all assertions pass, then the process can't exit. Several tests await fetch(...) (the DELETE calls, the rate-cap loops) without consuming the response body, so undici keeps the HTTP/1.1 keep-alive sockets open and after(() => server.close()) never completes. hellgraph passes because it consumes every body. One-line fix owned by each app lane: server.closeAllConnections() in the after() hook.

They are quarantined in .github/app-ci/quarantine.jsonnamed, with root cause + fix, and shrink-only: the runner asserts len(quarantined) <= ceiling (currently 2) on every run and fails if it grows; stale entries (app/test gone) also fail, forcing pruning. This is a quarantine, not a silent skip.

Proof (local; not pushed to watch CI)

Runner exit codes, direct capture:

  1. GREEN baseline → 0 (3 run + pass, 2 quarantined, socioprophet-web reported).
  2. Inject a failing test (throwaway app) → 1 (RED).
  3. Inject a hanging test, PER_APP_TIMEOUT=51 via exit 124, whole run bounded at 9s (proves a hang is RED, never a hang).
  4. Remove throwaway → GREEN 0 restored, tree clean.
  5. Zero-match glob → 1 (refuses to pass on nothing).
  6. Grow quarantine past ceiling → 1 (shrink invariant holds).

Out of scope / not touched

apps/health-twin/src/**, gitops-promote.yml, images.yml, infra/**, tools/, ci.yml, Makefile.

DO NOT MERGE pending review.

The apps/* suites that use the Node built-in test runner (health-twin,
hellgraph-service, lifecycle-warden, commons-search, search-gateway) were
never executed by CI — only images.yml built their containers, and the
Dockerfiles run npm install, never npm test. So the PHI-path exposure and
auth-gate tests never ran on any PR.

Wire them in as a matrix leg (node-app-suites) of the already-required
app-test-diagnostics job, so coverage rides the single required context
(diagnostics-gate) without adding a new job or touching the gate's needs:.
One leg iterates every node --test app (Actions is spend-capped); each app
runs under a hard per-app timeout so a hang is RED (exit 124), never a
silently cancelled job.

commons-search and search-gateway hang at teardown (unconsumed fetch bodies
hold undici keep-alive sockets open so server.close() never completes); they
are quarantined in a named, shrink-only allowlist with the root cause and the
one-line fix, asserted <= ceiling on every run. The leg fails on zero matches
and refuses to pass if everything is quarantined.
Copilot AI review requested due to automatic review settings July 30, 2026 08:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Integrates previously unrun apps/* Node built-in test runner suites (node --test) into the already-required diagnostics-gate by adding a new matrix leg, plus a shared runner script and quarantine file to handle known hangs with enforced limits.

Changes:

  • Add a node-app-suites matrix leg to the existing required diagnostics job.
  • Introduce a CI script to discover and run all apps/* using node --test with per-app hard timeouts.
  • Add a quarantine allowlist JSON for known hanging suites with a ceiling concept.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
.github/workflows/validate-target-diagnostics.yml Adds the node-app-suites matrix entry and sets up Node for that leg.
.github/app-ci/run-node-app-tests.sh Discovers apps/* with node --test, applies quarantine rules, runs each suite with a hard timeout.
.github/app-ci/quarantine.json Defines quarantined apps and a ceiling value used by the runner script.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/app-ci/run-node-app-tests.sh Outdated
Comment on lines +89 to +95
# Invariant 2: quarantine may only shrink. The ceiling is a ratchet that only goes down.
if [ "${#Q_APPS[@]}" -gt "$CEILING" ]; then
echo "FATAL: quarantine holds ${#Q_APPS[@]} app(s) but the ceiling is $CEILING."
echo " The ceiling may ONLY be lowered. Adding an app means raising it, which"
echo " a reviewer must consciously approve — that friction is the point."
exit 1
fi

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid and fixed in 5d78d11. You're right that len(apps) <= ceiling within one file can't stop an add-app and raise-ceiling landing in the same diff. Added Invariant 2b: diff against the origin/main baseline (fetched shallow when the checkout lacks it) — the ceiling may not rise and no new app key may appear; a newly-failing suite must be fixed, not quarantined. Proven locally: baseline {ceiling:1, apps:{a}} vs a change to {ceiling:2, apps:{a,b}} now exits RED (ceiling raised 1 -> 2; new quarantined app(s): b). This leg's introduction (no baseline yet) is printed, never silently skipped.

Comment on lines +36 to +44
read_test_script() {
node -e '
const fs=require("fs"), path=require("path");
try {
const p=JSON.parse(fs.readFileSync(path.resolve(process.argv[1]),"utf8"));
process.stdout.write(((p.scripts&&p.scripts.test)||""));
} catch(e) { process.stdout.write(""); }
' "$1"
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid and fixed in 5d78d11 — this was precisely the silent omission the leg exists to prevent. read_test_script now exits 3 with a reason on an unreadable/unparseable package.json (a valid manifest that simply declares no test still returns empty, as before), and both discovery and the stale-check fail RED on it. Proven: a broken package.json now exits 1 rather than being dropped as 'no tests'.

Comment on lines +146 to +147
npm install --no-audit --no-fund
timeout "$PER_APP_TIMEOUT" npm test
Comment on lines 197 to +204
- name: Create virtualenv
working-directory: ${{ matrix.working-directory }}
run: python3 -m venv .venv
- name: Setup Node
if: matrix.name == 'node-app-suites'
uses: actions/setup-node@v4
with:
node-version: '22'
…en manifest is RED

Two silent-omission holes in node-app-suites — the exact failure mode this leg exists to stop:
- len(apps)<=ceiling in one file cannot stop a PR that adds an app AND raises the
  ceiling together. Add Invariant 2b: diff against the origin/main baseline (fetched
  shallow if absent) — the ceiling may not rise and no new app may appear. A
  newly-failing suite must be fixed, not quarantined. No baseline yet (this leg's
  introduction) -> printed, never silently skipped. Seams: QUARANTINE_BASELINE_REF/FILE.
- read_test_script swallowed read/parse errors and returned "", so a broken
  package.json was treated as 'no tests' and silently dropped. It now exits 3 with a
  reason; discovery and the stale-check fail RED on an unreadable/unparseable manifest.
Addresses Copilot inline on #1124.
Copilot AI review requested due to automatic review settings July 30, 2026 17:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

.github/workflows/validate-target-diagnostics.yml:204

  • The Create virtualenv step appears to run for the node-app-suites leg where it isn’t needed, and with working-directory: . it will create .venv at the repo root. Consider adding an if: matrix.name != 'node-app-suites' guard (or splitting language-specific setup) to avoid unnecessary work and potential cross-leg side effects in this required CI gate.
      - name: Create virtualenv
        working-directory: ${{ matrix.working-directory }}
        run: python3 -m venv .venv
      - name: Setup Node
        if: matrix.name == 'node-app-suites'
        uses: actions/setup-node@v4
        with:
          node-version: '22'

.github/app-ci/run-node-app-tests.sh:79

  • The node --test detection pattern is currently redundant and can miss legitimate variants (e.g., multiple spaces like node --test, or other formatting). This can incorrectly classify node --test suites as OTHER_APPS and silently stop running them in this leg. Use a single, clear match such as a regex on node[[:space:]]+--test (or a more robust parse) to reliably detect the built-in runner.
  [ -z "$script" ] && continue
  if [[ "$script" == *"node "*"--test"* || "$script" == *"node --test"* ]]; then
    NODE_APPS+=("$app")
  else
    OTHER_APPS+=("$app  [${script}]")
  fi

.github/app-ci/run-node-app-tests.sh:182

  • Quarantine validation checks that the app exists and has a test script, but doesn’t ensure the quarantined app is actually in the node --test discovery set. If an app switches harness (e.g., to vitest) it could remain quarantined forever even though this leg will never run it anyway, undermining the intended shrink pressure and correctness of the quarantine list. Consider additionally asserting that each quarantined app is a member of NODE_APPS (i.e., still a node --test suite), otherwise fail and force pruning + ceiling lowering.
# Invariant 3: no stale quarantine entries.
for qa in "${Q_APPS[@]}"; do
  qpj="apps/$qa/package.json"
  if [ ! -f "$qpj" ]; then
    echo "FATAL: quarantined app '$qa' no longer exists ($qpj is missing)."
    echo "       Remove it from $QUARANTINE_FILE AND lower the ceiling."
    exit 1
  fi
  qscript="$(read_test_script "$qpj")"; qrc=$?
  if [ "$qrc" -ne 0 ]; then
    echo "FATAL: quarantined app '$qa' has an unreadable/unparseable $qpj."
    echo "       A broken quarantined app must be fixed, not waved through."
    exit 1
  fi
  if [ -z "$qscript" ]; then
    echo "FATAL: quarantined app '$qa' no longer declares a test script."
    echo "       Remove it from $QUARANTINE_FILE AND lower the ceiling."
    exit 1
  fi
done

.github/app-ci/run-node-app-tests.sh:133

  • The quarantine ratchet is currently 'best-effort' and fails open: if fetching/reading the baseline file fails, BASELINE_JSON becomes empty and the script skips the shrink-only enforcement. Since this leg is intended to mechanically prevent widening quarantine, it’s safer to fail closed once the baseline quarantine exists (e.g., if the baseline ref can’t be retrieved/read, exit non-zero with a clear error) so the ratchet can’t be bypassed by transient git/checkout constraints.
  # Shallow PR checkouts don't carry origin/main; fetch it best-effort before reading.
  if ! git cat-file -e "$BASELINE_REF:.github/app-ci/quarantine.json" 2>/dev/null; then
    git fetch --no-tags --depth=1 origin main >/dev/null 2>&1 || true
  fi
  BASELINE_JSON="$(git show "$BASELINE_REF:.github/app-ci/quarantine.json" 2>/dev/null || true)"
fi

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.

2 participants