Files
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
ci / test (push) Waiting to run
ci / lint-and-format (push) Waiting to run
ci / build (push) Waiting to run
ci / dev-startup (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
gitleaks / gitleaks (push) Waiting to run
Markdown Links / Relative Markdown Links (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Quality (Extended) / Homepage Build (PR smoke) (push) Waiting to run
Quality (Extended) / Comment-only diff guard (push) Waiting to run
Quality (Extended) / Format + Type Safety Ratchet (push) Waiting to run
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Waiting to run
Quality (Extended) / Develop Gate (lint) (push) Waiting to run
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

525 lines
18 KiB
JavaScript

/**
* Boot KPI.
*
* Measures cold-start of the agent + dashboard API: spawn the dev-server
* headless, poll GET /api/health until ready, and record wall-clock readyMs,
* peak RSS (sampled from /proc/<pid>/status VmRSS while booting), and
* steady-state RSS (median of a post-ready idle settle window — the resident
* cost the headless agent carries once boot churn subsides).
*
* Default: spawn a fresh child and measure cold boot.
* node packages/benchmarks/loadperf/boot-kpi.mjs
*
* --attach: skip spawning and measure an already-running instance at
* LOADPERF_BASE_URL (default http://127.0.0.1:<ELIZA_API_PORT>). Useful for a
* warm-boot reading against a server someone else started.
* LOADPERF_BASE_URL=http://127.0.0.1:31337 node ... boot-kpi.mjs --attach
*
* Env:
* ELIZA_API_PORT API port for the spawned/attached server (default 31337)
* LOADPERF_BASE_URL base URL to probe (overrides host:port derivation)
* LOADPERF_BOOT_TIMEOUT_MS ready timeout (default 120000)
* LOADPERF_BOOT_RUNS cold boots to spawn for median/p95 (default 3; the CLI
* --runs=N takes precedence; --attach forces a single run)
*
* Honesty gates (so a stale server / early-liveness 200 can never read as PASS):
* - the run FAILS unless the final probe returned health.ready === true;
* - the run FAILS if the median readyMs is below the sanity floor
* (READY_SANITY_FLOOR_MS) — a real agent boot can never be sub-second, so a
* sub-floor reading means a false-positive / stale-server measurement.
*
* Fail-safe: if the agent cannot boot or never reports ready, records
* { skipped: true, error } and exits 2 (so run-all can carry on).
*
* Exit: 0 pass, 1 budget/honesty fail, 2 skipped/unavailable.
*/
import { spawn } from "node:child_process";
import { existsSync, readdirSync, readFileSync } from "node:fs";
import os from "node:os";
import {
join,
loadBudgets,
ms,
REPO_ROOT,
recordResult,
sleep,
waitForReady,
} from "./lib.mjs";
const NOW = new Date().toISOString();
const ATTACH = process.argv.includes("--attach");
const JSON_ONLY = process.argv.includes("--json");
// Cold boot varies run-to-run (CPU contention, JIT warmup). Spawn N cold boots
// and report the median (the budget is checked against it) plus p95/min/max so a
// single noisy run can't be mistaken for a real delta. Default 3; override with
// --runs=N (precedence) or LOADPERF_BOOT_RUNS. --attach forces a single probe.
const DEFAULT_RUNS = 3;
const RUNS_ARG = process.argv.find((a) => a.startsWith("--runs="));
const RUNS_REQUESTED =
Number(RUNS_ARG?.split("=")[1]) ||
Number(process.env.LOADPERF_BOOT_RUNS) ||
DEFAULT_RUNS;
const RUNS = ATTACH ? 1 : Math.max(1, Math.trunc(RUNS_REQUESTED));
// A real agent cold boot is multiple seconds (blocking phase alone is ~2 s, and
// the full readiness gate is tens of seconds today). Any median below this floor
// is physically impossible for a genuine boot and signals a stale server / an
// early-liveness 200 that slipped past the readiness check — fail loudly.
const READY_SANITY_FLOOR_MS = 3000;
// Steady-state RSS: peak RSS captures the boot-time high-water mark, but a
// booted agent that never releases boot scratch (or slowly grows at idle) is a
// separate regression class the peak number hides. After `ready`, we hold the
// idle process for a settle window, sample RSS, and report the MEDIAN of the
// window's tail (post-GC-settle) as steadyRssMb — the resident cost a headless
// agent actually carries once boot churn subsides. Off during --attach (no pid).
const STEADY_SETTLE_MS = Math.max(
0,
Math.trunc(Number(process.env.LOADPERF_STEADY_SETTLE_MS ?? 12_000)),
);
const STEADY_SAMPLE_MS = Math.max(
100,
Math.trunc(Number(process.env.LOADPERF_STEADY_SAMPLE_MS ?? 500)),
);
const API_PORT = Number(process.env.ELIZA_API_PORT ?? 31337);
const BASE_URL = (
process.env.LOADPERF_BASE_URL ?? `http://127.0.0.1:${API_PORT}`
).replace(/\/$/, "");
const BOOT_TIMEOUT_MS = Number(process.env.LOADPERF_BOOT_TIMEOUT_MS ?? 120_000);
const DEV_SERVER = join(
"packages",
"app-core",
"src",
"runtime",
"dev-server.ts",
);
// By DEFAULT measure the SHIPPED binary. The desktop/mobile app spawns the
// pre-built `dist/entry.js start` via Bun (native/agent.ts), NOT the
// tsx-transpiled dev-server — so the old default counted a ~2s on-the-fly tsx
// transpile + dev-only orchestration that production never pays, and never the
// real `start` blocking work (vault/keychain bootstrap, embedding warmup,
// provider load). Pass --dev to measure the old tsx dev-server path instead.
const PROD_ENTRY = join("packages", "app-core", "dist", "entry.js");
const USE_DEV = process.argv.includes("--dev");
const BUN_BIN = process.env.BUN_PATH || "bun";
/** Read VmRSS (kB) for a pid from /proc; returns bytes or null. */
function readRssBytes(pid) {
try {
const status = readFileSync(`/proc/${pid}/status`, "utf8");
const m = status.match(/^VmRSS:\s+(\d+)\s+kB/m);
return m ? Number(m[1]) * 1024 : null;
} catch {
return null;
}
}
/**
* Best-effort detection of CPU contention from sibling node/agent processes.
* Boot is single-threaded and import-bound (research/03 F8), so a contended host
* inflates readyMs without any code regression. We count peer processes whose
* /proc/<pid>/comm is node/bun/tsx (excluding our own pid) and read loadavg; the
* caller WARNs when either looks heavy so a contended run is visibly flagged.
*/
function detectContention() {
const cpuCount = os.cpus().length;
const loadAvg1 = os.loadavg()[0];
let siblingProcs = 0;
try {
const self = process.pid;
for (const name of readdirSync("/proc")) {
if (!/^\d+$/.test(name)) continue;
const pid = Number(name);
if (pid === self) continue;
let comm;
try {
comm = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
} catch {
continue; // process exited between readdir and read
}
if (comm === "node" || comm === "bun" || comm === "tsx")
siblingProcs += 1;
}
} catch {
siblingProcs = -1; // /proc unavailable (non-Linux); leave the rest meaningful
}
const heavy =
loadAvg1 > cpuCount || (siblingProcs >= 0 && siblingProcs > cpuCount);
return { cpuCount, loadAvg1, siblingProcs, heavy };
}
function checkBudgets(readyMs, peakRssBytes, steadyRssBytes) {
const b = loadBudgets().boot;
const peakRssMb = peakRssBytes == null ? null : peakRssBytes / (1024 * 1024);
const steadyRssMb =
steadyRssBytes == null ? null : steadyRssBytes / (1024 * 1024);
const checks = [
{ name: "coldReadyMs", value: readyMs, budget: b.coldReadyMs, unit: "ms" },
];
if (peakRssMb != null) {
checks.push({
name: "peakRssMb",
value: peakRssMb,
budget: b.peakRssMb,
unit: "MB",
});
}
// steadyRssMb is only checked when the budget is set AND we measured a value.
// A null budget (no baseline yet) records the number without gating.
if (steadyRssMb != null && b.steadyRssMb != null) {
checks.push({
name: "steadyRssMb",
value: steadyRssMb,
budget: b.steadyRssMb,
unit: "MB",
});
}
return checks.map((c) => ({ ...c, pass: c.value <= c.budget }));
}
async function measureAttached() {
const { readyMs, health } = await waitForReady(BASE_URL, {
timeoutMs: BOOT_TIMEOUT_MS,
});
return {
readyMs,
peakRssBytes: null,
steadyRssBytes: null,
health,
mode: "attach",
};
}
// Resolve the agent's state dir the same way @elizaos/core does, so we can read
// the per-phase boot telemetry the runtime writes on every completed boot
// (<stateDir>/telemetry/boot/latest.json). Surfacing the lap breakdown turns a
// single readyMs number into a per-phase profile — you can see WHICH phase
// regressed, not just that total boot moved.
function resolveStateDirForKpi() {
const explicit = process.env.ELIZA_STATE_DIR;
if (explicit) return explicit;
const ns = process.env.ELIZA_NAMESPACE || "eliza";
const xdg =
process.env.XDG_STATE_HOME || join(os.homedir(), ".local", "state");
return join(xdg, ns);
}
function readBootPhases() {
try {
const file = join(
resolveStateDirForKpi(),
"telemetry",
"boot",
"latest.json",
);
if (!existsSync(file)) return null;
const data = JSON.parse(readFileSync(file, "utf8"));
if (!Array.isArray(data.laps)) return null;
return {
totalMs: data.totalMs,
// slowest-first — what you read to find the boot bottleneck
laps: [...data.laps]
.sort((a, b) => b.ms - a.ms)
.map((l) => ({ name: l.name, ms: l.ms })),
};
} catch {
return null;
}
}
async function measureSpawned() {
if (!USE_DEV && !existsSync(join(REPO_ROOT, PROD_ENTRY))) {
throw new Error(
`built agent entry not found at ${PROD_ENTRY} — run \`bun run --cwd packages/app-core build\` first, or pass --dev to measure the tsx dev-server path`,
);
}
const startMs = Date.now();
// Production: `bun run dist/entry.js start` (what the desktop/mobile app
// spawns). Dev (--dev): the tsx-transpiled dev-server. Match the desktop
// launcher's ELIZA_DEFER_APP_ROUTES=1 default so the readiness gate is
// representative of the shipped boot.
const [cmd, args] = USE_DEV
? [
process.execPath,
["--conditions=eliza-source", "--import", "tsx", DEV_SERVER],
]
: [BUN_BIN, ["run", PROD_ENTRY, "start"]];
const child = spawn(cmd, args, {
cwd: REPO_ROOT,
env: {
...process.env,
ELIZA_HEADLESS: "1",
ELIZA_API_PORT: String(API_PORT),
ELIZA_DEFER_APP_ROUTES: process.env.ELIZA_DEFER_APP_ROUTES ?? "1",
},
stdio: ["ignore", "ignore", "pipe"],
});
let stderrTail = "";
child.stderr.on("data", (d) => {
stderrTail = (stderrTail + d.toString()).slice(-2000);
});
let peakRssBytes = 0;
let sampling = true;
const sampler = (async () => {
while (sampling) {
const rss = readRssBytes(child.pid);
if (rss != null && rss > peakRssBytes) peakRssBytes = rss;
await sleep(250);
}
})();
let childExited = false;
child.once("exit", () => {
childExited = true;
});
const exited = new Promise((_, reject) => {
child.once("exit", (code, signal) => {
reject(
new Error(
`agent process exited early (code=${code} signal=${signal})\n${stderrTail}`,
),
);
});
child.once("error", (err) => reject(err));
});
try {
const ready = await Promise.race([
waitForReady(BASE_URL, { timeoutMs: BOOT_TIMEOUT_MS, startMs }),
exited,
]);
// one last RSS sample at ready
const rssAtReady = readRssBytes(child.pid);
if (rssAtReady != null && rssAtReady > peakRssBytes)
peakRssBytes = rssAtReady;
// Stop counting boot peak at `ready`; steady-state is measured separately so
// a post-ready settle sample can never inflate the boot high-water mark.
sampling = false;
await sampler;
// Hold the idle process for the settle window, sampling RSS. steadyRss is
// the median of the window's tail (last 60%), after boot-time GC/scratch
// churn subsides — the resident cost the headless agent actually carries.
const steadySamples = [];
if (STEADY_SETTLE_MS > 0) {
const settleDeadline = Date.now() + STEADY_SETTLE_MS;
while (Date.now() < settleDeadline && !childExited) {
const rss = readRssBytes(child.pid);
if (rss != null) steadySamples.push(rss);
await sleep(STEADY_SAMPLE_MS);
}
}
const tail = steadySamples.slice(Math.floor(steadySamples.length * 0.4));
const steadyRssBytes = tail.length > 0 ? median(tail) : null;
return {
readyMs: ready.readyMs,
peakRssBytes: peakRssBytes || null,
steadyRssBytes,
health: ready.health,
mode: "spawn",
};
} finally {
sampling = false;
await sampler;
child.kill("SIGTERM");
// give it a moment, then SIGKILL if still alive
await sleep(500);
try {
child.kill("SIGKILL");
} catch {
// already gone
}
}
}
function median(values) {
if (values.length === 0) return null;
const s = [...values].sort((a, b) => a - b);
const mid = Math.floor(s.length / 2);
return s.length % 2 ? s[mid] : Math.round((s[mid - 1] + s[mid]) / 2);
}
function percentile(values, p) {
if (values.length === 0) return null;
const s = [...values].sort((a, b) => a - b);
return s[Math.min(s.length - 1, Math.floor(s.length * p))];
}
async function main() {
const contention = detectContention();
if (contention.heavy) {
const sib = contention.siblingProcs < 0 ? "?" : contention.siblingProcs;
console.warn(
`[boot-kpi] WARN: heavy CPU contention — loadavg(1m)=${contention.loadAvg1.toFixed(2)} ` +
`over ${contention.cpuCount} cpus, ${sib} sibling node/bun/tsx procs. ` +
`Boot is single-threaded and import-bound, so readyMs will be inflated; ` +
`re-run on a quiet host before trusting this number.`,
);
}
// Collect 1 (attach) or N (spawn) measurements. A run that fails to boot is
// recorded but doesn't abort the others; we only "skip" if EVERY run failed.
const runs = [];
let lastError = null;
const total = ATTACH ? 1 : RUNS;
for (let i = 0; i < total; i++) {
try {
runs.push(ATTACH ? await measureAttached() : await measureSpawned());
} catch (err) {
lastError = err;
}
}
if (runs.length === 0) {
const payload = {
skipped: true,
mode: ATTACH ? "attach" : "spawn",
error: lastError?.message ?? String(lastError),
};
const { file } = recordResult("boot", payload, NOW);
if (JSON_ONLY) console.log(JSON.stringify({ ...payload, file }, null, 2));
else
console.error(
`[boot-kpi] skipped: ${payload.error}\nrecorded -> ${file}`,
);
process.exit(2);
}
const mode = runs[0].mode;
const readyMsRuns = runs.map((r) => r.readyMs);
const rssRuns = runs.map((r) => r.peakRssBytes).filter((v) => v != null);
const steadyRssRuns = runs
.map((r) => r.steadyRssBytes)
.filter((v) => v != null);
// The median is the canonical readyMs; peak + steady RSS are the worst observed.
const medianReadyMs = median(readyMsRuns);
const peakRssBytes = rssRuns.length > 0 ? Math.max(...rssRuns) : null;
const steadyRssBytes =
steadyRssRuns.length > 0 ? Math.max(...steadyRssRuns) : null;
const checks = checkBudgets(medianReadyMs, peakRssBytes, steadyRssBytes);
const peakRssMb =
peakRssBytes == null
? null
: Number((peakRssBytes / (1024 * 1024)).toFixed(1));
const steadyRssMb =
steadyRssBytes == null
? null
: Number((steadyRssBytes / (1024 * 1024)).toFixed(1));
// Honesty gates — these are PASS/FAIL conditions, not budget tunables. They
// make a stale-server / early-liveness false positive fail the run loudly.
const healthReady = runs[runs.length - 1].health?.ready ?? null;
checks.push({
name: "healthReady",
value: healthReady === true ? 1 : 0,
budget: 1,
unit: "bool",
pass: healthReady === true,
});
checks.push({
name: "readyMsSanityFloor",
value: medianReadyMs,
budget: READY_SANITY_FLOOR_MS,
unit: "ms",
// A genuine boot is ABOVE the floor; below it means a false-positive read.
pass: medianReadyMs != null && medianReadyMs >= READY_SANITY_FLOOR_MS,
});
const result = {
summary: {
mode,
baseUrl: BASE_URL,
runs: runs.length,
requestedRuns: total,
readyMs: medianReadyMs,
readyMsRuns,
readyMsP95: percentile(readyMsRuns, 0.95),
readyMsMin: Math.min(...readyMsRuns),
readyMsMax: Math.max(...readyMsRuns),
peakRssBytes,
peakRssMb,
steadyRssBytes,
steadyRssMb,
steadySettleMs: STEADY_SETTLE_MS,
healthReady,
readySanityFloorMs: READY_SANITY_FLOOR_MS,
contention,
// Per-phase agent-boot breakdown from the runtime's boot telemetry (the
// last completed boot). null when telemetry is disabled/unavailable.
bootPhases: readBootPhases(),
},
checks,
pass: checks.every((c) => c.pass),
};
const { file } = recordResult("boot", result, NOW);
if (JSON_ONLY) {
console.log(JSON.stringify(result, null, 2));
} else {
const s = result.summary;
console.log("\n=== Boot KPI ===");
console.log(`mode: ${mode}`);
console.log(`base url: ${BASE_URL}`);
console.log(
`runs: ${runs.length}${total !== runs.length ? ` (of ${total} requested)` : ""}`,
);
if (runs.length > 1) {
console.log(
`ready median:${ms(medianReadyMs)} (p95 ${ms(s.readyMsP95)}, min ${ms(s.readyMsMin)}, max ${ms(s.readyMsMax)})`,
);
console.log(
`ready runs: ${readyMsRuns.map((v) => Math.round(v)).join(", ")} ms`,
);
} else {
console.log(`ready: ${ms(medianReadyMs)}`);
}
console.log(`peak RSS: ${peakRssMb == null ? "—" : `${peakRssMb} MB`}`);
console.log(
`steady RSS: ${steadyRssMb == null ? "—" : `${steadyRssMb} MB`}${STEADY_SETTLE_MS > 0 ? ` (${Math.round(STEADY_SETTLE_MS / 1000)}s settle)` : " (settle off)"}`,
);
console.log(
`health.ready:${healthReady === true ? " true" : ` ${healthReady}`}`,
);
if (s.bootPhases?.laps?.length) {
console.log(
`\n-- agent boot phases (total ${ms(s.bootPhases.totalMs)}, slowest first) --`,
);
for (const lap of s.bootPhases.laps) {
console.log(` ${String(lap.ms).padStart(7)} ms ${lap.name}`);
}
}
console.log("\n-- budget checks --");
for (const c of checks) {
let v;
let bud;
if (c.unit === "MB") {
v = `${c.value.toFixed(1)} MB`;
bud = `${c.budget} MB`;
} else if (c.unit === "bool") {
v = healthReady === true ? "true" : String(healthReady);
bud = "true";
} else if (c.name === "readyMsSanityFloor") {
v = ms(c.value);
bud = `≥ ${ms(c.budget)}`;
} else {
v = ms(c.value);
bud = ms(c.budget);
}
console.log(
` ${c.pass ? "PASS" : "FAIL"} ${c.name}: ${v} / budget ${bud}`,
);
}
console.log(
`\nresult: ${result.pass ? "PASS" : "FAIL"} recorded -> ${file}\n`,
);
}
process.exit(result.pass ? 0 : 1);
}
main();