Files
fusion/scripts/pg-preadmission-campaign.mjs
gsxdsm 1a3e68de5d FN-9139: add bounded PostgreSQL pre-admission measurement
Establish an inert PostgreSQL setup signal and repeatable evidence tooling without changing harness behavior.

- add explicit setup participation semantics and inertness coverage
- survey Vitest setup boundaries with isolated report-only fixtures
- bound interleaved campaigns by process group and campaign deadline
- reject missing backend samples and enable candidate diagnostics
- document the rejected boundary result and successor protocol

Files changed:
 .../test-failures/postgres-ddl-admission-bound.md  |  15 ++
 docs/testing.md                                    |  10 ++
 packages/core/package.json                         |   2 +-
 .../src/__test-utils__/pg-setup-participation.ts   |  22 +++
 .../src/__tests__/pg-setup-participation.test.ts   |  21 +++
 .../__tests__/vitest-setup-pg-inertness.test.ts    |  10 ++
 packages/core/vitest.pg.config.ts                  |  10 ++
 .../__tests__/pg-preadmission-campaign.test.mjs    |  63 +++++++
 scripts/__tests__/pg-setup-boundary-probe.test.mjs |  92 ++++++++++
 scripts/pg-preadmission-campaign.mjs               | 194 +++++++++++++++++++++
 scripts/pg-setup-boundary-probe.mjs                | 182 +++++++++++++++++++
 11 files changed, 620 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-9139

Fusion-Task-Lineage: 2cf8ccbf-37f9-4fdc-8e8f-326df823e1cd

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-16 21:09:58 -07:00

195 lines
9.8 KiB
JavaScript

