mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-10 18:50:18 +01:00
* Give the ship-queue ready tests complete pregate records The notes-push and fresh-base suites wrote a one-check pregate record, which ready refuses as a partial run since #4258. They now write every registered check, as the other queue tests do. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Add a changelog entry for the ship-queue ready test fix 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>
85 lines
5.5 KiB
TypeScript
85 lines
5.5 KiB
TypeScript
/**
|
|
* `gstack-ship-queue ready` under a train policy records its READY receipt on
|
|
* origin's refs/notes/gstack-trains, which `land --pin` reads. When that push
|
|
* never reaches origin, ready must refuse instead of printing "notes pushed"
|
|
* and exiting 0 (reports/GSTA-461.md item 19: #4245's land refused
|
|
* QUEUE_NOT_READY because origin had no note).
|
|
*/
|
|
import { afterEach, describe, expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { EXIT } from '../../lib/headless-artifacts';
|
|
import { CHECK_IDS } from '../../lib/pregate/index';
|
|
import { cleanup, runBin } from '../../test/helpers/restamp-fixture';
|
|
import { line, openPr, queue, repoKey, trainFixture, type TrainFixture } from '../../test/helpers/ship-queue-train-fixture';
|
|
|
|
const NOTES = 'refs/notes/gstack-trains';
|
|
const FULL_CHECKS = CHECK_IDS.map(id => ({ id, status: 'pass' }));
|
|
const fixtures: TrainFixture[] = [];
|
|
afterEach(() => { while (fixtures.length) cleanup(fixtures.pop()!.repo); });
|
|
|
|
function readyFixture() {
|
|
const fx = trainFixture();
|
|
fixtures.push(fx);
|
|
const head = openPr(fx, '1');
|
|
fx.write(d => { d.ci[head] = [{ status: 'completed', conclusion: 'success' }]; });
|
|
const tree = runBin(fx.repo, 'gstack-wtree', []).out.trim();
|
|
const pregate = path.join(fx.repo.root, 'pregate.json');
|
|
fs.writeFileSync(pregate, JSON.stringify({ schema_version: 1, tree, head, verdict: 'pass', checks: FULL_CHECKS }));
|
|
return { fx, head, pregate };
|
|
}
|
|
|
|
function hook(fx: TrainFixture, name: 'pre-receive' | 'post-receive', body: string): void {
|
|
fs.writeFileSync(path.join(fx.repo.origin, 'hooks', name), `#!/bin/sh\n${body}\n`, { mode: 0o755 });
|
|
}
|
|
|
|
const remoteNotes = (fx: TrainFixture) => fx.repo.git('ls-remote', 'origin', NOTES).split(/\s+/)[0] ?? '';
|
|
const localNotes = (fx: TrainFixture) => fx.repo.git('rev-parse', '--verify', '--quiet', NOTES);
|
|
|
|
describe('ready refuses a READY note that never reached origin', () => {
|
|
// Value: protects=a lane never reports READY while origin lacks the note land reads; fails_when=ready ignores persist's pushed:false and exits 0 with "notes pushed"; why_new=no case pushes ready's note to a remote that rejects it; seam=a pre-receive hook in the fixture's bare origin; no_claim=GitHub's own rejection messages
|
|
test('origin rejects refs/notes/*: ready exits refused with QUEUE_READY_NOTE_UNPUSHED and prints no success', () => {
|
|
const { fx, head, pregate } = readyFixture();
|
|
hook(fx, 'pre-receive', 'while read old new ref; do case "$ref" in refs/notes/*) echo "notes refused by fixture" >&2; exit 1;; esac; done; exit 0');
|
|
const r = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--no-screen']);
|
|
expect(r.status).toBe(EXIT.refused);
|
|
expect(r.err).toContain('(QUEUE_READY_NOTE_UNPUSHED)');
|
|
expect(r.out).not.toContain('notes pushed');
|
|
expect(line(r.out, 'GSTACK_RESULT:')).toContain('status=refused');
|
|
expect(line(r.out, 'NEXT:')).toContain('gstack-ship-queue ready --pr 1 --no-screen');
|
|
expect(remoteNotes(fx)).toBe('');
|
|
expect(fs.existsSync(path.join(fx.state, 'ship-queue', 'ready', repoKey(fx), `1-${head}.json`))).toBe(true);
|
|
}, 120_000);
|
|
|
|
// Value: protects=success is printed only after origin is read back holding the note; fails_when=a push that exits 0 without moving origin's ref passes as pushed; why_new=the case above covers a push git reports as failed; seam=a post-receive hook that deletes the ref after accepting it, standing in for a proxy that drops the update; no_claim=which proxy dropped #4245's push
|
|
test('a push that exits 0 but leaves origin without the note is refused too', () => {
|
|
const { fx, pregate } = readyFixture();
|
|
hook(fx, 'post-receive', `git update-ref -d ${NOTES} 2>/dev/null; exit 0`);
|
|
const r = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--no-screen']);
|
|
expect(r.status).toBe(EXIT.refused);
|
|
expect(r.err).toContain('(QUEUE_READY_NOTE_UNPUSHED)');
|
|
expect(r.out).not.toContain('notes pushed');
|
|
expect(remoteNotes(fx)).toBe('');
|
|
}, 120_000);
|
|
|
|
// Value: protects=the normal push still succeeds and origin holds exactly the local notes tip; fails_when=the confirmation read refuses a push that did land; why_new=hold case for the two refusals above; seam=none; no_claim=a real GitHub remote
|
|
test('hold: a normal push succeeds, prints notes pushed, and origin holds the local notes tip', () => {
|
|
const { fx, pregate } = readyFixture();
|
|
const r = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--no-screen']);
|
|
expect(r.status).toBe(EXIT.ok);
|
|
expect(line(r.out, 'READY_RECEIPT:')).toEndWith(`(notes pushed to origin ${NOTES})`);
|
|
expect(remoteNotes(fx)).toBe(localNotes(fx));
|
|
}, 120_000);
|
|
|
|
// Value: protects=rerunning ready once origin accepts notes pushes the kept receipt and succeeds; fails_when=the refusal's NEXT command cannot recover; why_new=the documented fix for QUEUE_READY_NOTE_UNPUSHED; seam=the pre-receive hook removed between runs; no_claim=none beyond the fixture
|
|
test('rerunning ready after origin accepts notes again pushes the note and succeeds', () => {
|
|
const { fx, pregate } = readyFixture();
|
|
hook(fx, 'pre-receive', 'exit 1');
|
|
expect(queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--no-screen']).status).toBe(EXIT.refused);
|
|
fs.rmSync(path.join(fx.repo.origin, 'hooks', 'pre-receive'));
|
|
const again = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--no-screen']);
|
|
expect(again.status).toBe(EXIT.ok);
|
|
expect(remoteNotes(fx)).toBe(localNotes(fx));
|
|
}, 120_000);
|
|
});
|