Files
gstack/test/plan-reality-receipts.test.ts
T
Garry Tanandcapy-ai[bot] ec1f2aa6a4 fix(redact): a short sha is not a phone number or a bc1 wallet; plan-reality receipt and pregate fixture flakes (#3998)
* fix(redact): a short sha is not a phone number or a bc1 wallet; plan-reality receipt and pregate fixture flakes (GSTA-279)

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

* docs: the review_loop_grant sha exemption and the fixture origin's maintenance setting (GSTA-279)

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

* docs(changelog): name the fragment for the branch and narrow its claim to the review_loop_grant position (GSTA-279)

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 00:01:33 +00:00

118 lines
6.4 KiB
TypeScript

/**
* G1 (#973): `gstack-plan-reality check` opens every file:line receipt. A
* receipt naming a missing file, or a line past the end of a real file, is
* unresolved and keeps the phase open; a receipt this checkout cannot judge
* (outside every root, a sha a shallow clone lacks) is reported, never
* blocking. The pure `checkRealityRows` is unchanged; only the bin resolves.
*/
import { 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';
import { parseRealityLines } from '../lib/plan-reality';
import { receiptTokens, resolveReceipts } from '../lib/plan-reality-receipts';
const ROOT = path.resolve(import.meta.dir, '..');
const BIN = path.join(ROOT, 'bin', 'gstack-plan-reality');
function gitOk(cwd: string, ...args: string[]) {
const r = spawnSync('git', args, { cwd, encoding: 'utf8', timeout: 10_000 });
if (r.status !== 0) throw new Error(`git ${args.join(' ')} exited ${r.status} (signal ${r.signal}, error ${r.error?.message ?? 'none'}): ${r.stderr}`);
return r;
}
function fixtureRepo(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'reality-receipts-'));
fs.mkdirSync(path.join(dir, 'lib'));
fs.writeFileSync(path.join(dir, 'lib', 'a.ts'), 'one\ntwo\nthree\n');
gitOk(dir, 'init', '-q');
return dir;
}
const rows = (receipts: Record<string, string>) => ['premise-table', 'already-done', 'surface-check', 'binding-decisions', 'numbers', 'deferred-asks', 'pr-count']
.map(id => `REALITY: ${id} pass checked ${receipts[id] ?? 'lib/a.ts:1'}`).join('\n') + '\n';
function check(cwd: string, review: string, extra: string[] = []) {
const r = spawnSync('bun', [BIN, 'check', '--phase', 'eng', '--session-kind', 'unattended', ...extra, '-'], { cwd, input: review, encoding: 'utf8', timeout: 30_000 });
return { code: r.status, out: r.stdout, err: r.stderr };
}
describe('gstack-plan-reality check resolves receipts', () => {
test('a missing file and a line past EOF are unresolved and keep the phase open', () => {
const dir = fixtureRepo();
const r = check(dir, rows({ 'premise-table': 'lib/does-not-exist.ts:12', 'already-done': 'lib/a.ts:99' }));
expect(r.out).toContain('unresolved=premise-table,already-done');
expect(r.out).toContain('verdict=incomplete');
expect(r.out).toContain('RECEIPT_UNRESOLVED: premise-table lib/does-not-exist.ts:12 no such file');
expect(r.out).toContain('RECEIPT_UNRESOLVED: already-done lib/a.ts:99 line 99 is past the end (3 lines)');
expect(r.err).toContain('(REALITY_RECEIPT_UNRESOLVED)');
expect(r.code).toBe(1);
});
test('real receipts and ranges pass', () => {
const dir = fixtureRepo();
const r = check(dir, rows({ 'premise-table': 'lib/a.ts:1-3', 'already-done': 'lib/a.ts:2' }));
expect(r.out).toContain('verdict=complete');
expect(r.out).not.toContain('unresolved=');
expect(r.code).toBe(0);
});
test('receipts this checkout cannot judge are reported, never blocking', () => {
const dir = fixtureRepo();
const r = check(dir, rows({ 'premise-table': '~/notes/plan.md:1', 'already-done': 'gate.r1.json:1', 'surface-check': 'deadbeefcafe' }));
expect(r.out).toContain('verdict=complete');
expect(r.err.match(/^RECEIPT_UNVERIFIABLE: /gm)?.length).toBe(3);
expect(r.code).toBe(0);
});
test('--root adds a root and the default is the git top level', () => {
const dir = fixtureRepo();
const other = fixtureRepo();
fs.writeFileSync(path.join(other, 'lib', 'b.ts'), 'x\n');
const review = rows({ 'premise-table': 'lib/b.ts:1' });
expect(check(dir, review).out).toContain('unresolved=premise-table');
expect(check(dir, review, ['--root', dir, '--root', other]).out).toContain('verdict=complete');
expect(check(path.join(dir, 'lib'), rows({})).out).toContain('verdict=complete');
});
});
describe('resolveReceipts', () => {
test('every receipt token on a row is checked; host:port is not a receipt', () => {
expect(receiptTokens('see lib/a.ts:1 and lib/b.ts:2, served on localhost:3000')).toEqual(['lib/a.ts:1', 'lib/b.ts:2']);
const dir = fixtureRepo();
const out = resolveReceipts(parseRealityLines('REALITY: numbers pass ok lib/a.ts:1 lib/gone.ts:4'), { roots: [dir] });
expect(out.map(r => [r.receipt, r.outcome])).toEqual([['lib/a.ts:1', 'resolved'], ['lib/gone.ts:4', 'unresolved']]);
});
test('a symlink out of the root, a directory and an oversized file are never read', () => {
const dir = fixtureRepo();
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'reality-outside-'));
fs.writeFileSync(path.join(outside, 'secret.txt'), 'TOKEN=abc\n');
fs.symlinkSync(path.join(outside, 'secret.txt'), path.join(dir, 'lib', 'link.txt'));
fs.writeFileSync(path.join(dir, 'lib', 'big.txt'), 'x'.repeat(64));
const out = resolveReceipts(parseRealityLines('REALITY: numbers pass ok lib/link.txt:1 lib/:1 lib/big.txt:1'), { roots: [dir], maxBytes: 32 });
expect(out.map(r => r.outcome)).toEqual(['unverifiable', 'unresolved', 'unverifiable']);
expect(JSON.stringify(out)).not.toContain('TOKEN');
});
test('line 0, a reversed range, a dangling link and a path through a file are unresolved, never a crash', () => {
const dir = fixtureRepo();
fs.symlinkSync(path.join(dir, 'lib', 'gone.ts'), path.join(dir, 'lib', 'dangling.ts'));
const out = resolveReceipts(parseRealityLines('REALITY: numbers pass ok lib/a.ts:0 lib/a.ts:3-1 lib/dangling.ts:1 lib/a.ts/x.ts:1 lib/a.ts:1-3'), { roots: [dir] });
expect(out.map(r => r.outcome)).toEqual(['unresolved', 'unresolved', 'unresolved', 'unresolved', 'resolved']);
});
test('a commit sha in the repository resolves; an absent one is unverifiable', () => {
const dir = fixtureRepo();
gitOk(dir, 'add', '.');
gitOk(dir, '-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-qm', 'seed');
const sha = gitOk(dir, 'rev-parse', 'HEAD').stdout.trim();
// A bare sha is a receipt only with a hex letter in it (RECEIPT_RE reads an all-digit run as a number unless `commit ` or
// backticks mark it), and about one 10-character prefix in 110 is all digits (Free Tests 41669379416, 41790613651).
const receipt = sha.slice(0, Math.max(10, sha.search(/[a-f]/) + 1));
const out = resolveReceipts(parseRealityLines(`REALITY: numbers pass ok ${receipt} abcdef1234`), { roots: [dir] });
expect(out.map(r => r.outcome)).toEqual(['resolved', 'unverifiable']);
});
});