#!/usr/bin/env node
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { spawn } from "node:child_process";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { pathToFileURL } from "node:url";
/*
FNXC:PgTestPreAdmission 2026-08-17-03:20:
FN-9139 requires pre-admission affordability evidence to be collected by one
bounded shell, so a missing operator terminal cannot turn measurement into an
unbounded handoff. The driver alternates arms and treats incomplete or invalid
samples as insufficient data instead of retrying toward a preferred outcome.
*/
export const MINIMUM_RUNS_PER_ARM = 5;
export const DEFAULT_RUN_TIMEOUT_MS = 300_000;
export const DEFAULT_CAMPAIGN_TIMEOUT_MS = 3_600_000;
export function parseArgs(args) {
const result = { runs: MINIMUM_RUNS_PER_ARM, scratchDir: undefined, control: undefined, candidate: undefined };
for (let i = 0; i < args.length; i += 1) {
const value = args[i];
if (value === "--runs") result.runs = Number(args[++i]);
else if (value === "--scratch-dir") result.scratchDir = args[++i];
else if (value === "--control") result.control = args[++i];
else if (value === "--candidate") result.candidate = args[++i];
else throw new Error(`Unknown argument: ${value}`);
}
if (!Number.isInteger(result.runs) || result.runs < 1) throw new Error("--runs must be a positive integer");
return result;
}
export function stripAnsi(text) {
return text.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g"), "");
}
export function classifyLog(log) {
const clean = stripAnsi(log);
const failed = [...clean.matchAll(/^\s*Test Files\s+.*?(\d+)\s+failed\b/gim)].at(-1);
return {
failingFiles: failed ? Number(failed[1]) : 0,
projectIdentityTimedOut: /project-identity\.test\.ts[\s\S]{0,500}?timed out/i.test(clean),
};
}
export function countProbeDegradations(jsonl) {
return jsonl.split(/\r?\n/).filter(Boolean).reduce((count, line) => {
try {
const row = JSON.parse(line);
return count + (row.event === "pg-preadmission-probe" && row.outcome !== "acquired-and-released" ? 1 : 0);
} catch { return count; }
}, 0);
}
export function median(values) {
const sorted = values.filter(Number.isFinite).toSorted((a, b) => a - b);
return sorted.length === 0 ? null : sorted[Math.floor(sorted.length / 2)];
}
export function decideVerdict(runs, { incomplete = false } = {}) {
const control = runs.filter((run) => run.arm === "control" && run.valid);
const candidate = runs.filter((run) => run.arm === "candidate" && run.valid);
const measurementsAvailable = [...control, ...candidate].every((run) => run.measurementAvailable === true && Number.isFinite(run.peakBackends));
if (incomplete || control.length < MINIMUM_RUNS_PER_ARM || candidate.length < MINIMUM_RUNS_PER_ARM || !measurementsAvailable) return "insufficient-data";
if (candidate.some((run) => run.probeRecords === 0 || run.probeDegradations !== 0)) return "boundary-taxing";
const controlMaxWall = Math.max(...control.map((run) => run.wallTimeMs));
const controlMaxFailures = Math.max(...control.map((run) => run.failingFiles));
const controlPeak = Math.max(...control.map((run) => run.peakBackends));
const candidateMedian = median(candidate.map((run) => run.wallTimeMs));
const candidateMaxFailures = Math.max(...candidate.map((run) => run.failingFiles));
const candidatePeak = Math.max(...candidate.map((run) => run.peakBackends));
const identityWorsened = candidate.some((run) => run.projectIdentityTimedOut) && !control.some((run) => run.projectIdentityTimedOut);
return candidateMedian <= controlMaxWall && candidateMaxFailures <= controlMaxFailures && candidatePeak <= controlPeak + 1 && !identityWorsened
? "boundary-affordable" : "boundary-taxing";
}
export function aggregate(runs, incomplete) {
const summarize = (arm) => {
const selected = runs.filter((run) => run.arm === arm);
return { count: selected.length, medianWallTimeMs: median(selected.map((run) => run.wallTimeMs)), maxWallTimeMs: Math.max(...selected.map((run) => run.wallTimeMs), 0), maxFailingFiles: Math.max(...selected.map((run) => run.failingFiles), 0), peakBackends: Math.max(...selected.map((run) => run.peakBackends), 0) };
};
return { runs, control: summarize("control"), candidate: summarize("candidate"), incomplete, verdict: decideVerdict(runs, { incomplete }) };
}
export function terminateProcessGroup(child, signal, kill = process.kill) {
if (!child.pid) return;
try {
kill(-child.pid, signal);
} catch {
try { child.kill(signal); } catch { /* The process already exited. */ }
}
}
export function campaignRunEnvironment(diagnostics) {
return {
FUSION_PG_TEST_TEARDOWN_DIAGNOSTICS: "1",
FUSION_PG_TEST_TEARDOWN_DIAGNOSTICS_LOG: diagnostics,
};
}
export function remainingRunTimeoutMs(startedAt, now = Date.now()) {
return Math.min(DEFAULT_RUN_TIMEOUT_MS, DEFAULT_CAMPAIGN_TIMEOUT_MS - (now - startedAt));
}
export async function runCommand(command, env, timeoutMs) {
return await new Promise((resolveRun) => {
const startedAt = Date.now();
// process-supervisor-allowlist: isolation is required so the bounded campaign can kill the shell command's entire process group
const child = spawn(command, { shell: true, detached: true, env: { ...process.env, ...env } });
// FNXC:PgTestPreAdmission 2026-08-17-04:05:
// The timeout owns the shell's whole process group. Killing only the shell
// can orphan Vitest or its workers and violate both campaign deadlines.
const samplerScript = `while kill -0 ${child.pid} 2>/dev/null; do if value=$(psql --no-password -X -Atqc 'select count(*) from pg_stat_activity;' postgres 2>/dev/null); then printf 'SAMPLE:%s\\n' "$value"; else echo SAMPLE_ERROR; exit 1; fi; sleep 1; done`;
// process-supervisor-allowlist: the sampler shares the campaign's bounded process-group cleanup contract
const sampler = spawn(samplerScript, { shell: true, detached: true, stdio: ["ignore", "pipe", "ignore"] });
let output = "";
let sampled = "";
let childResult;
let samplerClosed = false;
let samplerErrored = false;
let timedOut = false;
const finish = () => {
if (!childResult || !samplerClosed) return;
const samples = [...sampled.matchAll(/^SAMPLE:(\d+)$/gm)].map((match) => Number(match[1]));
const measurementAvailable = !samplerErrored && !sampled.includes("SAMPLE_ERROR") && samples.length > 0;
resolveRun({
output,
...childResult,
wallTimeMs: Date.now() - startedAt,
peakBackends: measurementAvailable ? Math.max(...samples) : null,
measurementAvailable,
timedOut,
});
};
child.stdout.on("data", (chunk) => { output += chunk; });
child.stderr.on("data", (chunk) => { output += chunk; });
sampler.stdout.on("data", (chunk) => { sampled += chunk; });
sampler.once("error", () => { samplerErrored = true; samplerClosed = true; finish(); });
sampler.once("close", () => { samplerClosed = true; finish(); });
const timeout = setTimeout(() => {
timedOut = true;
terminateProcessGroup(child, "SIGTERM");
terminateProcessGroup(child, "SIGKILL");
}, timeoutMs);
child.once("close", (code, signal) => {
clearTimeout(timeout);
childResult = { code, signal };
terminateProcessGroup(sampler, "SIGTERM");
terminateProcessGroup(sampler, "SIGKILL");
finish();
});
});
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.control || !args.candidate) throw new Error("Supply --control and --candidate commands");
const scratch = resolve(args.scratchDir ?? join(tmpdir(), `fusion-pg-preadmission-${process.pid}`));
mkdirSync(scratch, { recursive: true });
const startedAt = Date.now();
const results = [];
let incomplete = false;
for (let index = 0; index < args.runs * 2; index += 1) {
const runTimeoutMs = remainingRunTimeoutMs(startedAt);
if (runTimeoutMs <= 0) { incomplete = true; break; }
const arm = index % 2 === 0 ? "control" : "candidate";
const diagnostics = join(scratch, `${String(index + 1).padStart(2, "0")}-${arm}.jsonl`);
// FNXC:PgTestPreAdmission 2026-08-17-04:05:
// Diagnostics must be explicitly enabled, and an arm may consume only the
// smaller of its five-minute allowance and the campaign's remaining time.
const result = await runCommand(
arm === "control" ? args.control : args.candidate,
campaignRunEnvironment(diagnostics),
runTimeoutMs,
);
const logPath = join(scratch, `${String(index + 1).padStart(2, "0")}-${arm}.log`);
writeFileSync(logPath, result.output);
const parsed = classifyLog(result.output);
const jsonl = (() => { try { return readFileSync(diagnostics, "utf8"); } catch { return ""; } })();
const probeRecords = [...jsonl.matchAll(/"event":"pg-preadmission-probe"/g)].length;
// FNXC:PgTestPreAdmission 2026-08-17-03:38:
// FN-9139's affordability verdict must retain the sampler's observed peak.
// Replacing it would falsely certify a candidate whose extra backend exceeds
// the one-connection observation-only footprint.
results.push({ arm, ...result, ...parsed, probeRecords, probeDegradations: arm === "control" ? null : countProbeDegradations(jsonl), valid: !result.timedOut && result.signal === null && result.measurementAvailable && (arm === "control" || probeRecords > 0) });
if (result.timedOut || Date.now() - startedAt >= DEFAULT_CAMPAIGN_TIMEOUT_MS) { incomplete = true; break; }
}
const report = aggregate(results, incomplete);
writeFileSync(join(scratch, "results.json"), `${JSON.stringify(report, null, 2)}\n`);
console.log(JSON.stringify({ ...report, scratchDir: scratch }, null, 2));
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main().catch((error) => { console.error(error); process.exitCode = 1; });