Files
gstack/scripts/test-health-report.ts
T
Garry Tanandcapy-ai[bot] a889ef9393 v1.91.105.0 fix(tests): the free suite stops needing its retry pass; test:health tells the truth (#3154) (#3212)
* test: turn git auto-maintenance off in free fixtures, with a runner backstop

git 2.29+ starts a detached `git maintenance run --auto` after every commit;
it creates and deletes .git/objects/maintenance.lock after the commit returns.
Diagnosed on 47d639a (git 2.55.0):

- gstack-docs-candidate (ledger 37742176952/16): 4/150 replicated snapshots
  saw the .git listing change, every one `-objects/maintenance.lock`; 0/150
  with the keys. repo() now sets maintenance.auto=false and gc.auto=0.
- plan-design-sdk-fixture (ledger 37807608021/6): under commit load 18/150
  runs failed the collector check; in every one Bun's rmSync(recursive, force)
  returned without throwing and left the scratch repo behind. 0/100 with the
  keys in the fixture's replacement git env, which never sees the runner's.
- qa-functional-observer (ledger 37800857251/4): under load 6/200 observer
  setups threw ENOENT lstat .git/objects/maintenance.lock; 0/400 with the keys
  in the inherited env. The observer and its fixture (22 paid cases) are not
  edited; the runner backstop covers them.

runFreeShard appends the two keys after any inherited GIT_CONFIG_* pairs of
each shard child (a malformed inherited count is left for git to reject).
A GIT_TRACE commit shows 3 maintenance lines with defaults and 0 in the
runner child env; the new test pins that and the env merge cases.
Failure messages now name the changed .git entries and surviving fixture
directories; no assertion changed.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: synchronize the zombie publication-lock test on a framed PID line

Ledger 38153371796/3 and 38161906980/3: line 69 `existsSync(ready)` failed.
The test polled 300 x 10 ms for the Python parent's readiness file, then
another 300 x 10 ms of /proc for state Z; under shard load the fixed count
expired. existsSync also raced the write (an empty file read as pid 0).

The parent now waits for its child with os.waitid(P_PID, WEXITED|WNOWAIT),
which leaves it unreaped in state Z, and prints the PID with flush=True. The
test reads exactly one newline-framed line from the parent's stdout under the
test's own (unchanged) 10 s timeout, rejects a malformed or non-positive PID,
and makes one /proc read that must show Z. Early exit, a malformed line or a
timeout reports the parent's exit status and stderr tail; the owned parent is
always killed in cleanup.

Intentional assertion replacement: the readiness-file existence check and the
polling loop are replaced by the framed-line requirement (a positive PID) and
one /proc read; `expect(state).toBe('Z')` and every lock-recovery assertion
are unchanged.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: stop the next-version smoke test from calling live GitHub

Ledger 37804770972/9: "CLI runs against real repo and emits parseable JSON"
failed with `JSON Parse error: Unexpected EOF` after 30032 ms. The smoke test
ran the real CLI against live `gh`; bin/gstack-next-version's per-PR
`gh api` calls run one after another (spawnSync inside its async workers), so
27 open PRs passed the 30 s spawn timeout and stdout was empty. Measured with
a delayed fake gh: 8 PRs x 0.5 s = 4108 ms; 27 PRs x 1.2 s = 32637 ms. The
CLI defect is filed as a P1 follow-up, not fixed here.

The test keeps the real repository and the real CLI and puts a named fake gh
first on PATH. It logs every argv, answers exactly the calls the CLI makes
(auth status only when origin is not github.com, pr view exit 1, canned pr
list, repo view and contents with one same-owner PR claiming 1.6.4.0) and
exits 2 with a FAKE_GH_UNHANDLED marker for anything else; glab exits 1. The
test now checks the exit code with stderr before JSON.parse, the exact call
set, no marker, no glab call, host github, offline false, no fallback, no
warnings, the canned claim, and version 1.6.5.0. Every existing assertion is
kept; the spawn timeout is unchanged.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(test-health): report the Windows wall over PR runs and name the seed's real prerequisite

free-wall read windows-free-tests.yml with `branch: 'main'`, but that
workflow runs only on pull requests and workflow_dispatch, so the "main" runs
it averaged were dispatch duration recordings. It now uses listWorkflowRuns'
existing `event: 'pull_request'` filter and prints the population inline:
`windows-free-tests (successful PR runs) 7.1 min (23 runs)`, or `no
successful PR runs in window`. The free-tests half is byte-identical.

The CEO-10 fix text stops offering a "CI recording" (none exists for the Linux
seed) and names UBICLOUD_API_KEY; test:ubicloud --record-durations copies the
seed back into the checkout, and contributors without the key leave the
refresh to a maintainer. docs/TESTING_INTERNALS.md drops "or a CI recording"
in the same change.

Intentional pin change: test/test-health-report.test.ts free-wall value
`windows-free-tests 0.0 min (1 runs)` -> `windows-free-tests (successful PR
runs) 0.0 min (1 runs)`; the fixture now carries a PR run and an excluded
dispatch run, plus a new case for the empty and mixed populations.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(timeline-read): reject missing and unparseable --since/--branch values

`--since` and `--branch` read an unbound $2 under `set -u` and crashed with
exit 1 when given no value, and an unparseable --since ("yesterday", "2
weeks") silently returned unfiltered history because the Bun regex was
unanchored and fell through. Unknown arguments were silently ignored.

--since is now parsed once in bash (no `date -d`, no bash-4 features) into a
cutoff in epoch ms, and the Bun regex is deleted. Accepted forms: "<N>
minute|hour|day|week|month[s] ago" (case-insensitive, as before), "<N>h|d|w"
(the form gstack-global-discover documents) and ISO dates YYYY-MM-DD or
YYYY-MM-DDTHH:MM:SSZ (UTC). In #1723's shape, before any read:

  --since must be a relative time like "7 days ago" (got no value); accepted: ...
  --since must be a relative time like "7 days ago" (got 'yesterday'); accepted: ...
  --branch must be a branch name (got flag '--limit')

with "got an empty value" for ''. Unknown arguments exit 2 with the usage
line; --help/-h prints it and exits 0. Tests extend the --limit table in
test/timeline.test.ts and run every case on empty and populated history,
plus each accepted --since form filtering old events; the quoted-branch and
no-filter tests are unchanged.

Behavior change: unrecognized arguments (previously ignored) now exit 2.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: record the due audit follow-ups with verdicts, and file wave-2 follow-ups

docs/test-audit-2026-10.md gains a dated 2026-10-15 section from a no-spend
`bun run test:health --since-days 7`, with the 2026-10-12 figures from #3154
kept as history and a verdict per target: full-fallback 45.9% (open; the
audit's next step is touchfile mappings for bin/, lib/, hosts/, make-pdf/),
spend per merged PR $73.52 (verdict unavailable: the pre-change baseline,
recomputed with collectHealth at now=2026-10-04 behind an upper-bound client,
holds no trial-outcomes artifacts because the recorder shipped with the
audit), cancelled slice-minutes 1,545/week, push-to-verdict p50 11.4 min
(missed against 3-5 min), flaky-pass 6.1% (missed; this wave repairs it).
Census reliability is not due until 2026-10-26; the interim figure is
recorded and the repair stays with #3153/#3200.

TODOS.md: one appended dated section (P1 gstack-next-version serial fetch with
the 4160 ms measurement, P1 standing seed recorder, P1 full-fallback/spend,
P1 dated flake follow-ups, P2 createQAFunctionalFixture maintenance keys, P2
diagnostic-mode HOME/state-root artifacts). The timeline-read bullet was
deleted with its fix.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore(release): tentative stamp v1.91.105.0 with its CHANGELOG entry

Stamp-at-merge policy (.gstack/ship-policy.json): the branch carries a
tentative PATCH stamp and its CHANGELOG section directly under
`# Changelog`; the queue restamps at merge time. Base 47d639a is 1.91.101.0,
but the live slot scan shows 1.91.102.0 (#3190), 1.91.103.0 (#3200) and
1.91.104.0 (#3209) claimed, so this branch takes 1.91.105.0. Written with
`gstack-restamp --version 1.91.105.0 --base main --expect-base 47d639a
--allow-repo-commands` (VERSION, package.json's npm translation, the agents
digest via the policy's release tool, and the CHANGELOG header).

* test(seed): refresh the free-suite duration seed from a Ubicloud recording (#3154)

Recorded with `bun run test:ubicloud --record-durations` (standard-16,
eu-central-h1) on this branch, which equals origin/main 47d639a plus this
wave; the recorder exited 0 and wrote 1219 durations (1055 before, 164 new,
none removed; serial 2439 s -> 3202 s). `test:health` CEO-10 now reports
0 unseeded files.

Two earlier recordings did not count: one failed on an unrelated fixed-sleep
race in test/plan-seed-submission.test.ts (filed P2, not edited here), the
other failed free-seed-ratchet because it ran against the first attempt's
pulled seed. No file was excluded.

free-seed-ratchet: three files are over the 60 s ceiling in this recording
and are allowlisted with a split todo (setup-install-registry 82 s,
host-renders-prune 64 s, setup-codex-scope 60.4 s); the split follow-up and
the plan-seed-submission race are filed in the appended TODOS section.

The ratchet test now names scripts/free-test-seed-allowlist.json literally
(a pin on SEED_ALLOWLIST_FILE): pregate's lanes check maps data files to
tests through string literals, and the allowlist had no lane
(PREGATE_NO_LANE). Additive assertion; nothing weakened.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: give the git-maintenance backstop test a Windows-safe empty global config

windows-free-shard (2) on #3212 (run 38832498861) failed two cases in
test/test-free-shards-git-maintenance.test.ts, both serially reproduced by
the retry pass: Git for Windows rejects GIT_CONFIG_GLOBAL=os.devNull
(`fatal: unable to access '\\.\nul': Invalid argument`), so the malformed-count
case saw that error instead of git's GIT_CONFIG_COUNT rejection, and the traced
commit's `git init` exited nonzero. Point GIT_CONFIG_GLOBAL at a file that does
not exist in the scratch HOME, as test/ceo-plan-mode-fixture.test.ts does;
git reads a missing global config as empty on every platform. No assertion
changed.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-16 13:29:38 -07:00

411 lines
26 KiB
TypeScript

#!/usr/bin/env bun
/**
* test:health — the audit's success metrics as one command, read from CI
* history (scripts/lib/ci-history.ts, the same reader pass-rates uses) plus
* the local tree. Every metric prints its counting rule; a metric whose input
* cannot be read prints "unavailable: <reason>; next: <step>", never 0.
*
* Enforced checks (`--enforce`): a free test file that flakes in more than 5%
* of main free-tests runs over a window of at least 20 runs (W7a), and more
* than 5 free test files missing from scripts/free-test-durations.json
* (CEO-10). Without `--enforce` the command always exits 0; an unavailable
* check never fails it. Needs `gh` with read access to the repository.
*
* Usage:
* bun run test:health [--since-days 7] [--json] [--enforce] [--summary <file>]
* [--repo owner/name] [--complexity-base <ref>]
* bun run test:health flake-summary <dir> # job-summary table of downloaded flake-ledger-* artifacts
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { GH_CLIENT, downloadRunArtifacts, formatFlakeSummary, historyFetcher, listRunJobs, listWorkflowRuns, readCiFlakeLedgers,
readFlakeLedgerDir, repoSlug, unavailableReason, type CiRun, type FlakeWindow, type GhClient } from './lib/ci-history';
import { analyzePassRates, readTrialOutcomeDir, type TrialRecord } from './eval-flake-rank';
import { collectFreeTestFiles, loadFreeTestDurations, unseededFreeFiles } from './test-free-shards';
import { EVAL_POLICY } from '../test/helpers/periodic-exclude-data';
import { resolveStateRoot } from '../lib/state-root';
const ROOT = path.resolve(import.meta.dir, '..');
export const UNSEEDED_LIMIT = 5;
export const COMPLEXITY_PATHS = ['scripts', 'test/helpers', '.github/workflows'] as const;
export interface Metric { id: string; title: string; rule: string; value?: string; detail?: string[]; unavailable?: string }
export interface Check { id: string; title: string; status: 'pass' | 'fail' | 'unavailable'; detail: string; fix: string }
export type IssueAction = 'upsert' | 'keep-open' | 'close';
export interface HealthReport { repo: string; sinceDays: number; generatedAt: string; metrics: Metric[]; checks: Check[]; issueAction: IssueAction }
export interface HealthInputs {
client: GhClient;
repo: string;
now: number;
sinceDays: number;
cacheDir: string;
root?: string;
complexityBase?: string;
}
// --- Pure arithmetic ---
export function percentile(values: number[], p: number): number | null {
if (values.length === 0) return null;
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(p * sorted.length) - 1))]!;
}
function choose(n: number, k: number): number {
let out = 1;
for (let i = 1; i <= k; i++) out = (out * (n - k + i)) / i;
return out;
}
/** P(fewer than k of n independent trials pass) when one trial fails with probability `failRate`. */
export function panelRedProbability(failRate: number, n: number, k: number): number {
let red = 0;
for (let passes = 0; passes < k; passes++) red += choose(n, passes) * (1 - failRate) ** passes * failRate ** (n - passes);
return red;
}
export interface CensusStats {
runs: number;
trials: number;
failed: number;
byKind: Record<string, { trials: number; failed: number }>;
byClass: Record<string, number>;
/** P(lane red) with each case's pooled trial failure rate, under the recorded panels. */
redInForce: number;
/** The same evidence with every case judged on one trial. */
redSingleTrial: number;
}
/**
* CEO-06/22 census arithmetic over one lane's trial records. Every observed
* failure is treated as nondeterminism, so both red probabilities are upper
* bounds for known-good inputs; they are reported separately from the raw
* trial failure rate so a verdict-policy change cannot read as a reliability gain.
*/
export function censusStats(records: TrialRecord[], runIds: string[]): CensusStats {
const scored = records.filter(record => record.outcome !== 'skipped');
const byKind: CensusStats['byKind'] = {};
const byClass: Record<string, number> = {};
const perCase = new Map<string, { trials: number; failed: number; n: number; k: number }>();
for (const record of scored) {
const failed = record.outcome === 'failed';
const kind = (byKind[record.kind] ??= { trials: 0, failed: 0 });
kind.trials += 1;
if (failed) { kind.failed += 1; byClass[record.failure_class ?? 'unknown'] = (byClass[record.failure_class ?? 'unknown'] ?? 0) + 1; }
const slot = perCase.get(record.case) ?? { trials: 0, failed: 0, n: 1, k: 1 };
slot.trials += 1;
if (failed) slot.failed += 1;
if (record.panel.n > slot.n) { slot.n = record.panel.n; slot.k = record.panel.k; }
perCase.set(record.case, slot);
}
let greenInForce = 1, greenSingle = 1;
for (const slot of perCase.values()) {
const p = slot.failed / slot.trials;
greenInForce *= 1 - panelRedProbability(p, slot.n, slot.k);
greenSingle *= 1 - p;
}
return { runs: runIds.length, trials: scored.length, failed: scored.filter(record => record.outcome === 'failed').length,
byKind, byClass, redInForce: 1 - greenInForce, redSingleTrial: 1 - greenSingle };
}
const pct = (value: number) => `${(Math.round(value * 1000) / 10).toFixed(1)}%`;
const minutes = (ms: number | null) => ms === null ? 'n/a' : `${(ms / 60_000).toFixed(1)} min`;
const span = (from: string | null, to: string | null) => from && to ? Math.max(0, Date.parse(to) - Date.parse(from)) : null;
/** DX-9: close only when every enforced check has evidence and passes; missing evidence keeps the issue open. */
export function issueActionFor(checks: Check[]): IssueAction {
if (checks.some(check => check.status === 'fail')) return 'upsert';
if (checks.some(check => check.status === 'unavailable')) return 'keep-open';
return 'close';
}
// --- Checks ---
export function flakeCheck(window: FlakeWindow | Error): Check {
const base = { id: 'flaky-files', title: 'Free-suite flaky files (W7a)',
fix: 'Fix the race at source (docs/TESTING_INTERNALS.md#flake-ledger); never raise the timeout or add a retry. Reproduce with `bun test <file>` under load.' };
if (window instanceof Error) return { ...base, status: 'unavailable', detail: `unavailable: ${unavailableReason(window)}` };
const failing = window.files.filter(file => file.failing);
if (!window.enforceable) {
return { ...base, status: 'pass', detail: `report-only: ${window.runs} main run(s) since ${window.since.slice(0, 10)} (< 20 needed to enforce)` };
}
if (failing.length === 0) return { ...base, status: 'pass', detail: `no file above 5% over ${window.runs} main runs` };
return { ...base, status: 'fail',
detail: failing.map(file => `${file.file}: flaky in ${file.flakyRuns}/${window.runs} main runs (${pct(file.rate)} > 5%)`).join('; ') };
}
export function unseededCheck(root: string): Check {
const base = { id: 'unseeded-free-files', title: 'Free test files missing from the duration seed (CEO-10)',
fix: 'Refresh the seed with UBICLOUD_API_KEY set: `bun run test:ubicloud --record-durations` times every free file on a CI-mirrored VM and copies scripts/free-test-durations.json back into this checkout; commit it (docs/TESTING_INTERNALS.md#free-suite-duration-seed). Without UBICLOUD_API_KEY, leave the refresh to a maintainer: a 4-core laptop or sandbox recording skips or slows browser and display tests, so it is not accepted.' };
const durations = loadFreeTestDurations(root);
if (!durations) return { ...base, status: 'unavailable', detail: 'unavailable: scripts/free-test-durations.json is missing or unreadable; next: refresh the seed' };
const unseeded = unseededFreeFiles(collectFreeTestFiles(root), durations);
const shown = unseeded.slice(0, 10).join(', ') + (unseeded.length > 10 ? `, +${unseeded.length - 10} more` : '');
if (unseeded.length > UNSEEDED_LIMIT) return { ...base, status: 'fail', detail: `stale seed: ${unseeded.length} unseeded file(s) > ${UNSEEDED_LIMIT}: ${shown}` };
return { ...base, status: 'pass', detail: `${unseeded.length} unseeded file(s) (limit ${UNSEEDED_LIMIT})${unseeded.length ? `: ${shown}` : ''}` };
}
// --- Metric collection ---
function attempt<T>(fn: () => T): T | Error {
try { return fn(); } catch (error) { return error instanceof Error ? error : new Error(String(error)); }
}
function metric(id: string, title: string, rule: string, compute: () => { value: string; detail?: string[] }): Metric {
const result = attempt(compute);
if (result instanceof Error) return { id, title, rule, unavailable: unavailableReason(result) };
return { id, title, rule, ...result };
}
function records(dirs: string[]): TrialRecord[] {
return dirs.flatMap(dir => readTrialOutcomeDir(dir).records);
}
function readJson(file: string): any {
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
/** Net LOC under COMPLEXITY_PATHS and the package-script count at one ref. */
export function complexityAt(root: string, ref: string): { loc: number; scripts: number } {
const grep = spawnSync('git', ['grep', '-c', '-I', '', ref, '--', ...COMPLEXITY_PATHS], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, timeout: 60_000 });
if (grep.status !== 0 && grep.status !== 1) throw new Error(`git grep ${ref} failed: ${grep.stderr.trim()}`);
const loc = grep.stdout.split('\n').filter(Boolean).reduce((sum, line) => sum + Number(line.slice(line.lastIndexOf(':') + 1) || 0), 0);
const pkg = spawnSync('git', ['show', `${ref}:package.json`], { cwd: root, encoding: 'utf8', timeout: 10_000 });
if (pkg.status !== 0) throw new Error(`git show ${ref}:package.json failed: ${pkg.stderr.trim()}`);
return { loc, scripts: Object.keys(JSON.parse(pkg.stdout).scripts ?? {}).length };
}
export function collectHealth(inputs: HealthInputs): HealthReport {
const { client, repo, now, sinceDays, cacheDir } = inputs;
const root = inputs.root ?? ROOT;
const since = new Date(now - sinceDays * 86_400_000).toISOString();
const fetcher = historyFetcher(client);
const artifactDirs = (run: CiRun, match: (name: string) => boolean) =>
downloadRunArtifacts({ repo, run, cacheDir, fetcher, match, maxBytes: 64 * 1024 * 1024 });
const prRuns = attempt(() => listWorkflowRuns(client, repo, 'evals.yml', { since, event: 'pull_request' }));
const need = <T>(value: T | Error): T => { if (value instanceof Error) throw value; return value; };
const prRecords = attempt(() => need(prRuns).filter(run => run.conclusion !== 'cancelled')
.map(run => ({ run, trials: records(artifactDirs(run, name => name.startsWith('trial-outcomes'))) })));
const metrics: Metric[] = [];
metrics.push(metric('pr-fallback-rate', 'PR full-fallback rate', 'share of evals.yml pull_request runs whose paid-plan manifest has prCoverage.mode = full-fallback', () => {
const modes = new Map<string, number>();
for (const run of need(prRuns)) {
const manifest = artifactDirs(run, name => name === 'paid-plan').flatMap(dir => [path.join(dir, 'manifest.json')]).find(file => fs.existsSync(file));
if (!manifest) continue;
const mode = String(readJson(manifest).prCoverage?.mode ?? 'none');
modes.set(mode, (modes.get(mode) ?? 0) + 1);
}
const total = [...modes.values()].reduce((a, b) => a + b, 0);
if (total === 0) throw new Error('no paid-plan manifests in the window');
return { value: `${pct((modes.get('full-fallback') ?? 0) / total)} (${modes.get('full-fallback') ?? 0}/${total} runs)`,
detail: [...modes].map(([mode, count]) => `${mode}: ${count}`) };
}));
metrics.push(metric('pr-reused-trials', 'Reused PR trial records', 'trial-outcomes records with execution = reused across non-cancelled PR runs', () => {
const all = need(prRecords).flatMap(entry => entry.trials);
const reused = all.filter(record => record.execution === 'reused').length;
return { value: `${reused} of ${all.length} trial records` };
}));
metrics.push(metric('pr-spend', 'Recorded PR-lane spend', 'sum of executed trial cost_usd in PR runs; per merged PR = / PRs merged in the window; per validated revision = / distinct head SHAs with a success or failure verdict', () => {
const entries = need(prRecords);
const spend = entries.flatMap(entry => entry.trials).filter(record => record.execution === 'executed')
.reduce((sum, record) => sum + record.cost_usd, 0);
const revisions = new Set(entries.filter(entry => entry.run.conclusion === 'success' || entry.run.conclusion === 'failure').map(entry => entry.run.sha)).size;
const merged = listMergedPulls(client, repo, since);
return { value: `$${spend.toFixed(2)} recorded`, detail: [
`per merged PR: ${merged ? `$${(spend / merged).toFixed(2)} (${merged} merged)` : 'n/a (0 merged)'}`,
`per validated revision: ${revisions ? `$${(spend / revisions).toFixed(2)} (${revisions} revisions)` : 'n/a'}`] };
}));
metrics.push(metric('cancelled-slice-minutes', 'Cancelled slice-minutes per week', 'wall minutes of eval-slices jobs in cancelled evals.yml PR runs, scaled to 7 days', () => {
const cancelled = need(prRuns).filter(run => run.conclusion === 'cancelled');
const total = cancelled.flatMap(run => listRunJobs(client, repo, run.id, run.attempt))
.filter(job => job.name.startsWith('eval-slices')).reduce((sum, job) => sum + (span(job.startedAt, job.completedAt) ?? 0), 0);
return { value: `${(total / 60_000 * 7 / sinceDays).toFixed(0)} min/week (${cancelled.length} cancelled run(s))` };
}));
metrics.push(metric('push-to-verdict', 'PR lane push-to-verdict', 'created_at to updated_at of evals.yml pull_request runs that reached success or failure (includes queue and debounce)', () => {
const walls = need(prRuns).filter(run => run.conclusion === 'success' || run.conclusion === 'failure').map(run => span(run.createdAt, run.updatedAt)!);
if (walls.length === 0) throw new Error('no PR run reached a verdict in the window');
return { value: `p50 ${minutes(percentile(walls, 0.5))}, p95 ${minutes(percentile(walls, 0.95))} (${walls.length} runs)` };
}));
metrics.push(...censusMetrics(inputs, artifactDirs));
const flakes = attempt(() => readCiFlakeLedgers({ client, repo, sinceDays, now, cacheDir }));
metrics.push(metric('free-flaky-pass-rate', 'Free-suite flaky-pass rate', 'share of main free-tests runs (success or failure) with at least one flaky-pass ledger entry; per-file rate = runs with that file / runs', () => {
const window = need(flakes);
return { value: `${pct(window.runs ? window.flakyRuns / window.runs : 0)} (${window.flakyRuns}/${window.runs} main runs since ${window.since.slice(0, 10)} needed the retry pass; ${window.files.length} file(s))`,
detail: window.files.slice(0, 15).map(file => `${file.file}: ${file.flakyRuns}/${window.runs} runs (${pct(file.rate)})${file.failing ? ' FAIL > 5%' : ''}`) };
}));
metrics.push(metric('free-wall', 'Free-suite and Windows wall', 'p50 of run_started_at to updated_at for successful main runs of free-tests.yml and successful pull_request runs of windows-free-tests.yml (its workflow_dispatch duration recordings excluded)', () => {
const walls = (workflow: string, filter: { branch?: string; event?: string }) =>
listWorkflowRuns(client, repo, workflow, { since, ...filter }).filter(run => run.conclusion === 'success').map(run => span(run.startedAt, run.updatedAt)!);
const free = walls('free-tests.yml', { branch: 'main' });
const windows = walls('windows-free-tests.yml', { event: 'pull_request' });
const windowsValue = windows.length ? `${minutes(percentile(windows, 0.5))} (${windows.length} runs)` : 'no successful PR runs in window';
return { value: `free-tests ${minutes(percentile(free, 0.5))} (${free.length} runs); windows-free-tests (successful PR runs) ${windowsValue}` };
}));
metrics.push(metric('free-serial', 'Free-suite serial time', 'sum of scripts/free-test-durations.json (the committed seed)', () => {
const durations = loadFreeTestDurations(root);
if (!durations) throw new Error('scripts/free-test-durations.json is missing or unreadable');
return { value: `${Math.round(Object.values(durations).reduce((a, b) => a + b, 0) / 1000)} s over ${Object.keys(durations).length} files` };
}));
metrics.push(metric('net-complexity', 'Net complexity (CEO-32, reported, no target)', `lines under ${COMPLEXITY_PATHS.join(', ')} and package.json script count, HEAD vs --complexity-base`, () => {
const head = complexityAt(root, 'HEAD');
if (!inputs.complexityBase) return { value: `HEAD: ${head.loc} lines, ${head.scripts} package scripts (pass --complexity-base <ref> for the delta)` };
const base = complexityAt(root, inputs.complexityBase);
const sign = (n: number) => (n >= 0 ? `+${n}` : `${n}`);
return { value: `${sign(head.loc - base.loc)} lines (${base.loc} -> ${head.loc}), ${sign(head.scripts - base.scripts)} package scripts (${base.scripts} -> ${head.scripts}) vs ${inputs.complexityBase}` };
}));
const checks = [flakeCheck(flakes), unseededCheck(root)];
return { repo, sinceDays, generatedAt: new Date(now).toISOString(), metrics, checks, issueAction: issueActionFor(checks) };
}
function listMergedPulls(client: GhClient, repo: string, since: string): number {
let merged = 0;
for (let page = 1; page <= 10; page++) {
const pulls = client.getJson(`repos/${repo}/pulls?state=closed&base=main&sort=updated&direction=desc&per_page=100&page=${page}`) as any[];
if (!Array.isArray(pulls)) throw new Error('pulls listing returned no array');
merged += pulls.filter(pull => pull.merged_at && String(pull.merged_at) >= since).length;
if (pulls.length < 100 || pulls.every(pull => String(pull.updated_at) < since)) break;
}
return merged;
}
/**
* Census metrics from main's evals-periodic history (scheduled runs plus main
* dispatches, the W4b weekly history): CEO-06/22 per lane over the trailing 4
* scheduled censuses, the INFRA re-dispatch count, slice wall, and DX-12
* quarantine evidence (qualifying vs required trials) over the last 10 runs.
*/
function censusMetrics(inputs: HealthInputs, artifactDirs: (run: CiRun, match: (name: string) => boolean) => string[]): Metric[] {
const { client, repo, now } = inputs;
const history = attempt(() => listWorkflowRuns(client, repo, 'evals-periodic.yml',
{ since: new Date(now - Math.max(inputs.sinceDays, 84) * 86_400_000).toISOString(), branch: 'main' })
.filter(run => run.event === 'schedule' || run.event === 'workflow_dispatch'));
const need = <T>(value: T | Error): T => { if (value instanceof Error) throw value; return value; };
const censuses = attempt(() => {
const out: { run: CiRun; trials: TrialRecord[] }[] = [];
for (const run of need(history)) {
if (out.length === 4) break;
const trials = records(artifactDirs(run, name => name.startsWith('trial-outcomes')));
if (trials.length) out.push({ run, trials });
}
return out;
});
const out: Metric[] = [];
for (const lane of ['periodic', 'gate'] as const) {
out.push(metric(`census-${lane}`, `${lane === 'gate' ? 'Gate' : 'Periodic'} census reliability (CEO-06/22)`,
'trailing 4 main censuses with trial outcomes (scheduled runs and main dispatches): expected failed trials per run, raw trial failure rate per kind and class, and P(lane red) from pooled per-case rates under the recorded panels vs single-trial aggregation (every failure treated as nondeterminism: upper bounds for known-good inputs)', () => {
const runs = need(censuses);
const laneRuns = runs.filter(entry => entry.trials.some(record => (record.lane ?? '').startsWith(`${lane}/`)));
if (laneRuns.length === 0) throw new Error(`no main census with ${lane} trial outcomes in the last 84 days`);
const stats = censusStats(laneRuns.flatMap(entry => entry.trials).filter(record => (record.lane ?? '').startsWith(`${lane}/`)), laneRuns.map(entry => `${entry.run.id}`));
return { value: `${(stats.failed / stats.runs).toFixed(2)} expected failed trials/run (${stats.failed}/${stats.trials} over ${stats.runs} run(s)); P(red) ${pct(stats.redInForce)} in force, ${pct(stats.redSingleTrial)} single-trial`,
detail: [...Object.entries(stats.byKind).map(([kind, slot]) => `kind ${kind}: ${slot.failed}/${slot.trials} failed (${pct(slot.failed / Math.max(1, slot.trials))})`),
...Object.entries(stats.byClass).map(([cls, count]) => `class ${cls}: ${count}`)] };
}));
}
out.push(metric('census-redispatch', 'INFRA re-dispatches (ENG-10)', 'trailing 4 main census runs whose redispatch job succeeded; longest paid slice job wall in the same runs', () => {
const runs = need(history).slice(0, 4);
const jobs = runs.map(run => listRunJobs(client, repo, run.id, run.attempt));
const redispatched = jobs.filter(list => list.some(job => job.name === 'redispatch' && job.conclusion === 'success')).length;
const walls = jobs.flat().filter(job => /^(eval-slices|eval-codex-slices|gate-census)/.test(job.name)).map(job => span(job.startedAt, job.completedAt) ?? 0);
return { value: `${redispatched} of ${runs.length} census(es) re-dispatched; longest paid slice job ${minutes(walls.length ? Math.max(...walls) : null)}` };
}));
out.push(metric('quarantine-evidence', 'Quarantine evidence (DX-12)', `blocking cases with >= ${EVAL_POLICY.quarantine.entry.minTrials} qualifying v${EVAL_POLICY.version} trials on their current series, over the last 10 main census runs`, () => {
const runs = need(history).slice(0, 10);
const report = analyzePassRates(runs.flatMap(run => records(artifactDirs(run, name => name.startsWith('trial-outcomes')))), { weeklyRuns: runs.map(run => run.createdAt) });
const blocking = report.cases.filter(entry => entry.tier === 'gate' || entry.tier === 'periodic');
const sufficient = blocking.filter(entry => (entry.current?.trials ?? 0) >= EVAL_POLICY.quarantine.entry.minTrials);
const labels = new Map<string, number>();
for (const entry of sufficient) labels.set(entry.label, (labels.get(entry.label) ?? 0) + 1);
return { value: `${sufficient.length} of ${blocking.length} blocking case(s) have enough evidence; ${blocking.length - sufficient.length} insufficient evidence (not the same as healthy)`,
detail: [...[...labels].map(([label, count]) => `${label}: ${count}`),
...(report.newerPolicyTrials ? [`ignored ${report.newerPolicyTrials} newer-policy trial(s)`] : []),
...(report.olderPolicyTrials ? [`${report.olderPolicyTrials} older-policy trial(s) display only`] : [])] };
}));
return out;
}
// --- Rendering ---
export function formatHealth(report: HealthReport): string {
const lines = [`test:health ${report.repo}, last ${report.sinceDays} day(s), ${report.generatedAt}`];
for (const entry of report.metrics) {
lines.push(`- ${entry.title}: ${entry.unavailable ? `unavailable: ${entry.unavailable}` : entry.value}`);
lines.push(` rule: ${entry.rule}`);
for (const detail of entry.detail ?? []) lines.push(` ${detail}`);
}
lines.push('Enforced checks:');
for (const check of report.checks) {
lines.push(`- [${check.status.toUpperCase()}] ${check.title}: ${check.detail}`);
if (check.status !== 'pass') lines.push(` fix: ${check.fix}`);
}
const unavailable = report.checks.filter(check => check.status === 'unavailable').map(check => check.id);
if (unavailable.length) lines.push(`Missing inputs (an unavailable check never fails the run, and keeps a tracking issue open): ${unavailable.join(', ')}`);
return lines.join('\n');
}
export function formatHealthMarkdown(report: HealthReport): string {
return ['## test:health', '', '```', formatHealth(report), '```', ''].join('\n');
}
export const HELP = `Usage: bun run test:health [options]
bun run test:health flake-summary <dir>
Prints the test/eval/CI success metrics from GitHub Actions history (needs gh
with read access to the repository; \`gh auth status\` checks it). A metric whose
input cannot be read prints "unavailable: <reason>; next: <step>", never 0.
Options:
--since-days <n> window in days (default 7)
--enforce exit 1 when an enforced check fails (flaky file > 5% of
>= 20 main free-tests runs; > 5 unseeded free files);
without it the command always exits 0
--json machine-readable report (includes issueAction)
--summary <file> append a markdown report (job summary / issue body)
--repo <owner/name> repository (default: origin)
--complexity-base <ref> report the net-complexity delta against <ref>
--help this text
flake-summary <dir> markdown table of the flake-ledger-* artifacts under <dir>`;
if (import.meta.main) {
const argv = process.argv.slice(2);
if (argv.includes('--help') || argv.includes('-h')) { console.log(HELP); process.exit(0); }
if (argv[0] === 'flake-summary') {
if (!argv[1]) { console.error('usage: bun run test:health flake-summary <dir>'); process.exit(2); }
console.log(formatFlakeSummary(readFlakeLedgerDir(argv[1])));
process.exit(0);
}
const valued = new Set(['--since-days', '--summary', '--repo', '--complexity-base']);
const known = new Set([...valued, '--enforce', '--json']);
for (let index = 0; index < argv.length; index++) {
if (!known.has(argv[index]!)) { console.error(`test:health: unknown argument ${argv[index]}\n\n${HELP}`); process.exit(2); }
if (valued.has(argv[index]!)) {
if (argv[index + 1] === undefined || argv[index + 1]!.startsWith('--')) { console.error(`test:health: ${argv[index]} needs a value`); process.exit(2); }
index++;
}
}
const flag = (name: string) => { const index = argv.indexOf(name); return index === -1 ? undefined : argv[index + 1]; };
const sinceDays = Number(flag('--since-days') ?? 7);
if (!Number.isInteger(sinceDays) || sinceDays < 1) { console.error('test:health: --since-days must be a positive integer'); process.exit(2); }
const repo = flag('--repo') ?? repoSlug(ROOT);
const cacheDir = path.join(path.resolve(resolveStateRoot()), 'test-health-cache', repo.replace('/', '-'));
const report = collectHealth({ client: GH_CLIENT, repo, now: Date.now(), sinceDays, cacheDir, complexityBase: flag('--complexity-base') });
console.log(argv.includes('--json') ? JSON.stringify(report, null, 2) : formatHealth(report));
const summary = flag('--summary');
if (summary) fs.appendFileSync(summary, formatHealthMarkdown(report));
if (argv.includes('--enforce') && report.checks.some(check => check.status === 'fail')) process.exit(1);
}