mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-11 11:10:13 +01:00
classifyOutsideReview() returns separate execution, findings and verdict fields from text, stderr and exit status; reason codes, anchors and fix actions live in lib/gate-outcomes.ts. validateOutsideReview() stays as the text-only wrapper and the two-argument CLI keeps exit 0/1/2 for importers and installed skills that predate a re-render. The --verdict form (implied by --stderr/--exit) prints VERDICT/FINDINGS/REASON and exits 0/3/4/1; the generated outside-voice caller moves to it and branches on each status (unverified prints OUTSIDE_STATUS: unverified, never completed). Adds docs/troubleshooting.md with one anchored section per reason code, the anchor test, and the gate-tail lint (query scripts may not swallow failures, gate calls may not end in || true, other best-effort tails say why).
72 lines
3.4 KiB
TypeScript
72 lines
3.4 KiB
TypeScript
/**
|
|
* INV-1 lint: a check that could not run must never look like "nothing found".
|
|
* 1. The learnings/timeline query scripts may not end in `2>/dev/null || exit 0`
|
|
* (a missing bun read as "no learnings recorded").
|
|
* 2. No template or resolver may wrap a gate binary in `|| true`, which turns
|
|
* "could not run" into success.
|
|
* 3. Any other `2>/dev/null || exit 0` tail in bin/ is a deliberate best-effort
|
|
* exit and says why on the same line (`# best-effort: <reason>`).
|
|
*/
|
|
import { describe, expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const QUERY_SCRIPTS = ['bin/gstack-learnings-search', 'bin/gstack-timeline-read'];
|
|
const GATE_CALLS = ['outside-review-result.ts', 'gstack-learnings-search', 'gstack-timeline-read',
|
|
'_gstack_codex_auth_probe', '_gstack_codex_model_probe', '_gstack_codex_sandbox_preflight'];
|
|
const SILENT_EXIT = /2>\/dev\/null \|\| exit 0\b/;
|
|
|
|
function walk(dir: string, keep: (file: string) => boolean, out: string[] = []): string[] {
|
|
for (const entry of fs.readdirSync(path.join(ROOT, dir), { withFileTypes: true })) {
|
|
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
const rel = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) walk(rel, keep, out);
|
|
else if (keep(rel)) out.push(rel);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function sources(): string[] {
|
|
const templates = fs.readdirSync(ROOT, { withFileTypes: true })
|
|
.filter(d => d.isDirectory() && !d.name.startsWith('.') && !['node_modules', 'test'].includes(d.name))
|
|
.flatMap(d => walk(d.name, f => f.endsWith('.tmpl')));
|
|
return [...templates, ...walk('scripts/resolvers', f => f.endsWith('.ts')), ...walk('bin', () => true)];
|
|
}
|
|
|
|
describe('gate tails never hide a check that did not run', () => {
|
|
test('query scripts do not swallow a failed run as an empty result', () => {
|
|
const offenders = QUERY_SCRIPTS.filter(f => SILENT_EXIT.test(fs.readFileSync(path.join(ROOT, f), 'utf8')));
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
|
|
test('no gate binary call is wrapped in || true', () => {
|
|
const offenders: string[] = [];
|
|
for (const rel of sources()) {
|
|
fs.readFileSync(path.join(ROOT, rel), 'utf8').split('\n').forEach((line, i) => {
|
|
if (/^\s*(?:#|\/\/|\*)/.test(line)) return;
|
|
if (GATE_CALLS.some(g => line.includes(g)) && /\|\|\s*true\b/.test(line)) offenders.push(`${rel}:${i + 1}: ${line.trim()}`);
|
|
});
|
|
}
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
|
|
test('other best-effort exits in bin/ carry a same-line reason', () => {
|
|
const offenders: string[] = [];
|
|
for (const rel of walk('bin', () => true)) {
|
|
if (QUERY_SCRIPTS.includes(rel)) continue;
|
|
fs.readFileSync(path.join(ROOT, rel), 'utf8').split('\n').forEach((line, i) => {
|
|
if (SILENT_EXIT.test(line) && !/#\s*best-effort:\s*\S/.test(line)) offenders.push(`${rel}:${i + 1}: ${line.trim()}`);
|
|
});
|
|
}
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
|
|
test('negative controls: each rule flags a planted offender', () => {
|
|
expect(SILENT_EXIT.test('" 2>/dev/null || exit 0')).toBe(true);
|
|
expect(/#\s*best-effort:\s*\S/.test('mkdir -p "$X" 2>/dev/null || exit 0 # best-effort: spool is optional')).toBe(true);
|
|
const line = '$GSTACK_BIN/gstack-learnings-search --limit 10 2>/dev/null || true';
|
|
expect(GATE_CALLS.some(g => line.includes(g)) && /\|\|\s*true\b/.test(line)).toBe(true);
|
|
});
|
|
});
|