mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-13 20:07:08 +01:00
Merge remote-tracking branch 'origin/main' into gsta161/opus-lite-loadout
This commit is contained in:
commit
74c3b3db1f
14 files changed
+172
-20
No files matched your search
@@ -304,6 +304,71 @@ describe('gstack-evidence check', () => {
|
||||
expect(chk.status).toBe(0);
|
||||
});
|
||||
|
||||
// Value: protects=a repository that gitignores .bun/ still gets a fingerprint (GST-449): an exclude pathspec naming an ignored path made
|
||||
// `git add -A` exit 1, so gstack-wtree failed and every review-log start and evidence check lost its fingerprint; fails_when=gstack-wtree
|
||||
// exits non-zero or the lane records no wtree with .bun/ (or /.bun) in .gitignore; why_new=the GST-421 test covered only an unignored
|
||||
// .bun/ (harness-evals#148); seam=none; no_claim=Windows git
|
||||
test('GST-449: with .bun/ gitignored, gstack-wtree still fingerprints and a lane that writes .bun/ grades FRESH', () => {
|
||||
for (const pattern of ['.bun/', '/.bun']) {
|
||||
fs.writeFileSync(path.join(repoDir, '.gitignore'), `${pattern}\n`);
|
||||
git('add .gitignore');
|
||||
git(`commit -q -m "ignore ${pattern}"`);
|
||||
fs.mkdirSync(path.join(repoDir, '.bun'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repoDir, '.bun', 'x'), 'cache\n');
|
||||
const wt = runBin(path.join(ROOT, 'bin', 'gstack-wtree'), [], { cwd: repoDir });
|
||||
expect(wt.stderr).toBe('');
|
||||
expect(wt.status).toBe(0);
|
||||
expect(wt.stdout.trim()).toMatch(/^[0-9a-f]{40}$/);
|
||||
const r = run(['run', '--label', 'tests', '--', 'echo cache2 > .bun/y && echo green']);
|
||||
expect(r.status).toBe(0);
|
||||
expect(records().pop().wtree).toMatch(/^[0-9a-f]{40}$/);
|
||||
expect(run(['check', '--label', 'tests']).stdout).toContain('EVIDENCE: FRESH label=tests');
|
||||
fs.rmSync(path.join(repoDir, '.bun'), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Value: protects=a repository that ignores .bun/ while still tracking a file under it gets a fingerprint (GST-449 review): the
|
||||
// index-aware check-ignore called .bun not ignored, so the exclude pathspec came back and `git add -A` exited 1; fails_when=gstack-wtree
|
||||
// exits non-zero, or its tree differs from a plain `git add -A`, or an edit or delete of the tracked file leaves it unchanged;
|
||||
// why_new=the pattern loop above starts from an untracked .bun/ only; seam=none; no_claim=Windows git
|
||||
test('GST-449: an ignored .bun/ that still tracks a file fingerprints like a plain git add -A, and edits to the tracked file count', () => {
|
||||
fs.mkdirSync(path.join(repoDir, '.bun'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repoDir, '.bun', 'tracked'), 'v1\n');
|
||||
git('add -f .bun/tracked');
|
||||
git('commit -q -m "track .bun/tracked"');
|
||||
fs.writeFileSync(path.join(repoDir, '.gitignore'), '.bun/\n');
|
||||
git('add .gitignore');
|
||||
git('commit -q -m "ignore .bun/"');
|
||||
fs.writeFileSync(path.join(repoDir, '.bun', 'cache'), 'cache\n');
|
||||
const wtree = () => {
|
||||
const wt = runBin(path.join(ROOT, 'bin', 'gstack-wtree'), [], { cwd: repoDir });
|
||||
expect(wt.stderr).toBe('');
|
||||
expect(wt.status).toBe(0);
|
||||
return wt.stdout.trim();
|
||||
};
|
||||
const plain = () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-wtree-idx-'));
|
||||
try {
|
||||
const env = { ...process.env, GIT_INDEX_FILE: path.join(dir, 'index') };
|
||||
spawnSync('git', ['read-tree', 'HEAD'], { cwd: repoDir, env, timeout: 10_000 });
|
||||
spawnSync('git', ['add', '-A'], { cwd: repoDir, env, timeout: 10_000 });
|
||||
return spawnSync('git', ['write-tree'], { cwd: repoDir, env, encoding: 'utf-8', timeout: 10_000 }).stdout.trim();
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
const clean = wtree();
|
||||
expect(clean).toBe(plain());
|
||||
fs.writeFileSync(path.join(repoDir, '.bun', 'tracked'), 'v2\n');
|
||||
const edited = wtree();
|
||||
expect(edited).not.toBe(clean);
|
||||
expect(edited).toBe(plain());
|
||||
fs.rmSync(path.join(repoDir, '.bun', 'tracked'));
|
||||
const deleted = wtree();
|
||||
expect(deleted).not.toBe(clean);
|
||||
expect(deleted).toBe(plain());
|
||||
});
|
||||
|
||||
test('GST-421: a lane that writes untracked source grades STALE and the reason names the path', () => {
|
||||
const r = run(['run', '--label', 'tests', '--', 'mkdir -p src && echo "export {}" > src/new.ts && echo green']);
|
||||
expect(r.status).toBe(0);
|
||||
|
||||
@@ -22,6 +22,7 @@ const GATE = 'node scripts/remote-gate.js';
|
||||
const GATE_SCRIPT = [
|
||||
"const fs = require('node:fs');",
|
||||
"fs.writeFileSync('../gate-env.json', JSON.stringify({ tree: process.env.PREGATE_TREE, cap: process.env.PREGATE_SPEND_CAP_USD, timeout: process.env.PREGATE_TIMEOUT_S, files: fs.readFileSync(process.env.PREGATE_FILES_FILE, 'utf8') }));",
|
||||
"if (process.env.GATE_QUIET_MS) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Number(process.env.GATE_QUIET_MS));",
|
||||
"if (process.env.GATE_SILENT) process.exit(0);",
|
||||
"const spent = process.env.GATE_SPENT ? ` spent=${process.env.GATE_SPENT}` : '';",
|
||||
"console.log(`PREGATE_REMOTE: tree=${process.env.GATE_TREE || process.env.PREGATE_TREE} tests=${process.env.GATE_TESTS ?? 3} failed=${process.env.GATE_FAILED ?? 0}${spent}`);",
|
||||
@@ -187,6 +188,22 @@ describe('gstack-pregate --remote (C-c)', () => {
|
||||
expect(fs.existsSync(path.join(repo.root, 'gate-env.json'))).toBe(false);
|
||||
}, 60_000);
|
||||
|
||||
// Value: protects=a remote gate that waits quietly on CI is bounded by its deadline (--timeout, else timeout_s), not killed by the
|
||||
// test-output stall timer; a gate past its deadline still stays uncleared (GST-431); fails_when=the stall timer kills a quiet gate
|
||||
// inside its deadline, or a gate past --timeout clears; why_new=a CI-waiting gate died at 300 s on qm (CAPY-1); seam=fake gate;
|
||||
// no_claim=the Ubicloud VM
|
||||
test('a gate that is quiet past the stall timeout but inside its deadline clears; past --timeout it stays uncleared', () => {
|
||||
const repo = gated();
|
||||
const quiet = remote(repo, ['--remote', '--allow-repo-commands', '--spend-cap', '2', '--stall-timeout', '1', '--timeout', '60'], { GATE_QUIET_MS: '2500' });
|
||||
expect(quiet.status).toBe(EXIT.ok);
|
||||
expect(pregateFile(repo).verdict).toBe('requires-remote cleared');
|
||||
const late = remote(repo, ['--remote', '--allow-repo-commands', '--spend-cap', '2', '--timeout', '2'], { GATE_QUIET_MS: '8000' });
|
||||
expect(late.status).toBe(EXIT.fail);
|
||||
expect(lanes(repo)).toMatchObject({ status: 'incomplete', code: 'PREGATE_INCOMPLETE' });
|
||||
expect(lanes(repo).detail).toContain('the remote gate did not finish (timed out after 2 s, --timeout)');
|
||||
expect(pregateFile(repo).requires_remote[0].receipt).toBeNull();
|
||||
}, 60_000);
|
||||
|
||||
test('a wrong tree, zero tests, a failure, a missing line or a non-zero exit leaves the obligation uncleared and the check incomplete; unreported spend counts at the cap', () => {
|
||||
const repo = gated();
|
||||
const cases: Array<[Record<string, string>, string]> = [
|
||||
|
||||
@@ -91,6 +91,28 @@ describe('supervise: process group, log file, stall and deadline', () => {
|
||||
expect(r.lines.filter(l => l.startsWith('PREGATE_HEARTBEAT: fixture running ')).length).toBeGreaterThanOrEqual(2);
|
||||
expect(r.lines.some(l => l.startsWith('PREGATE_TEST:'))).toBe(false);
|
||||
});
|
||||
|
||||
// Value: protects=a remote gate's heartbeat says what the gate last printed (GST-431); fails_when=the heartbeat for a gate that
|
||||
// reports its CI wait shows only "last file none reported"; why_new=a CI-waiting gate looked dead on qm (CAPY-1); seam=none; no_claim=none
|
||||
test('with lastLine, a heartbeat names the command\'s last output line', async () => {
|
||||
const dir = scratch();
|
||||
const r = await run(dir, 'echo "waiting on run 7 (in_progress)"; sleep 1.2; echo done', { heartbeatMs: 300, lastLine: true });
|
||||
expect(r.outcome).toBe('exited');
|
||||
expect(r.lines.some(l => l.startsWith('PREGATE_HEARTBEAT: fixture running ') && l.includes('last output: waiting on run 7 (in_progress)'))).toBe(true);
|
||||
const redraw = await run(dir, "printf 'run 7 queued\\r\\033[2Krun 7 in_progress\\n'; sleep 1.2", { heartbeatMs: 300, lastLine: true });
|
||||
expect(redraw.lines.some(l => l.endsWith('last output: run 7 in_progress'))).toBe(true);
|
||||
expect(redraw.lines.some(l => l.includes('\x1b') || l.includes('\r'))).toBe(false);
|
||||
});
|
||||
|
||||
// Value: protects=reading the last line is best effort: a log that disappears mid-run never throws out of the supervisor's timer, which would
|
||||
// abandon the gate's deadline and cleanup (GST-431 review); fails_when=lastLine sampling throws ENOENT; why_new=lastLine is new; seam=none; no_claim=none
|
||||
test('with lastLine, a log removed mid-run still ends in a normal outcome', async () => {
|
||||
const dir = scratch();
|
||||
const log = path.join(dir, 'run.log');
|
||||
fs.writeFileSync(log, '');
|
||||
const r = await supervise({ command: `echo hi; sleep 0.4; rm -f "${log}"; sleep 1.2; echo done`, cwd: dir, env: process.env, log, deadline: Date.now() + 30_000, stallMs: 0, graceMs: 300, pollMs: 50, heartbeatMs: 300, lastLine: true, label: 'fixture', progress: () => {} });
|
||||
expect(r.outcome).toBe('exited');
|
||||
});
|
||||
});
|
||||
|
||||
describe('supervise: per-file progress adapters', () => {
|
||||
|
||||
Reference in new issue
Block a user