mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 15:27:56 +01:00
* feat(sealed): skip commits the destination already holds (refs/pull/<n>/head) or whose patch landed (GSTA-156 P1) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(sealed): compare landed patches byte for byte (latin1), so bytes that are not UTF-8 cannot collapse into a false match Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(sealed): a landed-patch skip still runs sealed.label_join, which judges the whole file the base may never have held 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>
197 lines
10 KiB
TypeScript
197 lines
10 KiB
TypeScript
/**
|
|
* GSTA-156 P1: the sealed guard rescans only what the destination does not
|
|
* already hold. gstack-evals#22 was retargeted onto main after its parent #6
|
|
* squash-landed; its range still listed #6's seven commits (one added and
|
|
* later deleted a held-out file), so the guard refused a push whose every
|
|
* byte the destination already had as refs/pull/6/head. A commit is skipped
|
|
* only when it is an ancestor of the destination's own refs/pull/<n>/head for
|
|
* a squash (#n) on the base since the merge-base, or when its own
|
|
* --verbatim patch-id and diff equal a base commit's. No combined-diff
|
|
* matching; any lookup failure scans everything.
|
|
*/
|
|
import { afterEach, 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';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const PREPUSH = path.join(ROOT, 'bin', 'gstack-redact-prepush');
|
|
const PREGATE = path.join(ROOT, 'bin', 'gstack-pregate');
|
|
const ZERO = '0'.repeat(40);
|
|
const POLICY = JSON.stringify({ sealed: { paths: ['heldout/**'], answer_markers: ['SEALED ANSWER:'], local_paths: true } }, null, 2) + '\n';
|
|
|
|
interface Scratch { root: string; work: string; origin: string; state: string; env: Record<string, string> }
|
|
const scratches: Scratch[] = [];
|
|
afterEach(() => { while (scratches.length) fs.rmSync(scratches.pop()!.root, { recursive: true, force: true }); });
|
|
|
|
function sh(s: Scratch, args: string[]): string {
|
|
const r = spawnSync('git', args, { cwd: s.work, encoding: 'utf8', timeout: 30_000, env: { ...process.env, ...s.env } });
|
|
if (r.status !== 0) throw new Error(`git ${args.join(' ')}: ${r.stderr}`);
|
|
return (r.stdout ?? '').trim();
|
|
}
|
|
function scratch(): Scratch {
|
|
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'sealed-skip-')));
|
|
const work = path.join(root, 'work'); const origin = path.join(root, 'origin.git'); const state = path.join(root, 'state');
|
|
fs.mkdirSync(work); fs.mkdirSync(state);
|
|
const env = { HOME: root, GSTACK_HOME: state, GSTACK_STATE_ROOT: state, GIT_CONFIG_GLOBAL: path.join(root, 'gitconfig'), GIT_CONFIG_NOSYSTEM: '1' };
|
|
fs.writeFileSync(env.GIT_CONFIG_GLOBAL, '[user]\n\tname = t\n\temail = t@example.com\n[commit]\n\tgpgsign = false\n[init]\n\tdefaultBranch = main\n');
|
|
const s = { root, work, origin, state, env };
|
|
scratches.push(s);
|
|
spawnSync('git', ['init', '-q', '--bare', origin], { env: { ...process.env, ...env }, timeout: 30_000 });
|
|
sh(s, ['init', '-q', '-b', 'main']);
|
|
commit(s, { 'README.md': '# evals\n', '.gstack/pregate.json': POLICY }, 'base');
|
|
sh(s, ['remote', 'add', 'origin', origin]);
|
|
sh(s, ['push', '-q', 'origin', 'main']);
|
|
sh(s, ['remote', 'set-head', 'origin', 'main']);
|
|
return s;
|
|
}
|
|
function commit(s: Scratch, files: Record<string, string | null>, msg: string): string {
|
|
for (const [rel, body] of Object.entries(files)) {
|
|
const abs = path.join(s.work, rel);
|
|
if (body === null) { fs.rmSync(abs, { force: true }); continue; }
|
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
fs.writeFileSync(abs, body);
|
|
}
|
|
sh(s, ['add', '-A']); sh(s, ['commit', '-q', '--allow-empty', '-m', msg]);
|
|
return sh(s, ['rev-parse', 'HEAD']);
|
|
}
|
|
|
|
/** The #22 shape: parent #6 has seven commits (the second adds heldout/answers.json, the fifth deletes it); main lands it as one squash "(#6)"; results stacks one commit on the parent. */
|
|
function retargeted(s: Scratch, opts: { publishPull: boolean }): void {
|
|
sh(s, ['checkout', '-q', '-b', 'parent']);
|
|
commit(s, { 'cells/a.md': 'a\n' }, 'cell a');
|
|
commit(s, { 'heldout/answers.json': '{"q1":"b"}\n' }, 'stage answers');
|
|
commit(s, { 'cells/b.md': 'b\n' }, 'cell b');
|
|
commit(s, { 'cells/c.md': 'c\n' }, 'cell c');
|
|
commit(s, { 'heldout/answers.json': null }, 'drop answers');
|
|
commit(s, { 'cells/d.md': 'd\n' }, 'cell d');
|
|
commit(s, { 'cells/e.md': 'e\n' }, 'cell e');
|
|
if (opts.publishPull) sh(s, ['push', '-q', 'origin', 'parent:refs/pull/6/head']);
|
|
sh(s, ['checkout', '-q', 'main']);
|
|
sh(s, ['merge', '-q', '--squash', 'parent']);
|
|
sh(s, ['commit', '-q', '-m', 'feat: cells a-e (#6)']);
|
|
sh(s, ['push', '-q', 'origin', 'main']);
|
|
sh(s, ['checkout', '-q', '-b', 'results', 'parent']);
|
|
commit(s, { 'cells/f.md': 'f\n' }, 'cell f');
|
|
}
|
|
function hook(s: Scratch, url = s.origin) {
|
|
const head = sh(s, ['rev-parse', 'HEAD']);
|
|
const r = spawnSync('bun', [PREPUSH, 'origin', url], { cwd: s.work, input: `refs/heads/results ${head} refs/heads/results ${ZERO}\n`, encoding: 'utf8', timeout: 60_000, env: { ...process.env, ...s.env } });
|
|
return { code: r.status, err: r.stderr ?? '' };
|
|
}
|
|
function preflight(s: Scratch, args: string[] = []) {
|
|
const r = spawnSync('bun', [PREGATE, 'sealed', '--base', 'main', ...args], { cwd: s.work, encoding: 'utf8', timeout: 60_000, env: { ...process.env, ...s.env } });
|
|
return { code: r.status, out: r.stdout ?? '', err: r.stderr ?? '' };
|
|
}
|
|
|
|
describe('sealed skips what the destination already holds', () => {
|
|
test('the #22 squash-retarget: seven commits published as refs/pull/6/head are skipped, the push is clear, the hook and the preflight agree, and --no-skip still refuses', () => {
|
|
const s = scratch();
|
|
retargeted(s, { publishPull: true });
|
|
const h = hook(s);
|
|
expect(h.err).toContain('skipped 7 commit(s) the destination already has as refs/pull/6/head');
|
|
expect(h.code).toBe(0);
|
|
const p = preflight(s);
|
|
expect(p.code).toBe(0);
|
|
expect(p.out).toMatch(/^SEALED: clear findings=0 commits=1 skipped=7 /m);
|
|
const full = preflight(s, ['--no-skip']);
|
|
expect(full.code).toBe(1);
|
|
expect(full.out).toMatch(/^SEALED: refused findings=1 commits=8 skipped=0 /m);
|
|
expect(full.err).toMatch(/sealed\.path\s+heldout\/answers\.json/);
|
|
expect(sh(s, ['for-each-ref', 'refs/gstack/'])).toBe('');
|
|
const ledger = fs.readFileSync(path.join(s.state, 'security', 'egress.jsonl'), 'utf8');
|
|
expect(ledger).toContain('"sink":"sealed-guard"');
|
|
});
|
|
|
|
test('an add-then-delete parent whose combined diff equals the squash but was never published still refuses', () => {
|
|
const s = scratch();
|
|
retargeted(s, { publishPull: false });
|
|
const h = hook(s);
|
|
expect(h.code).toBe(1);
|
|
expect(h.err).toMatch(/sealed\.path\s+heldout\/answers\.json/);
|
|
const p = preflight(s);
|
|
expect(p.code).toBe(1);
|
|
expect(p.out).toMatch(/^SEALED: refused findings=1 commits=8 skipped=0 /m);
|
|
});
|
|
|
|
test('a ref lookup failure scans everything and says so', () => {
|
|
const s = scratch();
|
|
retargeted(s, { publishPull: true });
|
|
sh(s, ['remote', 'set-url', 'origin', path.join(s.root, 'gone.git')]);
|
|
const h = hook(s, path.join(s.root, 'gone.git'));
|
|
expect(h.code).toBe(1);
|
|
expect(h.err).toContain('could not list refs/pull/<n>/head on origin');
|
|
expect(h.err).toMatch(/sealed\.path\s+heldout\/answers\.json/);
|
|
});
|
|
|
|
test('a cherry-picked commit whose patch landed on the base is skipped, naming the base commit', () => {
|
|
const s = scratch();
|
|
sh(s, ['checkout', '-q', '-b', 'results']);
|
|
const mine = commit(s, { 'notes.md': 'intro\nraw at /home/alice/evals/raw.jsonl\n' }, 'notes');
|
|
sh(s, ['checkout', '-q', 'main']);
|
|
commit(s, { 'other.md': 'x\n' }, 'unrelated');
|
|
sh(s, ['cherry-pick', mine]);
|
|
sh(s, ['push', '-q', 'origin', 'main']);
|
|
const landed = sh(s, ['rev-parse', 'HEAD']);
|
|
expect(landed).not.toBe(mine);
|
|
sh(s, ['checkout', '-q', 'results']);
|
|
const h = hook(s);
|
|
expect(h.code).toBe(0);
|
|
expect(h.err).toContain(`skipped commit ${mine.slice(0, 12)} (its patch landed on the base as ${landed.slice(0, 12)})`);
|
|
expect(preflight(s).out).toMatch(/^SEALED: clear findings=0 commits=0 skipped=1 /m);
|
|
});
|
|
|
|
test('a whitespace near-miss of a landed commit is scanned and refuses', () => {
|
|
const s = scratch();
|
|
sh(s, ['checkout', '-q', '-b', 'results']);
|
|
commit(s, { 'notes.md': 'intro\nraw at /home/alice/evals/raw.jsonl\n' }, 'notes');
|
|
sh(s, ['checkout', '-q', 'main']);
|
|
commit(s, { 'notes.md': 'intro\nraw at /home/ alice/evals/raw.jsonl\n' }, 'notes');
|
|
sh(s, ['push', '-q', 'origin', 'main']);
|
|
sh(s, ['checkout', '-q', 'results']);
|
|
const h = hook(s);
|
|
expect(h.code).toBe(1);
|
|
expect(h.err).toMatch(/sealed\.local_path\s+notes\.md:2/);
|
|
expect(preflight(s).out).toMatch(/^SEALED: refused findings=1 commits=1 skipped=0 /m);
|
|
});
|
|
|
|
test('a landed commit still gets the label_join check, because the join can form only on the branch', () => {
|
|
const s = scratch();
|
|
const rec = (extra: Record<number, string>) => ['{', ' "id": "r1",', ' "k1": 1,', ' "k2": 2,', ' "k3": 3,', ' "k4": 4,', ' "k5": 5,', ' "k6": 6,', ' "k7": 7', '}'].flatMap((l, i) => extra[i] ? [l, extra[i]!] : [l]).join('\n') + '\n';
|
|
const labeled = JSON.stringify({ sealed: { paths: ['heldout/**'], label_keys: ['stratum'] } }, null, 2) + '\n';
|
|
commit(s, { '.gstack/pregate.json': labeled, 'rec.json': rec({}) }, 'labeled policy');
|
|
sh(s, ['push', '-q', 'origin', 'main']);
|
|
sh(s, ['checkout', '-q', '-b', 'results']);
|
|
commit(s, { 'rec.json': rec({ 1: ' "stratum": "decoy",' }) }, 'stratum');
|
|
const head = commit(s, { 'rec.json': rec({ 1: ' "stratum": "decoy",', 7: ' "head": "abc1234def5678",' }) }, 'head');
|
|
sh(s, ['checkout', '-q', 'main']);
|
|
sh(s, ['cherry-pick', head]);
|
|
sh(s, ['push', '-q', 'origin', 'main']);
|
|
expect(sh(s, ['show', 'HEAD:rec.json'])).not.toContain('stratum');
|
|
sh(s, ['checkout', '-q', 'results']);
|
|
const h = hook(s);
|
|
expect(h.code).toBe(1);
|
|
expect(h.err).toMatch(/sealed\.label_join\s+rec\.json/);
|
|
const p = preflight(s);
|
|
expect(p.code).toBe(1);
|
|
expect(p.out).toMatch(/^SEALED: refused findings=1 commits=1 skipped=1 /m);
|
|
});
|
|
|
|
test('a landed commit that differs only in bytes that are not UTF-8 is scanned and refuses', () => {
|
|
const s = scratch();
|
|
sh(s, ['checkout', '-q', '-b', 'results']);
|
|
fs.writeFileSync(path.join(s.work, 'notes.md'), Buffer.from('intro\nraw at /home/alice/evals/raw\xe8.jsonl\n', 'latin1'));
|
|
sh(s, ['add', 'notes.md']); sh(s, ['commit', '-q', '-m', 'notes']);
|
|
sh(s, ['checkout', '-q', 'main']);
|
|
fs.writeFileSync(path.join(s.work, 'notes.md'), Buffer.from('intro\nraw at /home/alice/evals/raw\xe9.jsonl\n', 'latin1'));
|
|
sh(s, ['add', 'notes.md']); sh(s, ['commit', '-q', '-m', 'notes']);
|
|
sh(s, ['push', '-q', 'origin', 'main']);
|
|
sh(s, ['checkout', '-q', 'results']);
|
|
const h = hook(s);
|
|
expect(h.code).toBe(1);
|
|
expect(h.err).toMatch(/sealed\.local_path\s+notes\.md:2/);
|
|
expect(preflight(s).out).toMatch(/^SEALED: refused findings=1 commits=1 skipped=0 /m);
|
|
});
|
|
});
|