mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
* pregate: starter policy, benign-finding allowlist, environment failures, per-run scratch
A repository with no .gstack/pregate.json now gets a warn (PREGATE_NO_POLICY)
with a starter policy printed, instead of PREGATE_NO_LANE on every touched
file; gstack-pregate init remains the explicit write. The policy gains an
allowlist for benign secrets/pii findings (env.value and secret never
allowlistable). Checks that cannot run because of the machine report
PREGATE_ENVIRONMENT (incomplete, with kind and evidence) apart from diff
failures. Every run writes its scratch under the state root, per run id.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* pregate: rename run-scratch module (strays rule) and map starter.ts to its test
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* pregate literals: match a module as a path fragment, require the constant name for a one-character value
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes
guards: an exited guard is the machine's only on exit 127, never from its output;
secrets: clear an interrupted run's added-line copies before scanning;
environment: one fix table per kind, one ANSI strip, hoisted not-found vocabulary;
starter returns its lines; allowlistDigest reuses sha256; stale headers and the
PREGATE_INCOMPLETE fix clause updated; tests for the mixed verify branch, disk
precedence, allowlist boundaries, a positionless finding, head-policy strictness,
run-id edges and the unwritable state root.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* pregate: count a module's full file name without a directory as a consumer
consumersOf anchored both the stem and the full basename on a path
separator, so run('install.sh') or bash install.sh without a directory
stopped counting for a multi-character old value. Only the bare stem
needs the separator; the full file name is specific on its own.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* pregate: a diff cannot clear itself as an environment failure; a taken --run id gets its own directory
Adversarial review of the environment classifier and the scratch layout:
- exit 127 and 'command not found' are missing-binary only when the
command's own binary is off PATH (runner, guard and remote gate alike);
a script the diff controls that exits 127 is the diff's failure
- a local lane with no test summary is classified from its output only
when the runner was not seen starting a test file; a remote gate only
from its first 40 lines, before any test output could stream
- environment.proof records launch (certain) or output (inferred); an
output-inferred cause never says 'the diff is not at fault', its line
says '(inferred from output)', and verify names such checks
- a runner killed by a signal is PREGATE_INCOMPLETE, not missing-binary
- a --run id whose directory exists takes <id>-2 .. -9 instead of
sharing (and truncating) a live run's logs
- engine.* findings are never allowlistable; a pattern entry the scanner
gave no position for is reported as not tested instead of silently inert
- docs: synopsis --run, no-policy lanes wording, verify's code list,
guard exit 127 carve-out, PREGATE_SECRETS allowlist pointer, scratch
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(pregate): PREGATE_INCOMPLETE names a signal-killed runner; CHANGELOG lists every doc edit
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>
140 lines
9.4 KiB
TypeScript
140 lines
9.4 KiB
TypeScript
/**
|
|
* Lane D (GSTA-112, E4): a repository with no pregate policy anywhere (no
|
|
* `.gstack/pregate.json` on the base, no user-owned file) gets a `warn` row
|
|
* with a printed starter policy instead of a hard PREGATE_NO_LANE, so the
|
|
* first /ship pass in a new repo is not blocked before anyone has written the
|
|
* lane policy (FOUN #292, harness-evals #81/#82). Nothing is written by `run`;
|
|
* `gstack-pregate init` is the explicit write. A repository that has a policy
|
|
* keeps the hard failure: missing coverage is never inferred silently.
|
|
*/
|
|
import { afterEach, describe, expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { EXIT } from '../lib/headless-artifacts';
|
|
import { validateConfig } from '../lib/pregate/config';
|
|
import { CHECK_IDS } from '../lib/pregate/index';
|
|
import { nearestTestsFor } from '../lib/pregate/starter';
|
|
import { BASE_PREGATE, cleanup, commit, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/pregate-fixture';
|
|
|
|
const repos: FixtureRepo[] = [];
|
|
const fixture = (opts?: Parameters<typeof makeFixtureRepo>[0]) => { const r = makeFixtureRepo(opts); repos.push(r); return r; };
|
|
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
|
|
/** The fixture's Windows workflow replaced by a lint job: with no policy its planner-computed matrix would otherwise make every selected test a requires-remote obligation. */
|
|
const NO_PLATFORM_LANE = { '.github/workflows/windows.yml': 'name: Lint\non: [push]\njobs:\n lint:\n runs-on: ubuntu-24.04\n steps:\n - run: echo lint\n' };
|
|
const lanes = (repo: FixtureRepo, extra: string[] = [], env: Record<string, string> = {}) => runBin(repo, 'gstack-pregate', ['--base', 'main', '--allow-repo-commands', '--only', 'lanes', ...extra], env);
|
|
const readOut = (repo: FixtureRepo) => JSON.parse(fs.readFileSync(path.join(repo.work, '.gstack/tmp/pregate.json'), 'utf8'));
|
|
const starterJson = (out: string): Record<string, unknown> => {
|
|
const block = /PREGATE_STARTER: begin \.gstack\/pregate\.json\n([\s\S]*?)\n\s*lanes: PREGATE_STARTER: end/.exec(out);
|
|
if (!block) throw new Error(`no starter block in:\n${out}`);
|
|
return JSON.parse(block[1]!.split('\n').map(l => l.replace(/^\s*lanes: /, '')).join('\n'));
|
|
};
|
|
/** An unmapped source file, a mapped one (its test runs) and a file whose only nearby test shares its stem without importing it. */
|
|
const NO_POLICY_DIFF = {
|
|
'lib/orphan.js': 'exports.lonely = () => 2;\n',
|
|
'lib/util.js': 'exports.add = (a, b) => b + a;\n',
|
|
'scripts/report.js': 'module.exports = () => "report";\n',
|
|
'test/report.test.js': "const test = require('node:test');\ntest('report placeholder', () => {});\n",
|
|
};
|
|
|
|
describe('no policy anywhere: warn plus a printed starter (never PREGATE_NO_LANE)', () => {
|
|
// Value: protects=the first /ship in a repo with no .gstack/pregate.json is not blocked by PREGATE_NO_LANE: the unmapped files
|
|
// are a warn row (PREGATE_NO_POLICY) with a starter policy the owner can commit, and the selected tests still run;
|
|
// fails_when=the lanes check exits 1 with PREGATE_NO_LANE (the behaviour before this change) or the starter is missing or
|
|
// invalid; why_new=the no-policy branch is new; seam=none; no_claim=the starter's mappings are right for every repo
|
|
test('unmapped files are a warn row, the mapped test still runs, and the starter validates and maps by nearest test or stem', () => {
|
|
const repo = fixture({ pregate: null, baseFiles: NO_PLATFORM_LANE });
|
|
commit(repo, NO_POLICY_DIFF);
|
|
const r = lanes(repo);
|
|
expect(r.err).not.toContain('PREGATE_NO_LANE');
|
|
expect(r.status).toBe(EXIT.ok);
|
|
expect(r.out).toContain('(PREGATE_NO_POLICY)');
|
|
expect(r.err).not.toContain('gstack-pregate: lanes');
|
|
expect(r.out).toContain('no policy: lib/orphan.js scripts/report.js have no test lane');
|
|
const row = readOut(repo).checks.find((c: any) => c.id === 'lanes');
|
|
expect([row.status, row.code]).toEqual(['warn', 'PREGATE_NO_POLICY']);
|
|
expect(row.detail).toMatch(/\d+ tests ran in \d+ file\(s\)/);
|
|
expect(row.fix).toContain('gstack-pregate init');
|
|
const starter = starterJson(r.out);
|
|
expect(validateConfig(starter, CHECK_IDS)).toEqual([]);
|
|
expect(starter.runner).toBe('node --test {files}');
|
|
expect((starter.dependencies as Record<string, string[]>)['scripts/report.js']).toEqual(['test/report.test.js']);
|
|
expect(starter.dependencies).not.toHaveProperty('lib/orphan.js');
|
|
expect(r.out).toContain('TODO lib/orphan.js: no test lane and no nearby test');
|
|
expect(r.out).toContain('PREGATE_STARTER: test directories: test');
|
|
expect(r.out).toContain('PREGATE_STARTER: CI lanes: free-tests.yml/unit (linux)');
|
|
expect(fs.existsSync(path.join(repo.work, '.gstack/pregate.json'))).toBe(false);
|
|
// With the planner-computed Windows lane and no selection manifest, the selected tests are a requires-remote obligation: that status stands, with the no-policy code and the starter beside it.
|
|
const platform = fixture({ pregate: null });
|
|
commit(platform, NO_POLICY_DIFF);
|
|
const p = lanes(platform);
|
|
expect(p.status).toBe(EXIT.ok);
|
|
expect(p.err).not.toContain('PREGATE_NO_LANE');
|
|
const prow = readOut(platform).checks.find((c: any) => c.id === 'lanes');
|
|
expect([prow.status, prow.code]).toEqual(['requires-remote', 'PREGATE_NO_POLICY']);
|
|
expect(p.out).toContain('PREGATE_STARTER: CI lanes: free-tests.yml/unit (linux), windows.yml/windows-shard (windows, matrix computed; declare selection)');
|
|
});
|
|
|
|
// Value: protects=warn never hides a failing selected test: a no-policy run whose lane fails is still fail; fails_when=the
|
|
// starter branch returns warn for a red lane; why_new=new branch; seam=none; no_claim=none
|
|
test('a failing selected test under no policy is still PREGATE_LANE_FAILED', () => {
|
|
const repo = fixture({ pregate: null, baseFiles: NO_PLATFORM_LANE });
|
|
commit(repo, { ...NO_POLICY_DIFF, 'lib/util.js': 'exports.add = (a, b) => a - b;\n' });
|
|
const r = lanes(repo);
|
|
expect(r.status).toBe(EXIT.fail);
|
|
expect(r.err).toContain('(PREGATE_LANE_FAILED)');
|
|
expect(r.out).toContain('no policy: lib/orphan.js scripts/report.js have no test lane');
|
|
});
|
|
|
|
// Value: protects=a repository with a policy keeps the hard PREGATE_NO_LANE (base copy, and the user-owned file); fails_when=the
|
|
// warn branch fires whenever an entry is unmapped; why_new=the branch condition is new; seam=none; no_claim=none
|
|
test('a base policy or a user-owned policy keeps PREGATE_NO_LANE as a hard failure', () => {
|
|
const withBase = fixture();
|
|
commit(withBase, { 'lib/orphan.js': 'exports.lonely = () => 2;\n' });
|
|
const b = lanes(withBase);
|
|
expect(b.status).toBe(EXIT.fail);
|
|
expect(b.err).toContain('(PREGATE_NO_LANE)');
|
|
expect(b.out).not.toContain('PREGATE_STARTER');
|
|
const withUser = fixture({ pregate: null });
|
|
commit(withUser, { 'lib/orphan.js': 'exports.lonely = () => 2;\n' });
|
|
const userPolicy = path.join(withUser.home, 'projects', 'starterslug', 'pregate.json');
|
|
fs.mkdirSync(path.dirname(userPolicy), { recursive: true });
|
|
fs.writeFileSync(userPolicy, JSON.stringify({ runner: 'node --test {files}' }));
|
|
const u = lanes(withUser, [], { GSTACK_PROJECT_SLUG: 'starterslug' });
|
|
expect(u.status).toBe(EXIT.fail);
|
|
expect(u.err).toContain('(PREGATE_NO_LANE)');
|
|
expect(u.out).not.toContain('PREGATE_STARTER');
|
|
// --policy-from head with a head policy (no base copy) is strict too.
|
|
const withHead = fixture({ pregate: null });
|
|
commit(withHead, { 'lib/orphan.js': 'exports.lonely = () => 2;\n', '.gstack/pregate.json': JSON.stringify({ runner: 'node --test {files}' }) + '\n' });
|
|
const h = lanes(withHead, ['--policy-from', 'head']);
|
|
expect(h.status).toBe(EXIT.fail);
|
|
expect(h.err).toContain('(PREGATE_NO_LANE)');
|
|
expect(h.out).not.toContain('PREGATE_STARTER');
|
|
});
|
|
|
|
// Value: protects=`run` never writes the policy; `init` is the explicit write and the starter and the init draft agree on the
|
|
// runner; fails_when=run creates .gstack/pregate.json or init writes something validate rejects; why_new=the run/init split is
|
|
// the contract this lane documents; seam=none; no_claim=none
|
|
test('init is the explicit write: it produces a policy validate accepts, and run left none behind', () => {
|
|
const repo = fixture({ pregate: null });
|
|
commit(repo, NO_POLICY_DIFF);
|
|
lanes(repo);
|
|
expect(fs.existsSync(path.join(repo.work, '.gstack/pregate.json'))).toBe(false);
|
|
const init = runBin(repo, 'gstack-pregate', ['init']);
|
|
expect(init.status).toBe(EXIT.ok);
|
|
expect(init.out).toContain('PREGATE_CONFIG_WRITTEN');
|
|
expect(runBin(repo, 'gstack-pregate', ['validate']).out).toContain('PREGATE_CONFIG_VALID');
|
|
expect(BASE_PREGATE.runner).toBe('node --test {files}');
|
|
});
|
|
});
|
|
|
|
describe('nearestTestsFor: the nearest directory with tests, else the tests sharing the stem', () => {
|
|
const tests = ['lib/render/a.test.ts', 'lib/render/b.test.ts', 'test/report.test.js', 'test/report-golden.test.js', 'test/reporter.test.js'];
|
|
test('a file under a directory with tests takes those tests; one with none takes its stem; a stem with no match is empty', () => {
|
|
expect(nearestTestsFor(tests, 'lib/render/draw.ts')).toEqual(['lib/render/a.test.ts', 'lib/render/b.test.ts']);
|
|
expect(nearestTestsFor(tests, 'src/report.js')).toEqual(['test/report.test.js', 'test/report-golden.test.js']);
|
|
expect(nearestTestsFor(tests, 'src/orphan.js')).toEqual([]);
|
|
expect(nearestTestsFor(tests, 'README')).toEqual([]);
|
|
});
|
|
});
|