Files
gstack/test/review-dispatch-plan-timeout.test.ts
T
Garry Tanandcapy-ai[bot] 713c672d28 fix(review-exclude): fixed git calls for the name scan and binary read; dispatch names a plan timeout (GST-1442) (#4040)
* fix(review-exclude): one git grep per tree through a scratch index, batched blob reads; dispatch prints REVIEW_PLAN_STOPPED (GST-1442)

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

* fix(review-dispatch): the plan limit lives beside the scan deadline it must stay a minute above (GST-1442)

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

* fix: pre-landing review fixes

Batched blob prefixes are copied, so each 64 MB cat-file --batch buffer is released after its group instead of staying alive until every blob is read.

Review-Finding: v1/outside-voice#1 reader=outside-voice
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(review-policy): a plan past the pass limit stops with REVIEW_PLAN_STOPPED (GST-1442)

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-11-05 17:40:15 +00:00

38 lines
2.9 KiB
TypeScript

/**
* gstack-review-dispatch pass stops with a REVIEW_PLAN_STOPPED line when gstack-review-plan runs past its limit (GST-1442):
* on harness-evals #279 the plan took about 214 s against the pass's 180 s, and the pass was killed with no output at all.
*/
import { afterEach, describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
const DISPATCH = path.join(import.meta.dir, '..', 'bin', 'gstack-review-dispatch');
const dirs: string[] = [];
afterEach(() => { while (dirs.length) fs.rmSync(dirs.pop()!, { recursive: true, force: true }); });
describe('pass names the plan step and its limit when the plan does not finish (GST-1442)', () => {
// Value: protects=a plan past the pass's limit ends in a line naming the step, the reason and the limit, with no manifest;
// fails_when=the pass treats the killed plan like any failed plan and exits 1 with empty output (the #279 silent kill);
// why_new=GST-1442; seam=gstack-review-dispatch pass with GSTACK_REVIEW_PLAN_TIMEOUT_MS=1; no_claim=the 180 s default
test('a plan that runs past the limit prints REVIEW_PLAN_STOPPED with reason=timeout and writes no manifest', () => {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'rd-plan-timeout-')));
dirs.push(root);
const work = path.join(root, 'work');
const env = { ...process.env, GSTACK_STATE_ROOT: path.join(root, 'state'), GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t', GIT_CONFIG_GLOBAL: path.join(root, 'gitconfig') };
fs.writeFileSync(path.join(root, 'gitconfig'), '[commit]\n\tgpgsign = false\n');
const sh = (cwd: string, c: string) => { const r = spawnSync('bash', ['-c', c], { cwd, env, encoding: 'utf8', timeout: 60_000 }); if (r.status !== 0) throw new Error(`${c}: ${r.stderr}`); };
sh(root, 'git init -q --bare -b main origin.git && git clone -q origin.git work 2>/dev/null');
sh(work, "mkdir src && echo 'export const a = 1;' > src/a.ts && git add -A && git commit -qm base && git push -q origin main && git checkout -qb feat && echo 'export const b = 2;' >> src/a.ts && git commit -qam feat");
const out = path.join(root, 'run');
const r = spawnSync(DISPATCH, ['pass', '--base', 'main', '--out', out, '--pass', 'timeout-p1'], { cwd: work, env: { ...env, GSTACK_REVIEW_PLAN_TIMEOUT_MS: '1' }, encoding: 'utf8', timeout: 120_000 });
expect(r.status, r.stdout + r.stderr).toBe(1);
expect(r.stderr).toContain('gstack-review-dispatch pass: gstack-review-plan did not finish inside the pass');
expect(r.stderr).toContain('step="gstack-review-plan --base main" reason=timeout timeout_ms=1 signal=SIGTERM');
expect(r.stderr.trimEnd().endsWith('(REVIEW_PLAN_STOPPED)')).toBe(true);
expect(r.stdout).not.toContain('MANIFEST:');
expect(fs.existsSync(path.join(out, 'pass-manifest.json'))).toBe(false);
}, 180_000);
});