#!/bin/bash # All repositories, queue writes, remotes and mutants are disposable scratch. set -euo pipefail ROOT=$(cd -- "$(dirname -- "$0")/.." && pwd) exec node --input-type=module - "$ROOT" "$@" <<'JS' import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { readFileSync, writeFileSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { createHash } from 'node:crypto'; const root = process.argv[2]; const { scratchRepo, genesisCommitted, cli } = await import(pathToFileURL(join(root, 'packages/queue/tests/helpers.mjs'))); const target = process.argv[3] ?? join(root, 'scripts/push-check.sh'); const sha256 = bytes => createHash('sha256').update(bytes).digest('hex'); const binary = Buffer.from([0, 1, 2, 3]); const digest = sha256(binary); const cleanups = []; let count = 0; function fixture() { const repo = scratchRepo({ after: fn => cleanups.push(fn) }); genesisCommitted(repo); const run = (args, by = 'darkwing', allow = false) => { const r = cli(repo, args, { by }); assert.ok(r.code === 0 || allow, r.err); return r; }; run(['move', '6', 'blocked', '--reason', 'paused', '--op', 'block-six-0001']); run(['move', '9', 'in-progress', '--op', 'start-nine-0001']); run(['set', '9', 'reviewers', 'dewey,filbert', '--op', 'set-reviewers-0001'], 'sage'); // A real local bare remote and clone, with no network or origin access. const bare = join(repo.base, 'remote.git'); repo.g('init', '--bare', '-q', bare); repo.g('clone', '-q', bare, join(repo.base, 'clone')); repo.g('remote', 'add', 'origin', bare); const base = repo.g('rev-parse', 'HEAD').trim(); repo.g('update-ref', 'refs/remotes/origin/refactor', base); const commit = (path, bytes, message = 'content') => { if (bytes === null) repo.g('rm', '--', path); else { writeFileSync(join(repo.root, path), bytes); repo.g('add', '--', path); } repo.g('commit', '-q', '-m', message); return repo.g('rev-parse', 'HEAD').trim(); }; const check = (sha, extra = [], expectedCode = 0, reason = null, script = target, cwd = repo.root) => { const log = join(repo.base, 'expected.log'); writeFileSync(log, repo.g('log', '--oneline', `${base}..${sha}`)); const r = spawnSync('bash', [script, sha, log, ...extra], { cwd, env: repo.env, encoding: 'utf8' }); assert.equal(r.status, expectedCode, `${r.stdout}\n${r.stderr}`); const lines = r.stdout.trim().split('\n'); if (expectedCode === 0) assert.match(lines.at(-1), /^PUSH_READY /); else { assert.equal(lines.filter(s => s.startsWith('STOP ')).length, 1, r.stdout); assert.match(lines.at(-1), reason ?? /^STOP /); assert.ok(!r.stdout.includes('PUSH_READY'), r.stdout); } count++; return r.stdout; }; let n = 0; const round = (candidate, verdict = 'approve') => { n++; const r = run(['move', '9', 'in-review', '--candidate', candidate, '--op', `review-nine-000${n}`], 'darkwing', true); // Missing dummy transport credentials fails the request, after opening a // valid round. Recording a receipt is cooperative and needs no credential. assert.equal(r.code, 1, r.out + r.err); const row = JSON.parse(run(['show', '9']).out); const c = row.review.rounds.at(-1).candidate.digest; run(['review', 'record', '9', '--verdict', verdict, '--candidate', c, '--comment', String(n), '--op', `verdict-nine-00${n}`], 'filbert'); }; const manifest = (d = digest, path = 'asset.bin') => { const file = join(repo.base, 'manifest.sha256'); writeFileSync(file, `${d} ${path}\n`); return file; }; return { repo, run, base, commit, check, round, manifest }; } try { { const f = fixture(); const sha = f.commit('text.txt', 'hello\n'); assert.match(f.check(sha), /NO_BINARY\nSECRETS_CLEAN\nNO_ATTRIBUTION\nPUSH_READY /); f.check(sha, ['--base-ref', f.base]); f.check(sha, ['--bad-option'], 2, /^STOP usage/); // Explicit fetch goes only to the scratch remote. f.check(sha, ['--fetch']); const wrong = join(f.repo.base, 'wrong.log'); writeFileSync(wrong, 'wrong\n'); const r = spawnSync('bash', [target, sha, wrong], { cwd: f.repo.root, env: f.repo.env, encoding: 'utf8' }); assert.equal(r.status, 1); assert.equal(r.stdout, 'STOP log\n'); count++; f.repo.g('update-ref', 'refs/remotes/origin/refactor', sha); const log = join(f.repo.base, 'empty.log'); writeFileSync(log, ''); const ff = spawnSync('bash', [target, f.base, log], { cwd: f.repo.root, env: f.repo.env, encoding: 'utf8' }); assert.equal(ff.status, 1); assert.equal(ff.stdout, 'LOG_OK\nSTOP ff\n'); count++; } // B1: attributes may add binary classifications, never hide NUL blobs. for (const source of ['tree', 'info', 'config']) { const f = fixture(); if (source === 'tree') f.commit('.gitattributes', '*.bin diff\n'); if (source === 'info') writeFileSync(join(f.repo.gitDir, 'info/attributes'), '*.bin diff\n'); if (source === 'config') { const attributes = join(f.repo.base, 'attributes'); writeFileSync(attributes, '*.bin diff\n'); f.repo.g('config', 'core.attributesFile', attributes); } const sha = f.commit('asset.bin', binary); f.check(sha, [], 1, /^STOP binary asset.bin /); f.round(f.manifest()); assert.match(f.check(sha, ['--row', '9']), /BINARY_REVIEWED 1\n/); } // The content rule includes byte 7999, and excludes byte 8000. for (const offset of [7999, 8000]) { const f = fixture(), bytes = Buffer.alloc(8001, 65); bytes[offset] = 0; f.commit('.gitattributes', '*.bin diff\n'); const sha = f.commit('asset.bin', bytes); if (offset === 7999) f.check(sha, [], 1, /^STOP binary asset.bin /); else assert.match(f.check(sha), /NO_BINARY\n/); } // N4: an approval beyond the durable witness is visible, not authority. { const f = fixture(), sha = f.commit('asset.bin', binary); const witness = join(f.repo.gitDir, 'mosaic-queue.head'); const durable = readFileSync(witness); f.round(f.manifest()); writeFileSync(witness, durable); const visible = f.run(['show', '9']); assert.match(visible.err, /not confirmed durable/); f.check(sha, ['--row', '9'], 2, /^STOP queue unconfirmed tail$/); } { const f = fixture(), sha = f.commit('asset.bin', binary); f.check(sha, [], 1, /^STOP binary asset.bin /); f.round(f.manifest('0'.repeat(64))); f.check(sha, ['--row', '9'], 1, /^STOP binary asset.bin /); } { const f = fixture(), sha = f.commit('asset.bin', binary); f.round(f.manifest()); writeFileSync(join(f.repo.root, 'asset.bin'), 'working tree differs'); assert.match(f.check(sha, ['--row', '9']), /BINARY_REVIEWED 1\n/); f.check(sha, [], 1, /^STOP binary asset.bin /); const other = f.commit('asset.bin', Buffer.from([0, 8, 9])); // The working tree is the approved version; the pushed blob isn't. writeFileSync(join(f.repo.root, 'asset.bin'), binary); f.check(other, ['--row', '9'], 1, /^STOP binary asset.bin /); } { const f = fixture(), sha = f.commit('asset.bin', binary); f.round(sha); assert.match(f.check(sha, ['--row', '9']), /BINARY_REVIEWED 1\n/); const other = f.commit('asset.bin', Buffer.from([0, 7])); f.check(other, ['--row', '9'], 1, /^STOP binary asset.bin /); } { const f = fixture(), sha = f.commit('space tab\tasset.bin', binary); f.round(sha); assert.match(f.check(sha, ['--row', '9']), /BINARY_REVIEWED 1\n/); } { const f = fixture(), sha = f.commit('line\nbreak.bin', binary); f.check(sha, [], 1, /^STOP binary "line\\nbreak.bin" /); writeFileSync(Buffer.concat([Buffer.from(f.repo.root + '/'), Buffer.from([255]), Buffer.from('.bin')]), binary); f.repo.g('add', '-A'); f.repo.g('commit', '-q', '-m', 'invalid UTF-8 path'); f.check(f.repo.g('rev-parse', 'HEAD').trim(), [], 2, /^STOP git or environment error$/); } { const f = fixture(), sha = f.commit('asset.bin', binary); const cwd = join(f.repo.root, 'nested'); mkdirSync(cwd); f.repo.g('config', 'diff.relative', 'true'); f.check(sha, [], 1, /^STOP binary asset.bin /, target, cwd); f.round(f.manifest()); assert.match(f.check(sha, ['--row', '9'], 0, null, target, cwd), /BINARY_REVIEWED 1\n/); const relativeLog = spawnSync('bash', [target, sha, '../../expected.log', '--row', '9'], { cwd, env: f.repo.env, encoding: 'utf8' }); assert.equal(relativeLog.status, 0, relativeLog.stdout + relativeLog.stderr); assert.match(relativeLog.stdout.trim().split('\n').at(-1), /^PUSH_READY /); count++; } { const f = fixture(), cwd = join(f.repo.root, 'nested'); mkdirSync(cwd); const sha = f.commit('nested/asset.bin', binary); f.repo.g('config', 'diff.relative', 'true'); f.check(sha, [], 1, /^STOP binary nested\/asset.bin /, target, cwd); f.round(sha); assert.match(f.check(sha, ['--row', '9'], 0, null, target, cwd), /BINARY_REVIEWED 1\n/); } { const f = fixture(); f.commit('asset.bin', binary); const unrelated = f.commit('text.txt', 'unrelated\n'); f.round(unrelated); f.check(unrelated, ['--row', '9'], 1, /^STOP binary asset.bin /); } { const f = fixture(), sha = f.commit('asset.bin', binary); f.round(f.manifest(), 'changes'); const row = JSON.parse(f.run(['show', '9']).out); f.run(['review', 'record', '9', '--verdict', 'approve', '--candidate', row.review.rounds.at(-1).candidate.digest, '--comment', '2', '--op', 'approve-dewey-0001'], 'dewey'); f.check(sha, ['--row', '9'], 1, /^STOP binary asset.bin /); } { const f = fixture(), sha = f.commit('asset.bin', binary); f.round(f.manifest()); f.run(['move', '9', 'in-progress', '--op', 'revise-nine-0001']); f.round(f.manifest('0'.repeat(64))); f.check(sha, ['--row', '9'], 1, /^STOP binary asset.bin /); } { const f = fixture(), sha = f.commit('asset.bin', binary); f.round(f.manifest()); // This shape cannot be recorded by the queue. Its read API must reject // tampered receipts before the guard can treat them as authority. const path = f.repo.queuePath; const doc = JSON.parse(readFileSync(path)); doc.rows.find(r => r.id === 9).review.rounds.at(-1).receipts[0].candidate = '0'.repeat(64); writeFileSync(path, JSON.stringify(doc)); f.check(sha, ['--row', '9'], 2, /^STOP queue unavailable or invalid$/); } { const f = fixture(), added = f.commit('asset.bin', binary); f.repo.g('update-ref', 'refs/remotes/origin/refactor', added); const deleted = f.commit('asset.bin', null); // Explicit base retains the check helper's original expected log range. f.repo.g('update-ref', 'refs/remotes/origin/refactor', f.base); assert.match(f.check(deleted, ['--base-ref', added], 1, /^STOP log$/), /STOP log/); const log = join(f.repo.base, 'delete.log'); writeFileSync(log, f.repo.g('log', '--oneline', `${added}..${deleted}`)); const r = spawnSync('bash', [target, deleted, log, '--base-ref', added], { cwd: f.repo.root, env: f.repo.env, encoding: 'utf8' }); assert.equal(r.status, 0, r.stdout); assert.match(r.stdout, /NO_BINARY\n/); assert.match(r.stdout.trim().split('\n').at(-1), /^PUSH_READY /); count++; } for (const [message, content, reason, prefix] of [ ['content', 'password="' + 'a'.repeat(30) + '"\n', 'secrets', 'LOG_OK\nFF_OK\nNO_BINARY\n'], ['Generated with example', 'hello\n', 'attribution', 'LOG_OK\nFF_OK\nNO_BINARY\nSECRETS_CLEAN\n'], ]) { const f = fixture(), sha = f.commit('text.txt', content, message); assert.equal(f.check(sha, [], 1), `${prefix}STOP ${reason}\n`); } console.log(`PASS push-check ${count} checks`); if (!process.argv[3]) { const dir = mkdtempSync(join(tmpdir(), 'push-check-mutants-')); cleanups.push(() => rmSync(dir, { recursive: true, force: true })); const source = readFileSync(target, 'utf8'); const mutants = [ ['skip-hash', "c.text.split('\\n').includes(`${digest} ${path}`)", "c.text.split('\\n').some(line => line.endsWith(` ${path}`))"], ['accept-changes', "round.receipts?.some(r => r.verdict === 'changes')", 'false'], ['earlier-round', 'row.review?.rounds.at(-1)', 'row.review?.rounds.at(0)'], ['working-tree', "hash(git('cat-file', 'blob', h.oid))", 'hash(readFileSync(path))'], ['no-row', 'if (!matches) stop(', 'if (!matches && rowIds.size) stop('], ['skip-nul-scan', " || git('cat-file', 'blob', entry.oid).subarray(0, 8000).includes(0)", ''], ]; for (const [name, before, after] of mutants) { assert.ok(source.includes(before), name); const file = join(dir, `${name}.sh`); writeFileSync(file, source.replace(before, after)); const r = spawnSync('timeout', ['-k', '10', '120', 'bash', join(root, 'scripts/test-push-check.sh'), file], { encoding: 'utf8', maxBuffer: 4 << 20 }); assert.equal(r.status, 1, `${name} survived or infrastructure failed: ${r.status}\n${r.stdout}\n${r.stderr}`); assert.match(r.stderr, /AssertionError/, r.stderr); console.log(`KILLED ${name}`); } } } finally { for (const fn of cleanups.reverse()) fn(); } JS