fix(train-land): prove the record's S_i is on the gated chain before stamping; a merge main does not show yet pends

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-29 09:01:05 +00:00
1 parent 739a374090
commit 472dd924ae
3 files changed
+6 -3

No files matched your search

+2
View File
@@ -56,6 +56,7 @@ export async function trainLand(ctx: QueueContext, o: { train: string; pr: strin
};
const rerun = `${Q} train land --train ${t.nonce} --pr ${o.pr}`;
const lines = [`TRAIN_LAND: ${t.nonce} slot ${i + 1}/${t.slots.length} #${o.pr} pin ${pin} -> S${s.index} v${s.version} tree ${s.tree}`];
if (git(root, ['rev-parse', `${s.commit}^{tree}`]).out !== s.tree || !git(root, ['merge-base', '--is-ancestor', s.commit, t.head]).ok) return outRefused({ code: 'QUEUE_PROOF_MISSING', message: `the record's S${s.index} is not a commit with tree ${s.tree.slice(0, 12)} on the gated head ${t.head.slice(0, 12)}` }, lines, `${Q} train status --train ${t.nonce}`, run);
let main = mainNow();
if ('error' in main) return outRefused({ code: 'QUEUE_PROOF_MISSING', message: main.error }, lines, rerun, run);
if (main.tree === s.tree) return confirmMerged(env, t, o.pr, main.sha, s.index, [...lines, `main: ${main.sha} is S${s.index} exactly`]);
@@ -105,6 +106,7 @@ export async function trainLand(ctx: QueueContext, o: { train: string; pr: strin
if (!m.ok) return outFailed({ code: 'QUEUE_NOT_READY', message: `gh pr merge refused: ${(m.err ?? m.out).trim().slice(0, 200)}` }, lines, rerun, run);
main = mainNow();
if ('error' in main) return outPending({ code: 'QUEUE_PROOF_MISSING', message: `merged; ${main.error}` }, lines, rerun, run);
if (main.sha === j.base) return outPending({ code: 'QUEUE_NOT_READY', message: `merged; main still reads ${j.base.slice(0, 12)}, so the merge is not visible yet` }, lines, rerun, run);
if (main.tree !== s.tree) return stopped(env, t, 'QUEUE_TRAIN_TREE_MISMATCH', `after the merge main ${main.sha.slice(0, 12)} is not S${s.index} exactly`, main.sha, lines, true);
return confirmMerged(env, t, o.pr, main.sha, s.index, [...lines, `MERGED: #${o.pr} as ${main.sha}; main's tree is S${s.index} exactly`]);
}
+2 -1
View File
@@ -83,8 +83,9 @@ export function stampChain(o: StampChainOptions): { stamped: StampedPrefix[]; li
if (linked) return { code: 'QUEUE_TRAIN_STAMP_MISMATCH', message: `S${i + 1} (#${slot.pr}): ${linked} is a symlink in the merged tree; the stamp writes release files only inside the repository` };
const r = restamp({ repoRoot: wt, base: o.base, baseRef: pred, next: true, policyFrom: 'base', allowRepoCommands: o.allowRepoCommands, dryRun: false, now: new Date(`${date}T12:00:00Z`), env: process.env });
if (r.status !== 'stamped') return { code: r.code ?? 'QUEUE_TRAIN_STAMP_MISMATCH', message: `S${i + 1} (#${slot.pr}): restamp ${r.status}: ${r.message ?? ''}`, fix: r.fix };
git(wt, ['add', '-A']);
if (!git(wt, ['add', '-A']).ok) return { code: 'QUEUE_TRAIN_STAMP_MISMATCH', message: `cannot stage the stamp for S${i + 1}` };
tree = git(wt, ['write-tree']).out;
if (!/^[0-9a-f]{40,64}$/.test(tree)) return { code: 'QUEUE_TRAIN_STAMP_MISMATCH', message: `cannot write the stamped tree for S${i + 1}` };
version = r.newVersion!;
} finally {
git(o.repoRoot, ['worktree', 'remove', '--force', wt]);
+2 -2
View File
@@ -118,7 +118,7 @@ describe('stamped trains: build and land', () => {
expect(line(drift.out, 'QUEUE_TRAIN_FROZEN:')).toMatch(/^QUEUE_TRAIN_FROZEN: train \S+ merged #1/);
}, 300_000);
test('a stamp commit that cannot be written pushes nothing and leaves the PR branch in place', () => {
test('a record whose S_i tree is not on the gated chain pushes nothing and leaves the PR branch in place', () => {
const { fx, h1, nonce } = stampedTrain();
expect(queue(fx, ['train', 'verdict', '--train', nonce]).status).toBe(EXIT.ok);
const file = path.join(fx.state, 'ship-queue', 'trains', nonce, 'train.json');
@@ -126,7 +126,7 @@ describe('stamped trains: build and land', () => {
t.stamped[0].tree = 'f'.repeat(40);
fs.writeFileSync(file, JSON.stringify(t));
const r = land(fx, nonce, '1');
expect(r.status).not.toBe(EXIT.ok);
expect(r.err).toMatch(/is not a commit with tree ffffffffffff on the gated head.*\(QUEUE_PROOF_MISSING\)/);
expect(r.out).not.toMatch(/^PUSHED:/m);
expect(fx.repo.git('--git-dir', fx.repo.origin, 'rev-parse', 'refs/heads/pr-1')).toBe(h1);
}, 180_000);