Compare commits

...
13 changed files with 698 additions and 0 deletions

No files matched your search

+162
View File
@@ -0,0 +1,162 @@
# Row 327 — reviewed binary push check (#1825)
2026-10-24 Bradford, branch bradford/row327, base b617b80f6.
## Plan and boundaries
Implement the scratch check's ordered tokens in scripts/push-check.sh, using an
embedded Node program for byte-safe git paths and SHA-256 blob hashing. Bash is
the public entry point; Node is already a repository requirement. Git commands
use explicit resolved object IDs, disable external diff/textconv, and inspect
committed bytes. No queue writes, hook changes, binary evidence, or live state.
Fetch is opt-in. Read reviewed rows with the queue package rows() API rooted in
the caller's repository; this preserves canonical/witness/replay checks.
Only the latest round counts; any changes receipt blocks that round. A matching
approval authorizes exactly the manifest path/hash, or a path changed from the
commit candidate's first parent with matching committed blob hash and mode.
Deleted binaries need no candidate. Multiple --row values form a union.
Add scratch-only integration coverage in scripts/test-push-check.sh. Reuse queue
test fixture helpers to bootstrap real scratch queues, then use queue verbs to
open rounds and record receipts. All git writes, bare remotes and queues remain
under TMPDIR. Test fail-closed ordering and five required mutants, generated as
text copies in scratch. No old tests are modified. Add the TOOLS entry.
Every run needs Sage CLEAR; run timeout -k 10. Record plain-text results here or
in this packet. After green checks, commit and push only bradford/row327 through
the repository helper, send Sage and Poe tip/base/path sha256 manifest, and move
327 in-review. Sage owns request posting and landing; Dewey reviews once there.
## Recovery and choices
Initial worktree queue next refused linked-worktree access as designed. Queue
claim is performed from the canonical checkout; production reads in the guard
retain the same refusal rather than invent a worktree override.
Requested Sage ruling on AGENTS log appends outside the brief's owned paths.
Sage sage-3989: lead handles BUILD-LOG and SESSIONS at landing. Session entry:
2026-10-24 Bradford — row 327 / #1825 — implementation and scratch verification
in this packet; outcome to be appended after acceptance.
Implementation note: commit candidates use an explicit first-parent diff (empty
tree for a root commit), with rename detection disabled, like verify-commit.
The changes-veto case has both an approval and a changes receipt; a veto mutant
would otherwise still fail for lack of approval and provide false confidence.
A receipt for another candidate cannot be produced through queue verbs: the
suite corrupts only a scratch queue and expects the production reader to refuse.
Diff color, external diff and textconv are disabled so scans inspect plain git
output. Paths are read from NUL-delimited records (including spaces/tabs).
## Verification (Sage CLEAR sage-3991)
First new-suite run red: my whitespace-path fixture attempted a manifest path
that the queue intentionally forbids. Corrected the fixture to a commit
candidate, which can contain that git path. Production queue code and old tests
remain unchanged. Raw first-run output is test-run-1.txt.
Second run green: PASS push-check 22 checks; KILLED skip-hash,
accept-changes, earlier-round, working-tree, no-row. test-run-2.txt holds the
plain-text receipt. bash -n both scripts and git diff --check passed. Runs used
TMPDIR recorded in tmpdir.txt and timeout -k 10 240; all generated binary bytes
were in disposable scratch repositories, never in this checkout or evidence.
The slot was used for only the authorized new suite and syntax/diff checks.
No claim of the lead's full scripts/test-*.sh landing gate is made here.
Final inspection after the first green run found a path-decoding concern: lossy
UTF-8 decoding can conflate distinct git filenames. Hardened NUL-delimited path
decoding to reject invalid UTF-8, and quote newline-containing paths in refusal
output to preserve one STOP line. Added scratch cases for both. Requested a new
CLEAR (bradford-0005) because the previous slot had already been released.
Final run (sage-3994 CLEAR) green on final code: PASS push-check 24 checks,
all five mutants KILLED again, bash -n both scripts and git diff --check passed.
Receipt: test-run-3.txt. The guard rejects non-UTF-8 git paths as environment
errors rather than comparing replacement characters; newline paths remain
checkable through commit candidates and refusals quote them on one line.
Acceptance for the builder stage: ordered tokens and stop behavior, explicit
base/fetch, exact blob manifest/commit checks, latest-round/veto checks, deletion,
log/FF/secrets/attribution refusals, and all required mutants are covered. Queue
reads use rows() with production validation; tests use only scratch queues.
Candidate-mismatched receipts are impossible through the queue API and are
covered by a corrupt scratch queue refusing its read. The lead's full-suite gate,
Poe first review, Dewey landing review, and adoption for Sage's next push remain
landing stages. No hooks, role policy, credentials or other seats' paths changed.
## Poe first review R1 (poe-0010) — changes
cceea407e has a blocking subdirectory bypass: ls-tree without --full-tree can
omit an existing binary outside the caller cwd, making it look deleted.
Accepted the finding. Git now anchors all invocations at the repository root,
ls-tree uses --full-tree, and diff/diff-tree use --no-relative. EXPECTED_LOG still
resolves from the original caller cwd. Added outside-cwd and nested-binary cases
under diff.relative=true: unreviewed stops, reviewed manifest/commit passes.
No old test changed. New run requires Sage CLEAR before execution.
R1 verification under sage-3999 CLEAR: test-run-4.txt records PASS 29 checks
and KILLED all five required mutants. Syntax and diff whitespace checks passed.
A second bounded run used the new suite against cceea407e's script copied under
TMPDIR: r1-regression-red.txt records the outside-cwd unreviewed binary reaching
NO_BINARY/PUSH_READY (actual exit 0, expected 1). Thus the new regression detects
the reviewed bug, while the revised guard refuses it. No code changed between
the green run and this old-candidate reproduction. All runs used timeout -k 10.
Holding claimant queue writes per sage-3999 until Boyd finishes row 309's record.
Last acknowledged row 327 state is in-progress (bradford-0010, revision 3868).
## Dewey landing review B1 (dewey-0956) — changes
Read the invited review body at /mnt/storage/scratch/tmp/dew327vWXloCT/body.md.
B1 identifies a brief-level gap: numstat follows candidate/host git attributes,
so *.bin diff makes a NUL-containing blob disappear from the binary-only set.
The production script indeed filters numstat before reading any blob, consistent
with that data flow. Proposed correction: enumerate every changed numstat path,
skip absent/non-blob entries at SHA, classify binary when numstat says binary OR
committed blob has NUL among first 8000 bytes. Read with cat-file, never worktree.
Test committed .gitattributes, info/attributes, and core.attributesFile overrides
in scratch; reject with no row, allow exact reviewed bytes; add NUL-scan mutant.
Sage owns the brief amendment and next CLEAR/queue sequencing.
N1 acknowledged: candidate-equality check is defense in depth; invalid receipt
cannot pass queue replay, so the corrupt-queue case does not isolate that check.
N2/N3 recorded as described in review, without optional secrets/reader changes.
N4 planned hardening: refuse unconfirmed durable tail notes from rows() with
exit 2, with a real scratch witness-prefix case rather than a test-only reader.
No test run or queue write started for B1/N4 before Sage CLEAR.
Sage sage-4013/4015 authorizes B1 and N4 within the brief; amendment committed on
refactor at 9042754c4 (read with git show, no branch change). Fix and tests written
before requesting CLEAR as directed. Tests cover all three attribute sources,
approved manifest success under each override, exact 7999/8000 offset boundary,
and a real prefix-witness rollback to yield an unconfirmed approval tail.
Added only the requested sixth mutant, skip-nul-scan; the original five remain.
No new production configuration knobs, no secrets-diff change, no queue writes.
Will demonstrate red against the prior candidate using the new tests, then green
against revised code, under the approved slot. Full landing gate remains Sage's.
After Dewey's round-2 CHANGES record (comment 31522, rev3876), Sage sage-4016
released only the claimant return to in-progress. Acknowledged:
`ok bradford-0017 rev 3877 row 327 in-review→in-progress`.
No other queue write before the next in-review candidate move.
Test slot remains pending Sage CLEAR after Gyro's build.
## Amendment 1 verification (sage-4017 CLEAR)
b1-regression-red.txt: new tests against 704910ca3 reproduce attributes-hidden
asset.bin reaching NO_BINARY/PUSH_READY with exit0 instead of exit1.
n4-regression-red.txt: revised code with only the N4 check removed lets the real
scratch unconfirmed approval tail reach BINARY_REVIEWED/PUSH_READY, exit0 rather
than exit2. These are expected negative controls; neither changed committed code.
Final full new-suite run is green: test-run-5.txt records PASS 38 checks and
KILLED skip-hash, accept-changes, earlier-round, working-tree, no-row, skip-nul-scan.
All runs used timeout -k 10 240 with the same scratch TMPDIR; no old tests were
changed or run. bash -n both scripts and git diff --check also passed. No source
changes followed this green run. Sage's full landing gate is still separate.
Scope in the final delta: classify all changed present blobs using numstat OR
first-8000-byte NUL; fail closed on queue unconfirmed-tail notes; regression cases
and requested sixth mutant; TOOLS entry and this plain-text packet. B1's brief
amendment is Sage's 9042754c4; this branch does not edit the brief or queue files.
@@ -0,0 +1,27 @@
node:internal/modules/run_main:107
triggerUncaughtException(
^
AssertionError [ERR_ASSERTION]: LOG_OK
FF_OK
NO_BINARY
SECRETS_CLEAN
NO_ATTRIBUTION
PUSH_READY ee394f31d1e52751716e2413049f41b2f3072e65..76acc948149e718091fcd3f3807257e752a6616e
0 !== 1
at Object.check (file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:44:12)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:100:7
at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 0,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
Node.js v26.8.1
@@ -0,0 +1,27 @@
node:internal/modules/run_main:107
triggerUncaughtException(
^
AssertionError [ERR_ASSERTION]: LOG_OK
FF_OK
BINARY_REVIEWED 1
SECRETS_CLEAN
NO_ATTRIBUTION
PUSH_READY c1a8a1e9b8c3988874a8f7a35afab7c06aea94a5..325cd3671a7bf70b9a3aee60f4446bc93594a12a
0 !== 2
at Object.check (file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:44:12)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:121:7
at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 0,
expected: 2,
operator: 'strictEqual',
diff: 'simple'
}
Node.js v26.8.1
@@ -0,0 +1,27 @@
node:internal/modules/run_main:107
triggerUncaughtException(
^
AssertionError [ERR_ASSERTION]: LOG_OK
FF_OK
NO_BINARY
SECRETS_CLEAN
NO_ATTRIBUTION
PUSH_READY 896bdf0ef56596c67d2dc427275743c46026e832..ad9f9c86112fadafef4d4854f46bd9994fe2fe5d
0 !== 1
at Object.check (file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:44:12)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:130:7
at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 0,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
Node.js v26.8.1
@@ -0,0 +1,21 @@
node:internal/modules/run_main:107
triggerUncaughtException(
^
AssertionError [ERR_ASSERTION]: queue: candidate manifest path must be a repo-relative path: "space tab\tasset.bin"
2 !== 1
at Object.round (file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:61:12)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:116:7
at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 2,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
Node.js v26.8.1
@@ -0,0 +1,6 @@
PASS push-check 22 checks
KILLED skip-hash
KILLED accept-changes
KILLED earlier-round
KILLED working-tree
KILLED no-row
@@ -0,0 +1,6 @@
PASS push-check 24 checks
KILLED skip-hash
KILLED accept-changes
KILLED earlier-round
KILLED working-tree
KILLED no-row
@@ -0,0 +1,6 @@
PASS push-check 29 checks
KILLED skip-hash
KILLED accept-changes
KILLED earlier-round
KILLED working-tree
KILLED no-row
@@ -0,0 +1,7 @@
PASS push-check 38 checks
KILLED skip-hash
KILLED accept-changes
KILLED earlier-round
KILLED working-tree
KILLED no-row
KILLED skip-nul-scan
@@ -0,0 +1 @@
/mnt/storage/scratch/tmp/YPrPwR
+25
View File
@@ -1365,3 +1365,28 @@ public sign-in smoke and a forged-loopback 401 check; rollback drops flags
before middleware. Same-uid callers can still read the file; separate service
uid is Jason's question on #385. The Authentik allowed-uid file remains an
identity input and is never reused for proof.
## `scripts/push-check.sh` — verify a prospective push
`scripts/push-check.sh SHA EXPECTED_LOG [--row N]... [--base-ref REF] [--fetch]`
checks the repository at the caller's cwd; it never pushes or writes the queue.
Git checks cover the full repository even from a subdirectory; EXPECTED_LOG is
resolved relative to the original caller. Non-UTF-8 git paths refuse with exit 2.
EXPECTED_LOG must contain exactly `git log --oneline BASE..SHA` (including its
newlines). BASE defaults to `refs/remotes/origin/refactor`; `--base-ref` overrides
it. Only `--fetch` fetches origin, before resolving BASE.
Success prints `LOG_OK`, `FF_OK`, `NO_BINARY` or `BINARY_REVIEWED n`,
`SECRETS_CLEAN`, `NO_ATTRIBUTION`, then `PUSH_READY BASE..SHA` as the last line.
A changed present blob counts as binary when numstat marks it binary or its
first 8000 committed bytes contain NUL, regardless of git attributes. Each
present binary needs its committed blob sha256 and exact path in an approved
latest-round candidate from a supplied row. A `changes` receipt blocks that round.
Commit candidates authorize only changed paths with matching bytes and mode.
Deleted binaries need no review. Queue reads retain canonical-checkout checks
and refuse unconfirmed durable tails with exit 2.
Exit 0 means ready; 1 means refusal (`STOP reason`, always the last line); 2 means
usage or environment error. A refusal never prints later success tokens.
`timeout -k 10 240 bash scripts/test-push-check.sh` exercises isolated scratch
repositories and the six required mutants; set TMPDIR to a scratch filesystem.
+127
View File
@@ -0,0 +1,127 @@
#!/bin/bash
# Check an explicit committed push range. Fetch is opt-in; never pushes.
command -v node >/dev/null 2>&1 || { echo 'STOP node unavailable'; exit 2; }
exec node --input-type=module - "$@" <<'JS'
import { spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const stop = (reason, code = 1) => { console.log(`STOP ${reason}`); process.exit(code); };
let gitRoot = process.cwd();
const git = (...args) => {
const r = spawnSync('git', args, { cwd: gitRoot, maxBuffer: 256 * 1024 * 1024 });
if (r.error || r.status !== 0) throw new Error('git');
return r.stdout;
};
// Git paths must round-trip as UTF-8 before comparing them with queue paths.
const pathsText = bytes => new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
const shownPath = path => /[\r\n]/.test(path) ? JSON.stringify(path) : path;
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
const text = (...args) => git(...args).toString('utf8').trimEnd();
const resolve = ref => text('rev-parse', '--verify', '--end-of-options', `${ref}^{commit}`);
const argv = process.argv.slice(2);
if (argv.length < 2) stop('usage: push-check.sh SHA EXPECTED_LOG [--row N]... [--base-ref REF] [--fetch]', 2);
const [ref, expectedFile] = argv.splice(0, 2);
let baseRef = 'refs/remotes/origin/refactor', fetch = false;
const rowIds = new Set();
while (argv.length) {
const flag = argv.shift();
if (flag === '--fetch' && !fetch) fetch = true;
else if (flag === '--row' && /^[1-9][0-9]*$/.test(argv[0] ?? '') && Number.isSafeInteger(Number(argv[0]))) rowIds.add(Number(argv.shift()));
else if (flag === '--base-ref' && argv[0] && !argv[0].startsWith('-')) baseRef = argv.shift();
else stop('usage', 2);
}
let sha, base, expected;
try {
// Resolve the log against the original caller, then anchor every Git command
// at the repository root. Queue reads retain their production cwd checks.
expected = readFileSync(expectedFile);
gitRoot = text('rev-parse', '--show-toplevel');
sha = resolve(ref);
}
catch { stop('sha or expected log unavailable', 2); }
if (fetch) { try { git('fetch', '-q', 'origin'); } catch { stop('fetch', 2); } }
try { base = resolve(baseRef); } catch { stop('base ref unavailable', 2); }
try {
if (!git('log', '--oneline', `${base}..${sha}`).equals(expected)) stop('log');
console.log('LOG_OK');
try { git('merge-base', '--is-ancestor', base, sha); } catch { stop('ff'); }
console.log('FF_OK');
// --no-renames keeps -z numstat to exactly one path per record. Never parse
// quoted path output or consult working-tree bytes.
const changedPaths = pathsText(git('diff', '--no-relative', '--no-color', '--no-ext-diff', '--no-textconv', '--no-renames', '--numstat', '-z', base, sha))
.split('\0').filter(Boolean).map(record => {
const first = record.indexOf('\t'), second = record.indexOf('\t', first + 1);
return { path: record.slice(second + 1), binary: record.startsWith('-\t-\t') };
});
const tree = commit => {
const entries = new Map();
for (const entry of pathsText(git('ls-tree', '--full-tree', '-r', '-z', commit)).split('\0').filter(Boolean)) {
const tab = entry.indexOf('\t');
const [mode, type, oid] = entry.slice(0, tab).split(' ');
entries.set(entry.slice(tab + 1), { mode, type, oid });
}
return entries;
};
const have = tree(sha);
// Attributes can force a binary to appear as text in numstat. Inspect raw
// committed bytes as well, following Git's first-8000-byte NUL rule.
const present = changedPaths.filter(({ path, binary }) => {
const entry = have.get(path);
if (!entry || entry.type !== 'blob') return false;
return binary || git('cat-file', 'blob', entry.oid).subarray(0, 8000).includes(0);
}).map(({ path }) => path);
let approved = [];
if (present.length && rowIds.size) {
let rows;
try {
const root = text('rev-parse', '--show-toplevel');
const { rows: readRows } = await import(pathToFileURL(join(root, 'packages/queue/src/store.mjs')).href);
const result = readRows({ cwd: process.cwd(), env: process.env });
if (result.code !== 0) throw new Error('queue');
if (result.err.some(note => /unconfirmed tail|not confirmed durable/.test(note))) stop('queue unconfirmed tail', 2);
rows = result.rows;
for (const id of rowIds) if (!rows.some(r => r.id === id)) throw new Error('row');
} catch { stop('queue unavailable or invalid', 2); }
for (const row of rows.filter(r => rowIds.has(r.id))) {
const round = row.review?.rounds.at(-1);
if (!round || round.receipts?.some(r => r.verdict === 'changes')) continue;
if (!round.receipts?.some(r => r.verdict === 'approve' && r.candidate === round.candidate.digest)) continue;
approved.push(round.candidate);
}
}
const candidates = new Map();
for (const c of approved) {
if (c.kind !== 'commit') continue;
const candidateTree = tree(c.digest);
let parent;
try { parent = text('rev-parse', '--verify', `${c.digest}^1`); }
catch { parent = text('hash-object', '-t', 'tree', '/dev/null'); }
const changed = new Set(pathsText(git('diff-tree', '--no-relative', '-r', '--no-renames', '--name-only', '-z', parent, c.digest)).split('\0').filter(Boolean));
candidates.set(c.digest, { candidateTree, changed });
}
for (const path of present) {
const h = have.get(path);
const digest = hash(git('cat-file', 'blob', h.oid));
const matches = approved.some(c => {
if (c.kind === 'manifest') return c.text.split('\n').includes(`${digest} ${path}`);
const { candidateTree, changed } = candidates.get(c.digest);
const entry = candidateTree.get(path);
return changed.has(path) && entry?.type === 'blob' && entry.mode === h.mode && hash(git('cat-file', 'blob', entry.oid)) === digest;
});
if (!matches) stop(`binary ${shownPath(path)} ${digest} not in a reviewed candidate`);
}
console.log(present.length ? `BINARY_REVIEWED ${present.length}` : 'NO_BINARY');
const diff = git('diff', '--no-relative', '--no-color', '--no-ext-diff', '--no-textconv', base, sha).toString('utf8');
const secret = /BEGIN [A-Z ]*PRIVATE KEY|ghp_[A-Za-z0-9]{20}|sk-ant-[A-Za-z0-9-]{10}|xox[bp]-|AKIA[0-9A-Z]{16}|(token|secret|password)["' ]*[:=] *["'][A-Za-z0-9_\-]{24,}/i;
if (diff.split('\n').some(line => line.startsWith('+') && secret.test(line))) stop('secrets');
console.log('SECRETS_CLEAN');
if (/co-authored-by|generated with/i.test(text('log', '--format=%B', `${base}..${sha}`))) stop('attribution');
console.log('NO_ATTRIBUTION');
console.log(`PUSH_READY ${base}..${sha}`);
} catch { stop('git or environment error', 2); }
JS
+256
View File
@@ -0,0 +1,256 @@
#!/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