Files
gstack/test/helpers/ship-queue-train-cases.ts
Garry Tanandcapy-ai[bot] e25997515c v1.92.54.0 ship-queue: train land takes the shared land lock; verify proves the stamp; restamp stages its writes (#3599, #3531) (#3737)
* feat(ship): pause /ship on a red base instead of failing (gstack-base-red, conditional receipts) (GSTA-157 PR 1)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: base-red shell runner launches bash through runBashSync

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: base-red coverage additions (unreproduced head, unresolved head, readPause, wait unconfigured)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: base-red pause in unattended endings; BASE_RED_UNCONFIGURED covers an invalid base policy

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: base-red coverage pass 2 (write --base-red, summary line, classify usage before fetch, unresolved-head detail)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(ship-queue): merge trains for any repo — train.gates (workflows/command), every prefix gated, generalized train land (direct/update, merge method), base-red trains, train init/doctor, watch --land (GSTA-157 PR 2)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (train gates: dispatch bound, lease, rebase, post-merge checks, output tail, caps)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: train land refuses the next slot until the previous slot's post-merge checks are green

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: ship-policy, troubleshooting and captain for train gates review fixes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: changelog fragment for merge trains for any repo

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: keep ship-queue-train-generic out of the Windows subset (gh shim resolves gh.exe there)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ship-queue): train land takes the shared land lock (train-scoped, freeze-owned), verify --train proves the stamp and needs GitHub MERGED, restamp stages its writes (#3599, #3531)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (train keeps the land lock through a mid-train refusal; freeze takeover reads the notes ref; a refused lock push is not reported as a taken lock)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: a freeze lock is taken over only on a freeze record cleared after it (pre-landing review pass 2)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: a stale restamp staging marker is dropped, not a failure; a failed dry run refuses before the real run; settleLock names the real lock-write failure (Step 11 review)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ship-queue): land takes over a cleared freeze lock; deleted lock ref reads absent; unread verify head refuses QUEUE_PROOF_MISSING

Step 11 adversarial findings on PR #3737: a freeze lock whose release push failed
blocked every standalone land after its freeze cleared (freezeLockCleared shared by
land and train land takeover); readLock kept a cached ref deleted on origin; verify
--train --no-fetch judged the unstamped pin; the slot call now restores the worktree
for an unrequested fifth file.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore(changelog): name the fragment after the branch slug

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(captain): the slot restamp stages the stamp files it wrote

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(ship-queue): verify --train refuses QUEUE_PROOF_MISSING on an unread head

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>
2026-11-02 03:23:35 +00:00

65 lines
3.4 KiB
TypeScript

/** Train setups shared by the test/ship-queue-train*.test.ts files (moved from test/ship-queue-train.test.ts, GSTA-151). */
import { expect } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { mergeChangelogs } from '../../lib/changelog-check';
import { EXIT } from '../../lib/headless-artifacts';
import { runBin } from './restamp-fixture';
import { GREEN_GATE, line, nonceOf, openPr, queue, readyReceipt, type Rule, type TrainFixture } from './ship-queue-train-fixture';
export function twoPrTrain(fx: TrainFixture, gate: Rule = GREEN_GATE) {
const h1 = openPr(fx, '1');
const h2 = openPr(fx, '2');
readyReceipt(fx, '1', h1);
readyReceipt(fx, '2', h2);
fx.write(d => { d.rules['evals.yml'] = gate; });
const r = queue(fx, ['train', 'build', '--prs', `1@${h1},2@${h2}`]);
return { h1, h2, r, nonce: nonceOf(r.out) };
}
/** A main commit with a prefix's tree plus a VERSION stamp, the way stamp-at-merge publishes it. */
export function publishMain(fx: TrainFixture, prefixCommit: string, version: string, extra?: { file: string; text: string }): string {
const { repo } = fx;
repo.git('checkout', '-q', '-B', 'mainsim', 'origin/main');
repo.git('rm', '-rq', '.');
repo.git('checkout', prefixCommit, '--', '.');
fs.writeFileSync(path.join(repo.work, 'VERSION'), version + '\n');
if (extra) fs.writeFileSync(path.join(repo.work, extra.file), extra.text);
repo.git('add', '-A');
repo.git('commit', '-q', '-m', `merge to ${version}`);
repo.git('push', '-q', 'origin', 'HEAD:refs/heads/main');
repo.git('fetch', '-q', 'origin');
return repo.git('rev-parse', 'HEAD');
}
/** The lane's slot stamp on its pin (main merged in first from slot 2 on): gstack-restamp --next, then the stamp files committed and pushed as the PR head. */
export function laneStamp(fx: TrainFixture, pr: string, pin: string, o: { mergeMain?: boolean; dropStamp?: boolean } = {}): string {
const { repo } = fx;
repo.git('reset', '-q', '--hard');
repo.git('fetch', '-q', 'origin');
repo.git('checkout', '-q', '-B', `lane${pr}`, pin);
if (o.mergeMain) {
const m = spawnSync('git', ['merge', '--no-ff', '--no-commit', '-q', 'origin/main'], { cwd: repo.work, encoding: 'utf8', timeout: 30_000, env: { ...process.env, ...repo.env } });
if (m.status !== 0) {
const side = (n: number) => repo.git('show', `:${n}:CHANGELOG.md`) + '\n';
fs.writeFileSync(path.join(repo.work, 'CHANGELOG.md'), mergeChangelogs(side(2), side(3), side(1))!);
repo.git('add', 'CHANGELOG.md');
}
}
const index = repo.git('write-tree');
const stamp = runBin(repo, 'gstack-restamp', ['--next', '--base', 'main', '--allow-repo-commands'], fx.env);
expect(stamp.status, stamp.err).toBe(EXIT.ok);
const files = line(stamp.out, 'FILES:').slice('FILES: '.length).split(' ').filter(Boolean);
if (o.dropStamp) repo.git('read-tree', index);
else repo.git('add', '-A', '--', ...files);
repo.git('commit', '-q', '--no-edit', '-m', `slot stamp #${pr}`);
if (o.dropStamp) repo.git('checkout', '-q', '--', '.');
const head = repo.git('rev-parse', 'HEAD');
repo.git('push', '-q', '-f', 'origin', `${head}:refs/pull/${pr}/head`);
return head;
}
/** GitHub reports the PR MERGED, which verify --train needs before it confirms a merge (#3531). */
export const markMerged = (fx: TrainFixture, pr: string) => fx.write(d => { (d.prs[pr] as any).state = 'MERGED'; });