mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 08:23:59 +01:00
* pregate: a conflict-markers guard refuses merge markers on the lines a branch adds (GST-1190) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(pregate): conflict-markers reads the pushed commit, fixed git prefixes and real declarations Review fixes for GST-1190: read HEAD and the working tree, force a/ b/ prefixes and unquoted paths, skip untracked files, require the fixture declaration on its own line near the top, refuse conflicts inside code fences in Markdown, run the test spawn through bunIsolationArgs for Windows. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(conflict-markers): type the git config env rows 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>
294 lines
21 KiB
TypeScript
294 lines
21 KiB
TypeScript
/**
|
|
* Field wave CD1/CD2 config keys in `.gstack/pregate.json`: the repo's guards
|
|
* (commands a lane must pass before it reports done), the serial-test
|
|
* patterns the lanes check runs in their own process, and the non-executing
|
|
* call-plan command a paid gate is priced from. Validation stays strict:
|
|
* every key is typed, globs are contained, commands are one line.
|
|
*/
|
|
import { describe, expect, test } from 'bun:test';
|
|
import { parseConfig, validateConfig } from '../lib/pregate/config';
|
|
import { CHECK_IDS } from '../lib/pregate/index';
|
|
|
|
const codes = (c: unknown) => validateConfig(c, CHECK_IDS).map(e => `${e.code}:${e.path}`);
|
|
|
|
describe('pregate config: price_file', () => {
|
|
test('a contained price_file is valid and parsed; an absolute, escaping or non-string one is PREGATE_CONFIG_INVALID', () => {
|
|
expect(codes({ price_file: 'evals/gate-calls.txt' })).toEqual([]);
|
|
expect(parseConfig(JSON.stringify({ price_file: 'evals/gate-calls.txt' })).config.price_file).toBe('evals/gate-calls.txt');
|
|
expect(parseConfig('{}').config.price_file).toBeNull();
|
|
expect(codes({ price_file: '/etc/passwd' })).toEqual(['PREGATE_CONFIG_INVALID:price_file']);
|
|
expect(codes({ price_file: '../x' })).toEqual(['PREGATE_CONFIG_INVALID:price_file']);
|
|
expect(codes({ price_file: 3 })).toEqual(['PREGATE_CONFIG_INVALID:price_file']);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CD1: guards, unknown flags, the lanes check's Bun and serial files
|
|
// ---------------------------------------------------------------------------
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { afterEach } from 'bun:test';
|
|
import { EXIT } from '../lib/headless-artifacts';
|
|
import { capyHost, type CapyHost } from './helpers/capy-host';
|
|
import { BASE_PREGATE, cleanup, commit, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/pregate-fixture';
|
|
|
|
const repos: FixtureRepo[] = [];
|
|
const hosts: CapyHost[] = [];
|
|
afterEach(() => { while (repos.length) cleanup(repos.pop()!); while (hosts.length) hosts.pop()!.cleanup(); });
|
|
const readOut = (repo: FixtureRepo) => JSON.parse(fs.readFileSync(path.join(repo.work, '.gstack/tmp/pregate.json'), 'utf8'));
|
|
const checkRow = (repo: FixtureRepo, id: string) => readOut(repo).checks.find((c: any) => c.id === id);
|
|
|
|
const CHECK_SERIAL = [
|
|
"const fs = require('node:fs');",
|
|
"const rows = new Set(fs.readFileSync('scripts/serial-files.tsv', 'utf8').split('\\n').map(l => l.split('\\t')[0]).filter(Boolean));",
|
|
"const missing = fs.readdirSync('test').filter(f => f.endsWith('.serial.test.js')).map(f => `test/${f}`).filter(f => !rows.has(f));",
|
|
"for (const f of missing) console.log(`${f} is a serial test file with no row in scripts/serial-files.tsv.`);",
|
|
'process.exit(missing.length ? 1 : 0);',
|
|
].join('\n') + '\n';
|
|
const SERIAL_GUARD = { id: 'serial-manifest', when: ['test/**/*.serial.test.js', 'scripts/serial-files.tsv'], command: 'node scripts/check-serial.js', fix: 'add a row for the file to scripts/serial-files.tsv' };
|
|
const SERIAL_TEST = "const test = require('node:test');\ntest('serial', () => {});\n";
|
|
|
|
/** The base branch gains the manifest, its check script and the given pregate config. */
|
|
function guardedFixture(pregate: Record<string, unknown>): FixtureRepo {
|
|
const repo = makeFixtureRepo({ pregate, baseFiles: { 'scripts/check-serial.js': CHECK_SERIAL, 'scripts/serial-files.tsv': 'path\treason\n' } });
|
|
repos.push(repo);
|
|
return repo;
|
|
}
|
|
const pregate = (repo: FixtureRepo, args: string[], env: Record<string, string> = {}) => runBin(repo, 'gstack-pregate', ['--base', 'main', ...args], env);
|
|
|
|
describe('guards (CD1)', () => {
|
|
test('a serial test that lands without its manifest row fails pregate naming the guard and its fix; adding the row clears it', () => {
|
|
const repo = guardedFixture({ ...BASE_PREGATE, guards: [SERIAL_GUARD] });
|
|
commit(repo, { 'test/widget-env.serial.test.js': SERIAL_TEST });
|
|
const r = pregate(repo, ['--allow-repo-commands', '--stage', 'preflight']);
|
|
expect(r.status).toBe(EXIT.fail);
|
|
expect(r.out).toMatch(/^preflight {2}guards {4}fail .*serial-manifest.*\(PREGATE_GUARD_FAILED\)/m);
|
|
expect(r.out).toContain('guards: GUARD: serial-manifest exit=1');
|
|
expect(r.out).toContain('test/widget-env.serial.test.js is a serial test file with no row in scripts/serial-files.tsv.');
|
|
expect(r.out).toContain('guards: fix: serial-manifest: add a row for the file to scripts/serial-files.tsv');
|
|
expect(r.err).toContain('(PREGATE_GUARD_FAILED)');
|
|
commit(repo, { 'scripts/serial-files.tsv': 'path\treason\ntest/widget-env.serial.test.js\tglobal-state\n' });
|
|
const ok = pregate(repo, ['--allow-repo-commands', '--stage', 'preflight']);
|
|
expect(ok.status).toBe(EXIT.ok);
|
|
expect(ok.out).toMatch(/^preflight {2}guards {4}pass .*1 guard\(s\) passed: serial-manifest/m);
|
|
});
|
|
|
|
test('without --allow-repo-commands a selected guard is incomplete and lists its command; an unmatched guard does not run', () => {
|
|
const repo = guardedFixture({ ...BASE_PREGATE, guards: [SERIAL_GUARD, { id: 'never', when: ['nothing/**'], command: 'false' }] });
|
|
commit(repo, { 'test/widget-env.serial.test.js': SERIAL_TEST });
|
|
const r = pregate(repo, ['--stage', 'preflight', '--only', 'guards']);
|
|
expect(r.status).toBe(EXIT.fail);
|
|
expect(r.out).toMatch(/^preflight {2}guards {4}incomplete .*\(REPO_COMMANDS_NOT_ALLOWED\)/m);
|
|
expect(r.out).toContain('guards: COMMAND: serial-manifest="node scripts/check-serial.js"');
|
|
expect(r.out).not.toContain('COMMAND: never=');
|
|
expect(r.out).toContain('GSTACK_RESULT: skill=pregate status=incomplete');
|
|
});
|
|
|
|
test('each guard runs in its own process with its timeout: a failing guard does not stop the next, a timed-out guard never passes', () => {
|
|
const repo = guardedFixture({ ...BASE_PREGATE, guards: [
|
|
{ id: 'first', when: ['lib/**'], command: 'echo first-ran; exit 3' },
|
|
{ id: 'second', when: ['lib/**'], command: 'touch ../second-ran' },
|
|
{ id: 'slow', when: ['lib/**'], command: 'sleep 20', timeout_s: 1 },
|
|
] });
|
|
commit(repo, { 'lib/util.js': "exports.add = (a, b) => a + b + 0;\n" });
|
|
const r = pregate(repo, ['--allow-repo-commands', '--stage', 'preflight', '--only', 'guards']);
|
|
expect(r.status).toBe(EXIT.fail);
|
|
expect(fs.existsSync(path.join(repo.root, 'second-ran'))).toBe(true);
|
|
expect(r.out).toContain('guards: GUARD: first exit=3');
|
|
expect(r.out).toContain('guards: GUARD: slow timed out after 1 s');
|
|
const row = checkRow(repo, 'guards');
|
|
expect(row.status).toBe('fail');
|
|
expect(row.detail).toContain('first');
|
|
expect(row.detail).not.toContain('second');
|
|
});
|
|
|
|
test('a timed-out guard with no failing guard beside it is incomplete, never pass', () => {
|
|
const slowOnly = guardedFixture({ ...BASE_PREGATE, guards: [{ id: 'slow', when: ['lib/**'], command: 'sleep 20', timeout_s: 1 }] });
|
|
commit(slowOnly, { 'lib/util.js': "exports.add = (a, b) => a + b + 0;\n" });
|
|
expect(pregate(slowOnly, ['--allow-repo-commands', '--stage', 'preflight', '--only', 'guards']).status).toBe(EXIT.fail);
|
|
expect(checkRow(slowOnly, 'guards')).toMatchObject({ status: 'incomplete', code: 'PREGATE_INCOMPLETE' });
|
|
});
|
|
|
|
test('guards cannot be downgraded to warn; a guard needs an id, globs and a one-line command', () => {
|
|
expect(codes({ guards: [SERIAL_GUARD] })).toEqual([]);
|
|
expect(codes({ guards: [SERIAL_GUARD], warn: ['guards'] })).toEqual(['PREGATE_CONFIG_INVALID:warn']);
|
|
expect(codes({ guards: [{ id: 'bad id!', when: ['x'], command: 'true' }] })).toEqual(['PREGATE_CONFIG_INVALID:guards[0].id']);
|
|
expect(codes({ guards: [{ id: 'g', when: ['../x'], command: 'true' }] })).toEqual(['PREGATE_CONFIG_INVALID:guards[0].when[0]']);
|
|
expect(codes({ guards: [{ id: 'g', when: ['x'], command: 'a\nb' }] })).toEqual(['PREGATE_CONFIG_INVALID:guards[0].command']);
|
|
expect(codes({ guards: [{ id: 'g', when: ['x'], command: 'true', extra: 1 }] })).toEqual(['PREGATE_CONFIG_INVALID:guards[0].extra']);
|
|
expect(codes({ guards: [{ id: 'g', when: ['x'], command: 'true' }, { id: 'g', when: ['y'], command: 'true' }] })).toEqual(['PREGATE_CONFIG_INVALID:guards[1].id']);
|
|
expect(codes({ serial_patterns: ['**/*.serial.test.*'] })).toEqual([]);
|
|
expect(codes({ serial_patterns: ['/abs/**'] })).toEqual(['PREGATE_CONFIG_INVALID:serial_patterns[0]']);
|
|
});
|
|
});
|
|
|
|
// Integration item 8: under the admission condition (GSTACK_CAPY=1, or a
|
|
// verdict exists for this checkout and project) the guards and lanes validate
|
|
// the install verdict as workflow admission does and return incomplete before
|
|
// launching anything; with neither, contributors and CI run as before.
|
|
describe('guards and lanes: install verdict admission (integration item 8)', () => {
|
|
const GSTACK = path.resolve(import.meta.dir, '..');
|
|
const { installVerdictPath } = require('../lib/install-verdict') as typeof import('../lib/install-verdict');
|
|
const head = () => require('node:child_process').spawnSync('git', ['-C', GSTACK, 'rev-parse', 'HEAD'], { encoding: 'utf8', timeout: 10_000 }).stdout.trim();
|
|
const guard = { id: 'marker', when: ['lib/**'], command: 'touch ../guard-ran' };
|
|
const verdict = (repo: FixtureRepo, fields: Record<string, unknown>) => {
|
|
const project = fs.realpathSync(repo.work);
|
|
const file = installVerdictPath(fs.realpathSync(GSTACK), repo.home, project);
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, JSON.stringify({ schema_version: 2, checkout: fs.realpathSync(GSTACK), project, head: head(), for: 'ship', required: ['claude', 'codex', 'patch'], pins_planning_only: false, ok: true, trailer: 'gstack: ok', min_revision: '', launch_path: '', time: '', ...fields }));
|
|
};
|
|
const run = (repo: FixtureRepo, env: Record<string, string> = {}) => pregate(repo, ['--allow-repo-commands', '--stage', 'preflight', '--only', 'guards'], { GSTACK_STATE_ROOT: repo.home, ...env });
|
|
const ran = (repo: FixtureRepo) => fs.existsSync(path.join(repo.root, 'guard-ran'));
|
|
|
|
test('GSTACK_CAPY=1 without a verdict: guards incomplete INSTALL_UNVERIFIED and nothing launched; without GSTACK_CAPY the guard runs', () => {
|
|
const repo = guardedFixture({ ...BASE_PREGATE, guards: [guard] });
|
|
commit(repo, { 'lib/util.js': 'exports.x = 1;\n' });
|
|
const r = run(repo, { GSTACK_CAPY: '1' });
|
|
expect(r.status).toBe(EXIT.fail);
|
|
expect(checkRow(repo, 'guards')).toMatchObject({ status: 'incomplete', code: 'INSTALL_UNVERIFIED' });
|
|
expect(checkRow(repo, 'guards').detail).toContain('install verdict: no install verdict at');
|
|
expect(ran(repo)).toBe(false);
|
|
expect(run(repo, { GSTACK_CAPY: '' }).status).toBe(EXIT.ok);
|
|
expect(ran(repo)).toBe(true);
|
|
});
|
|
|
|
test('an existing verdict is enforced without GSTACK_CAPY: ok:false and a stale HEAD are INSTALL_UNVERIFIED, a gone recorded Bun is BUN_RESOLUTION_DRIFT, a valid one launches its Bun', () => {
|
|
const repo = guardedFixture({ ...BASE_PREGATE, guards: [guard] });
|
|
commit(repo, { 'lib/util.js': 'exports.x = 1;\n' });
|
|
verdict(repo, { ok: false, trailer: 'gstack: fail pins' });
|
|
expect(run(repo).status).toBe(EXIT.fail);
|
|
expect(checkRow(repo, 'guards')).toMatchObject({ status: 'incomplete', code: 'INSTALL_UNVERIFIED' });
|
|
expect(checkRow(repo, 'guards').detail).toContain('the install verdict says: gstack: fail pins');
|
|
verdict(repo, { head: '0'.repeat(40) });
|
|
expect(run(repo).status).toBe(EXIT.fail);
|
|
expect(checkRow(repo, 'guards').detail).toContain('the install verdict is for another HEAD');
|
|
verdict(repo, { pins_planning_only: true, for: 'autoplan' });
|
|
expect(run(repo).status).toBe(EXIT.fail);
|
|
expect(checkRow(repo, 'guards').detail).toContain('planning-only');
|
|
verdict(repo, { bun_path: path.join(repo.root, 'gone/bun'), bun_version: '1.4.3' });
|
|
expect(run(repo).status).toBe(EXIT.fail);
|
|
expect(checkRow(repo, 'guards')).toMatchObject({ status: 'incomplete', code: 'BUN_RESOLUTION_DRIFT' });
|
|
expect(checkRow(repo, 'guards').detail).toContain(`recorded bun 1.4.3 at ${repo.root}/gone/bun is gone`);
|
|
expect(ran(repo)).toBe(false);
|
|
const stub = path.join(fs.realpathSync(repo.root), 'pinned', 'bun');
|
|
fs.mkdirSync(path.dirname(stub), { recursive: true });
|
|
fs.writeFileSync(stub, `#!/bin/sh\n[ "$1" = --version ] && { echo 1.4.2; exit 0; }\nexec '${fs.realpathSync(process.execPath)}' "$@"\n`, { mode: 0o755 });
|
|
verdict(repo, { bun_path: stub, bun_version: '1.4.2' });
|
|
const ok = run(repo);
|
|
expect(ok.status, ok.out).toBe(EXIT.ok);
|
|
expect(ok.out).toContain(`runtime: bun 1.4.2 at ${stub} (recorded by the install verdict)`);
|
|
expect(ran(repo)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('pregate config: sealed (CD4)', () => {
|
|
test('`gstack-pregate validate` accepts a sealed policy and names each bad field', () => {
|
|
expect(codes({ sealed: { paths: ['heldout/**'], transcripts: ['runs/**/transcript*.jsonl'], answer_markers: ['SEALED ANSWER:'], seed_patterns: ['seed=\\d+'], local_paths: true, visibility: 'private' } })).toEqual([]);
|
|
expect(codes({ sealed: { paths: 'heldout/**' } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.paths']);
|
|
expect(codes({ sealed: { seed_patterns: ['('] } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.seed_patterns[0]']);
|
|
expect(codes({ sealed: { visibility: 'internal' } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.visibility']);
|
|
expect(codes({ sealed: { block: true } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.block']);
|
|
expect(codes({ sealed: { paths: ['**/sealed/**'], allow_paths: ['**/sealed/*/MANIFEST.json', '**/protected/MANIFEST.json'], case_insensitive: true } })).toEqual([]);
|
|
expect(codes({ sealed: { allow_paths: '**/sealed/*/MANIFEST.json' } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.allow_paths']);
|
|
expect(codes({ sealed: { allow_paths: ['../x/MANIFEST.json'] } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.allow_paths[0]']);
|
|
expect(codes({ sealed: { allow_paths: Array.from({ length: 65 }, (_, i) => `s${i}/MANIFEST.json`) } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.allow_paths']);
|
|
expect(codes({ sealed: { allow_paths: ['**', 'sealed/**', 'sealed/*', '**/*.json', 'sealed/*.*', '**/sealed/**.json', '**/sealed/*/MANIFEST.json', '**/sealed/*/transcript-*.jsonl', '.hidden'] } })).toEqual([0, 1, 2, 3, 4, 5].map(i => `PREGATE_CONFIG_INVALID:sealed.allow_paths[${i}]`));
|
|
expect(codes({ sealed: { case_insensitive: 'yes' } })).toEqual(['PREGATE_CONFIG_INVALID:sealed.case_insensitive']);
|
|
});
|
|
});
|
|
|
|
describe('gstack-pregate flags (CD1)', () => {
|
|
test('an unknown flag is usage (exit 2) on every verb instead of a silent normal run', () => {
|
|
const repo = makeFixtureRepo(); repos.push(repo);
|
|
const bogus = runBin(repo, 'gstack-pregate', ['run', '--base', 'main', '--bogus']);
|
|
expect(bogus.status).toBe(EXIT.usage);
|
|
expect(bogus.err).toContain('unknown flag --bogus');
|
|
const sealedFlag = pregate(repo, ['--sealed']);
|
|
expect(sealedFlag.status).toBe(EXIT.usage);
|
|
expect(sealedFlag.err).toContain('gstack-pregate sealed');
|
|
expect(runBin(repo, 'gstack-pregate', ['verify', '--nope']).status).toBe(EXIT.usage);
|
|
expect(fs.existsSync(path.join(repo.work, '.gstack/tmp/pregate.json'))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('lanes runtime and serial files (CD1)', () => {
|
|
const recordingRunner = `bash -c 'echo "RUN: $*" >> ../runs.log; echo "bun=$(bun --version)"; node --test "$@"' _ {files}`;
|
|
|
|
test('on a Capy-shaped host the lanes runner gets the verified Bun from $HOME/.bun/bin, not the older PATH Bun', () => {
|
|
const host = capyHost({ verifiedBun: '1.4.2' }); hosts.push(host);
|
|
const repo = makeFixtureRepo({ pregate: { ...BASE_PREGATE, runner: recordingRunner } }); repos.push(repo);
|
|
commit(repo, { 'lib/util.js': "exports.add = (a, b) => a + b + 0;\n" });
|
|
const r = pregate(repo, ['--allow-repo-commands', '--stage', 'tests'], host.env);
|
|
expect(r.status).toBe(EXIT.ok);
|
|
expect(r.out).toContain('lanes: bun=1.4.2');
|
|
expect(r.out).not.toContain('bun=1.3.14');
|
|
expect(r.out).toContain(`lanes: runtime: bun 1.4.2 at ${fs.realpathSync(host.bunInstallBin)}/bun (resolved: `);
|
|
});
|
|
|
|
test('serial-pattern test files run in their own process, apart from the unit files', () => {
|
|
const repo = makeFixtureRepo({ pregate: { ...BASE_PREGATE, runner: recordingRunner } }); repos.push(repo);
|
|
commit(repo, { 'lib/util.js': "exports.add = (a, b) => a + b + 0;\n", 'test/widget-env.serial.test.js': "const test = require('node:test');\ntest('serial', () => {});\n" });
|
|
const r = pregate(repo, ['--allow-repo-commands', '--stage', 'tests']);
|
|
expect(r.status).toBe(EXIT.ok);
|
|
const runs = fs.readFileSync(path.join(repo.root, 'runs.log'), 'utf8').trim().split('\n');
|
|
expect(runs).toEqual(['RUN: test/util.test.js', 'RUN: test/widget-env.serial.test.js']);
|
|
expect(r.out).toMatch(/^tests {6}lanes {5}pass .*2 tests ran in 2 file\(s\) \(1 serial\), exit 0/m);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GST-1138 (ship-90-90 L6): the pre-push housekeeping run and gstack's own guard list
|
|
// ---------------------------------------------------------------------------
|
|
import { matchesAny } from '../lib/ship-policy';
|
|
|
|
describe('--stage guards: the pre-push housekeeping run (GST-1138)', () => {
|
|
// Value: protects=--stage guards runs only regen and guards for the touched files, writes its own record and never replaces the full run's
|
|
// pregate.json; fails_when=it runs the secrets, strays or test checks, overwrites .gstack/tmp/pregate.json, or accepts --only/--from-run;
|
|
// why_new=gbrain: 64 of 93 red heads were housekeeping (FOUN-64), #3908 went red on the context-budget ratchet; seam=gstack-pregate run; no_claim=CI
|
|
test('runs only regen and guards, writes pregate-guards.json, leaves pregate.json alone and refuses --only and --from-run', () => {
|
|
const repo = guardedFixture({ ...BASE_PREGATE, guards: [SERIAL_GUARD] });
|
|
commit(repo, { 'test/widget-env.serial.test.js': SERIAL_TEST });
|
|
const r = pregate(repo, ['--allow-repo-commands', '--stage', 'guards']);
|
|
expect(r.status).toBe(EXIT.fail);
|
|
expect(r.out).toMatch(/^preflight {2}guards {4}fail .*serial-manifest.*\(PREGATE_GUARD_FAILED\)/m);
|
|
expect(fs.existsSync(path.join(repo.work, '.gstack/tmp/pregate.json'))).toBe(false);
|
|
const rec = JSON.parse(fs.readFileSync(path.join(repo.work, '.gstack/tmp/pregate-guards.json'), 'utf8'));
|
|
expect(rec.checks.map((c: any) => c.id)).toEqual(['regen', 'guards']);
|
|
expect(rec.stage2_ms).toBeUndefined();
|
|
commit(repo, { 'scripts/serial-files.tsv': 'path\treason\ntest/widget-env.serial.test.js\tglobal-state\n' });
|
|
const ok = pregate(repo, ['--allow-repo-commands', '--stage', 'guards']);
|
|
expect(ok.status, ok.out + ok.err).toBe(EXIT.ok);
|
|
expect(ok.out).toContain('1 guard(s) passed: serial-manifest');
|
|
expect(pregate(repo, ['--stage', 'guards', '--only', 'secrets']).status).toBe(EXIT.usage);
|
|
expect(pregate(repo, ['--stage', 'guards', '--from-run', '123']).status).toBe(EXIT.usage);
|
|
});
|
|
});
|
|
|
|
describe("gstack's own pre-push guards (GST-1138)", () => {
|
|
// Value: protects=gstack declares the housekeeping guards that went red after push (typecheck, skill size, parity, context budget,
|
|
// module size, goldens, doc caps), each runnable and selected only by its inputs; fails_when=a guard is dropped, names a missing test file,
|
|
// fails validation, or fires on a docs-only change; why_new=GST-1138; seam=.gstack/pregate.json; no_claim=the guards' own verdicts
|
|
const GSTACK = path.resolve(import.meta.dir, '..');
|
|
const policy = JSON.parse(fs.readFileSync(path.join(GSTACK, '.gstack/pregate.json'), 'utf8'));
|
|
const guards: { id: string; when: string[]; command: string; fix?: string }[] = policy.guards ?? [];
|
|
const selected = (file: string) => guards.filter(g => matchesAny(g.when, file)).map(g => g.id).sort();
|
|
|
|
test('the list validates, every guard has a fix, and every test file it names exists', () => {
|
|
expect(codes(policy)).toEqual([]);
|
|
expect(guards.map(g => g.id).sort()).toEqual(['conflict-markers', 'context-budget', 'doc-caps', 'goldens', 'module-size', 'parity-suite', 'skill-size', 'typecheck']);
|
|
for (const g of guards) {
|
|
expect(g.fix, g.id).toBeTruthy();
|
|
for (const f of g.command.match(/test\/[\w./-]+\.test\.ts/g) ?? []) expect(fs.existsSync(path.join(GSTACK, f)), `${g.id}: ${f}`).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('each guard fires on its own inputs and none fires on a docs-only change', () => {
|
|
expect(selected('ship/SKILL.md.tmpl')).toEqual(['conflict-markers', 'context-budget', 'goldens', 'parity-suite', 'skill-size']);
|
|
expect(selected('lib/pregate/index.ts')).toEqual(['conflict-markers', 'module-size', 'typecheck']);
|
|
expect(selected('test/fixtures/golden/claude-ship-SKILL.md')).toEqual(['goldens']);
|
|
expect(selected('CLAUDE.md')).toEqual(['doc-caps']);
|
|
expect(selected('docs/pregate.md')).toEqual([]);
|
|
expect(selected('changelog.d/x.md')).toEqual([]);
|
|
});
|
|
});
|