mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-11 15:39:04 +02:00
spawnSync/execSync/Bun.spawnSync BLOCK the main thread, so bun's in-process per-test timeout can never fire while one waits — a hung child (stdin read, network probe, dead daemon) wedges the whole shard until the runner's external wall-clock SIGKILL. This exact class reached main: free-tests run 33262077256, test/gstack-memory-ingest.test.ts (normally 2.3s) held shard 2 at the 360s wall while its five siblings finished in ~65s. Mechanical sweep in two waves (12 + 4 fan-out agents, every edit verified against its call site): default timeout: 30_000 (matches the free runner's per-test budget), 120_000 for genuinely slow ops (installs, builds, playwright, provider CLIs), helper wrappers fixed ONCE where call sites route through them. Sites that only LOOK like calls (string fixtures, grep needles, comments) were skipped with reasons — the enforcement commit that follows marks them exempt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
143 lines
6.6 KiB
TypeScript
143 lines
6.6 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { execSync } from 'child_process';
|
|
|
|
/**
|
|
* Wiring scanner: every tracker-TEXT read (PR/issue bodies, comment bodies,
|
|
* issue titles judged by the model) in skill templates, resolvers, and runtime
|
|
* reference docs must flow through bin/gstack-issue-guard. Same posture as
|
|
* test/egress-receipt-wiring.test.ts: a regex tripwire behind a centralized
|
|
* helper — it catches drift, it is not the enforcement itself.
|
|
*
|
|
* A line is compliant when it mentions gstack-issue-guard, or when the
|
|
* (file, reason) pair is enumerated in SCANNER_EXEMPT below. Exemptions are
|
|
* REASONED — a new raw read needs either the guard or an entry here explaining
|
|
* why it is not model-context ingress.
|
|
*/
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
|
|
// Tracker-TEXT read shapes. Field-list/state-routing fetches (e.g.
|
|
// `--json number,state,title` used to route on state) are deliberately not
|
|
// matched — see the pattern notes.
|
|
const READ_PATTERNS: { name: string; re: RegExp }[] = [
|
|
// The field list must contain `body` immediately after --json (comma list),
|
|
// so `--json number` followed by unrelated prose mentioning "body" (e.g.
|
|
// ship's REST write fallback `-F body=@file`) does not over-match.
|
|
{ name: 'gh pr body read', re: /gh pr view[^\n|]*--json[\s"']*[a-z,]*\bbody\b/ },
|
|
{ name: 'gh issue body read', re: /gh issue view[^\n|]*--json[\s"']*[a-z,]*\bbody\b/ },
|
|
{ name: 'gh comment-body api read', re: /gh api[^\n]*\/(pulls|issues)\/[^\n]*comments/ },
|
|
// Titles are tracker text when the MODEL judges them (dedupe similarity);
|
|
// `gh issue list` with a title field is matched, `gh pr view --json title`
|
|
// (mechanical title-prefix rewrite) is not.
|
|
{ name: 'gh issue-list title read', re: /gh issue list[^\n]*--json[\s"']*[a-z,]*\btitle\b/ },
|
|
{ name: 'glab body/description read', re: /glab mr view[^\n]*(description|--json[\s"']*[a-z,]*\bbody\b)/ },
|
|
// Flagless `gh pr view` / `gh issue view <n>` print the FULL body in their
|
|
// default human output — a raw read without --json is still a body read.
|
|
// (?![`/]) excludes prose mentions like "If `gh pr view` / `glab mr view` fails".
|
|
{ name: 'gh flagless body read', re: /gh (pr|issue) view(?![`/])(?![^\n]*--json)(?![^\n]*-q )[^\n]*/ },
|
|
];
|
|
|
|
// (file, pattern-name) exemptions with reasons. Keep every entry REASONED.
|
|
const SCANNER_EXEMPT: { file: string; pattern: string; reason: string }[] = [
|
|
{
|
|
file: 'review/greptile-triage.md',
|
|
pattern: 'gh comment-body api read',
|
|
reason:
|
|
'raw fetch lands in /tmp json FILES (metadata/body split); body text is read into context only via the gstack-issue-guard --stdin pipes documented in the same file',
|
|
},
|
|
{
|
|
file: 'document-release/sections/release-body.md.tmpl',
|
|
pattern: 'gh pr body read',
|
|
reason:
|
|
'two-artifact flow: this is the RAW write-back tempfile fetch; the context read is enveloped at step 1b and a banner tripwire guards the write side',
|
|
},
|
|
{
|
|
file: 'document-release/sections/release-body.md.tmpl',
|
|
pattern: 'glab body/description read',
|
|
reason: 'two-artifact flow (GitLab twin of the raw write-back fetch); context read enveloped at step 1b',
|
|
},
|
|
];
|
|
|
|
function trackedFiles(): string[] {
|
|
const out = execSync('git ls-files', { cwd: ROOT, encoding: 'utf-8', maxBuffer: 32 * 1024 * 1024, timeout: 30_000 });
|
|
return out
|
|
.split('\n')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
.filter(
|
|
(f) =>
|
|
// Sources of truth only: templates, template sections, resolvers, and
|
|
// runtime reference docs inside skill dirs. Generated SKILL.md files
|
|
// are derived from these and would double-report.
|
|
(f.endsWith('.md.tmpl') ||
|
|
f.endsWith('SKILL.md.tmpl') ||
|
|
/^scripts\/resolvers\/.*\.ts$/.test(f) ||
|
|
/^review\/[^/]+\.md$/.test(f)) &&
|
|
!f.endsWith('SKILL.md'),
|
|
);
|
|
}
|
|
|
|
describe('tracker-text wiring scanner', () => {
|
|
test('every tracker-text read flows through gstack-issue-guard (or carries a reasoned exemption)', () => {
|
|
const violations: string[] = [];
|
|
for (const rel of trackedFiles()) {
|
|
const abs = path.join(ROOT, rel);
|
|
if (!fs.existsSync(abs)) continue;
|
|
const lines = fs.readFileSync(abs, 'utf-8').split('\n');
|
|
lines.forEach((line, i) => {
|
|
for (const { name, re } of READ_PATTERNS) {
|
|
if (!re.test(line)) continue;
|
|
if (line.includes('gstack-issue-guard')) continue;
|
|
// Multi-line shell pipeline: a read whose continuation lines pipe
|
|
// into the guard is compliant (spec's dedupe block ends in `\`).
|
|
if (line.trimEnd().endsWith('\\')) {
|
|
const continuation = lines.slice(i + 1, i + 4).join('\n');
|
|
if (continuation.includes('gstack-issue-guard')) continue;
|
|
}
|
|
const exempt = SCANNER_EXEMPT.some((e) => e.file === rel && e.pattern === name);
|
|
if (exempt) continue;
|
|
violations.push(`${rel}:${i + 1} [${name}] ${line.trim().slice(0, 120)}`);
|
|
}
|
|
});
|
|
}
|
|
if (violations.length > 0) {
|
|
throw new Error(
|
|
`Raw tracker-text read(s) outside gstack-issue-guard:\n ${violations.join('\n ')}\n\n` +
|
|
`Fix: pipe the read through bin/gstack-issue-guard (--stdin for pre-fetched text), or — ` +
|
|
`if this is genuinely not model-context ingress (mechanical rewrite, state routing, raw ` +
|
|
`write-back artifact) — add a REASONED entry to SCANNER_EXEMPT in this file.`,
|
|
);
|
|
}
|
|
expect(violations).toEqual([]);
|
|
});
|
|
|
|
test('exemption entries stay live (a stale exemption means the site moved — re-audit it)', () => {
|
|
for (const e of SCANNER_EXEMPT) {
|
|
const abs = path.join(ROOT, e.file);
|
|
expect(fs.existsSync(abs)).toBe(true);
|
|
const content = fs.readFileSync(abs, 'utf-8');
|
|
const pat = READ_PATTERNS.find((p) => p.name === e.pattern)!;
|
|
const hasMatch = content.split('\n').some((l) => pat.re.test(l) && !l.includes('gstack-issue-guard'));
|
|
expect(hasMatch).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('the guarded sites actually mention the guard (wiring, not just lib existence)', () => {
|
|
const mustMention = [
|
|
'review/greptile-triage.md',
|
|
'document-release/sections/release-body.md.tmpl',
|
|
'spec/SKILL.md.tmpl',
|
|
// Carved: the pr-body trust-envelope read lives in Step 3.5c, which moved
|
|
// into the on-demand readiness-gate section.
|
|
'land-and-deploy/sections/readiness-gate.md.tmpl',
|
|
'scripts/resolvers/review.ts',
|
|
];
|
|
for (const rel of mustMention) {
|
|
const content = fs.readFileSync(path.join(ROOT, rel), 'utf-8');
|
|
expect(content).toContain('gstack-issue-guard');
|
|
}
|
|
});
|
|
});
|