mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 22:48:57 +02:00
fix: pre-landing review fixes for the v2 port wave
Review army (checklist + 5 specialists) + coverage/plan audits on the assembled branch. Genuine correctness/security/hygiene fixes: - test-paid-shards: strictTestExitCode now receives expectedFiles on the real bun path, so a shard that runs fewer files than planned (harness crash, nothing loaded) with exit 0 is no longer recorded 'passed' — the invisible-non-execution class the runner exists to kill. Pinned by the new test/strict-output.test.ts (also covers the chunk-boundary classifier). - test-paid-shards: EVALS_TIER env is validated (gate|periodic) like the --tier flag, so a typo can't self-skip every test and exit 0 green. - package.json: test:periodic:sharded sets EVALS_ALL=1, restoring the full-tier semantics the pre-shard script had (CI already set it; local eval:bg:periodic silently under-measured without it). - brain-sync.test: run() pins HOME to the temp home so gstack-artifacts-init stops writing/clobbering the operator's real ~/.gstack-artifacts-remote.txt every free-suite run; afterEach now also scrubs the current filename. - egress-receipt: cap each receipt field at 512B so a serialized line always fits the 4KB tail-read window — a longer line would make the next append hash a truncated prior line and verifyLedger report a permanent false TAMPER. warnLedgerSize short-circuits before statSync once fired (append hot path). - gstack-egress: import.meta.dir (Windows-safe) instead of new URL().pathname so grants doesn't silently report defaults on Windows; strip control chars from ledger-derived fields on render so a crafted receipt can't spoof the auditor's view. - extension/background.js + CLAUDE.md: renumber the identity-pin migration refs v1.62 -> v1.63 (main claimed 1.62.0.0; this wave queue-advances). - egress-receipt-wiring: pin lib/context-bill.ts unconditionally (both land together now); drop the dead RunShardsOptions.tier field. All fix-affected test files green; gate failures triaged as external-env (codex/gemini CLI drift) or pre-existing (hermetic-canary fails identically on base). Deferred polish tracked in the PR body + decision store. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ea7ba921ce
commit
ccb91c3afb
+17
-8
@@ -34,7 +34,11 @@ function run(argv: string[], opts: { env?: Record<string, string>; input?: strin
|
||||
const bin = argv[0];
|
||||
const full = bin.startsWith('/') ? bin : path.join(BIN, bin);
|
||||
const res = spawnSync(full, argv.slice(1), {
|
||||
env: { ...process.env, GSTACK_HOME: tmpHome, ...(opts.env || {}) },
|
||||
// HOME is overridden too: gstack-artifacts-init writes
|
||||
// $HOME/.gstack-artifacts-remote.txt (plain $HOME, not GSTACK_HOME), so
|
||||
// without this every free-suite run clobbers the operator's real
|
||||
// artifacts-remote pointer. Keep it inside tmpHome, which afterEach removes.
|
||||
env: { ...process.env, HOME: tmpHome, GSTACK_HOME: tmpHome, ...(opts.env || {}) },
|
||||
encoding: 'utf-8',
|
||||
input: opts.input,
|
||||
cwd: ROOT,
|
||||
@@ -56,13 +60,18 @@ beforeEach(() => {
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
fs.rmSync(bareRemote, { recursive: true, force: true });
|
||||
// Clean up any remote-helper file init may have written.
|
||||
const remoteFile = path.join(os.homedir(), '.gstack-brain-remote.txt');
|
||||
// Only remove if it points at OUR bare remote (don't clobber a real user file).
|
||||
try {
|
||||
const contents = fs.readFileSync(remoteFile, 'utf-8').trim();
|
||||
if (contents === bareRemote) fs.unlinkSync(remoteFile);
|
||||
} catch {}
|
||||
// Clean up any remote-helper file init may have written. run() now pins
|
||||
// HOME to tmpHome so these land inside the removed temp dir, but scrub the
|
||||
// real home too as defense in depth — and cover BOTH the legacy brain-remote
|
||||
// name and the current artifacts-remote name (init writes the latter).
|
||||
for (const name of ['.gstack-brain-remote.txt', '.gstack-artifacts-remote.txt']) {
|
||||
const remoteFile = path.join(os.homedir(), name);
|
||||
// Only remove if it points at OUR bare remote (don't clobber a real user file).
|
||||
try {
|
||||
const contents = fs.readFileSync(remoteFile, 'utf-8').trim();
|
||||
if (contents === bareRemote) fs.unlinkSync(remoteFile);
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@@ -67,8 +67,10 @@ const MODULE_SINKS = [
|
||||
'bin/gstack-gbrain-sync.ts',
|
||||
'bin/gstack-memory-ingest.ts',
|
||||
'browse/src/server.ts',
|
||||
// context-bill lands after this tripwire in the same wave; assert once present.
|
||||
...(exists('lib/context-bill.ts') ? ['lib/context-bill.ts'] : []),
|
||||
// Unconditional: context-bill ships in the same tree as this tripwire. A
|
||||
// missing file must fail loudly (a rename/move that drops its receipt wiring
|
||||
// is exactly what this pins), not silently soften the assertion.
|
||||
'lib/context-bill.ts',
|
||||
];
|
||||
|
||||
/** Shell sinks: must source the shared lib; every network op receipted. */
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Pins scripts/test-strict-output.ts — the verdict-integrity layer of the
|
||||
* sharded paid runner. Its whole reason to exist is refusing to trust a zero
|
||||
* exit when failures were printed OR fewer files ran than planned; paid-shards'
|
||||
* fake commands never emit real Bun result lines, so without this file that
|
||||
* core was exercised nowhere and a regex regression would silently revert the
|
||||
* paid tier to trusting exit codes.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { BunTestOutputClassifier, strictTestExitCode } from '../scripts/test-strict-output';
|
||||
|
||||
describe('strictTestExitCode', () => {
|
||||
it('trusts a clean zero exit when the expected file count ran', () => {
|
||||
const summary = { failedTests: 0, unhandledBetweenTests: 0, terminalFileCounts: [1] };
|
||||
expect(strictTestExitCode(0, summary, 1)).toBe(0);
|
||||
});
|
||||
|
||||
it('refuses a zero exit when fewer files ran than expected (invisible non-execution)', () => {
|
||||
const summary = { failedTests: 0, unhandledBetweenTests: 0, terminalFileCounts: [1] };
|
||||
expect(strictTestExitCode(0, summary, 2)).toBe(1);
|
||||
});
|
||||
|
||||
it('refuses a zero exit when failure lines were printed', () => {
|
||||
const summary = { failedTests: 1, unhandledBetweenTests: 0, terminalFileCounts: [1] };
|
||||
expect(strictTestExitCode(0, summary, 1)).toBe(1);
|
||||
});
|
||||
|
||||
it('refuses a zero exit on an unhandled error between tests', () => {
|
||||
const summary = { failedTests: 0, unhandledBetweenTests: 1, terminalFileCounts: [1] };
|
||||
expect(strictTestExitCode(0, summary, 1)).toBe(1);
|
||||
});
|
||||
|
||||
it('propagates a non-zero child exit regardless of expectedFiles', () => {
|
||||
const summary = { failedTests: 0, unhandledBetweenTests: 0, terminalFileCounts: [1] };
|
||||
expect(strictTestExitCode(1, summary, 1)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BunTestOutputClassifier', () => {
|
||||
it('counts a (fail) line split across write chunks', () => {
|
||||
const c = new BunTestOutputClassifier();
|
||||
c.write('[31m(fail) my te');
|
||||
c.write('st [3.42ms][0m\nRan 4 tests across 1 files. [2.10s]\n');
|
||||
const summary = c.end();
|
||||
expect(summary.failedTests).toBe(1);
|
||||
expect(summary.terminalFileCounts).toEqual([1]);
|
||||
// exit 0 + a printed failure must not be trusted
|
||||
expect(strictTestExitCode(0, summary, 1)).toBe(1);
|
||||
});
|
||||
|
||||
it('records the terminal file count from the summary line', () => {
|
||||
const c = new BunTestOutputClassifier();
|
||||
c.write('Ran 0 tests across 1 files. [0.01s]\n');
|
||||
const summary = c.end();
|
||||
expect(summary.terminalFileCounts).toEqual([1]);
|
||||
// a fully diff-skipped single-file shard (0 tests, 1 file loaded) still
|
||||
// passes: 1 file ran, which is what was expected
|
||||
expect(strictTestExitCode(0, summary, 1)).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user