mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-14 20:37:14 +01:00
fix: close review-cycle gaps (a stamp must change a stamp file, result requires a full --head and fails closed without headRefOid, digits-only --pr) with tests for every cycle-3 branch
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
4afaa98f54
commit
95cf833708
7 files changed
+54
-12
No files matched your search
@@ -330,6 +330,11 @@ describe('ready', () => {
|
||||
const bare = queue(repo, ['ready', '--pr', '1', '--pregate', pregateFor(repo, bareMerge)], shim(bareMerge, shippedBody('1', pin)));
|
||||
expect(bare.status).toBe(EXIT.refused);
|
||||
expect(bare.err).toMatch(staleRe(bareMerge));
|
||||
repo.git('commit', '-q', '--allow-empty', '-m', 'empty commit is not a stamp');
|
||||
const emptyOnMerge = repo.git('rev-parse', 'HEAD');
|
||||
const empty = queue(repo, ['ready', '--pr', '1', '--pregate', pregateFor(repo, emptyOnMerge)], shim(emptyOnMerge, shippedBody('1', pin)));
|
||||
expect(empty.status).toBe(EXIT.refused);
|
||||
expect(empty.err).toMatch(staleRe(emptyOnMerge));
|
||||
repo.git('reset', '-q', '--hard', pin);
|
||||
fs.appendFileSync(path.join(repo.work, 'pr-s.txt'), 'real change before the merge\n');
|
||||
repo.git('commit', '-qam', 'edit on the pin side');
|
||||
|
||||
@@ -154,9 +154,11 @@ describe('gstack-ship-receipt result (/ship terminal line, GBRA-108)', () => {
|
||||
const head = repo.git('rev-parse', 'HEAD');
|
||||
const state = path.join(repo.root, 'state');
|
||||
const env = { ...ghShim(repo, {
|
||||
5: { body: placeReceiptFirst('Prose.\n', { schema_version: 1, pr: 5, head, base: 'main', queue_mode: 'stamp-at-merge' } as any) },
|
||||
6: { body: placeReceiptFirst('Prose.\n', { schema_version: 1, pr: 6, head: 'b'.repeat(40), base: 'main' } as any) },
|
||||
7: { body: 'No receipt here.\n' },
|
||||
5: { headRefOid: head, body: placeReceiptFirst('Prose.\n', { schema_version: 1, pr: 5, head, base: 'main', queue_mode: 'stamp-at-merge' } as any) },
|
||||
6: { headRefOid: head, body: placeReceiptFirst('Prose.\n', { schema_version: 1, pr: 6, head: 'b'.repeat(40), base: 'main' } as any) },
|
||||
7: { headRefOid: head, body: 'No receipt here.\n' },
|
||||
9: { headRefOid: 'd'.repeat(40), body: placeReceiptFirst('Prose.\n', { schema_version: 1, pr: 9, head, base: 'main' } as any) },
|
||||
10: { body: placeReceiptFirst('Prose.\n', { schema_version: 1, pr: 10, head, base: 'main' } as any) },
|
||||
}), GSTACK_STATE_ROOT: state, GSTACK_SESSION_KIND: 'unattended' };
|
||||
const ok = runBin(repo, 'gstack-ship-receipt', ['result', '--pr', '5'], env);
|
||||
expect(ok.status).toBe(EXIT.ok);
|
||||
@@ -179,6 +181,13 @@ describe('gstack-ship-receipt result (/ship terminal line, GBRA-108)', () => {
|
||||
const missing = runBin(repo, 'gstack-ship-receipt', ['result', '--pr', '7', '--json'], env);
|
||||
expect(missing.status).toBe(EXIT.refused);
|
||||
expect(JSON.parse(missing.out)).toMatchObject({ skill: 'ship', status: 'refused', run: '-', code: 'QUEUE_RECEIPT_MISSING' });
|
||||
const unpushed = runBin(repo, 'gstack-ship-receipt', ['result', '--pr', '9'], env);
|
||||
expect([unpushed.status, unpushed.out.trim()]).toEqual([EXIT.refused, 'GSTACK_RESULT: skill=ship status=refused run=-']);
|
||||
expect(unpushed.err).toContain(`HEAD ${head.slice(0, 12)} is not PR #9's pushed head dddddddddddd; push it, then rerun gstack-ship-receipt result`);
|
||||
const noOid = runBin(repo, 'gstack-ship-receipt', ['result', '--pr', '10'], env);
|
||||
expect([noOid.status, noOid.out.trim()]).toEqual([EXIT.fail, 'GSTACK_RESULT: skill=ship status=incomplete run=-']);
|
||||
expect(noOid.err).toContain('gh pr view returned no headRefOid for PR #10');
|
||||
expect(runBin(repo, 'gstack-ship-receipt', ['result', '--pr', '--repo'], env).status).toBe(EXIT.usage);
|
||||
|
||||
const bodyFile = path.join(repo.work, '.gstack/tmp/ship-receipt.md');
|
||||
fs.mkdirSync(path.dirname(bodyFile), { recursive: true });
|
||||
@@ -189,6 +198,15 @@ describe('gstack-ship-receipt result (/ship terminal line, GBRA-108)', () => {
|
||||
expect(JSON.parse(fs.readFileSync(path.join(repo.root, 'run-out', 'ship-run.json'), 'utf8'))).toMatchObject({ head, pr: null, receipt_source: bodyFile, session_kind: 'unattended' });
|
||||
expect(runBin(repo, 'gstack-ship-receipt', ['result', '--body', path.join(repo.root, 'absent.md')], env).status).toBe(EXIT.refused);
|
||||
expect(runBin(repo, 'gstack-ship-receipt', ['result'], env).status).toBe(EXIT.usage);
|
||||
const relative = runBin(repo, 'gstack-ship-receipt', ['result', '--body', bodyFile, '--out', 'rel-out'], env);
|
||||
expect(relative.status).toBe(EXIT.ok);
|
||||
expect(relative.out.trim()).toBe(`GSTACK_RESULT: skill=ship status=complete run=${path.join(repo.work, 'rel-out')}`);
|
||||
expect(fs.existsSync(path.join(repo.work, 'rel-out', 'ship-run.json'))).toBe(true);
|
||||
const blocker = path.join(repo.root, 'not-a-dir');
|
||||
fs.writeFileSync(blocker, 'file\n');
|
||||
const unwritable = runBin(repo, 'gstack-ship-receipt', ['result', '--body', bodyFile, '--out', path.join(blocker, 'sub')], env);
|
||||
expect([unwritable.status, unwritable.out.trim()]).toEqual([EXIT.fail, 'GSTACK_RESULT: skill=ship status=incomplete run=-']);
|
||||
expect(unwritable.err).toContain(`cannot write the run record under ${path.join(blocker, 'sub')}`);
|
||||
expect(fs.readdirSync(path.join(state, 'ship', 'runs')).length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -218,7 +236,7 @@ describe('gstack-ship-receipt result (/ship terminal line, GBRA-108)', () => {
|
||||
const invalid = runBin(repo, 'gstack-ship-receipt', ['result', '--body', invalidFile], env);
|
||||
expect([invalid.status, invalid.out.trim()]).toEqual([EXIT.refused, 'GSTACK_RESULT: skill=ship status=refused run=-']);
|
||||
expect(invalid.err).toMatch(/the receipt's ship receipt is invalid \(.*\).*\(QUEUE_RECEIPT_MISSING/);
|
||||
const otherPrEnv = { ...env, ...ghShim(repo, { 8: { body: placeReceiptFirst('Prose.\n', { schema_version: 1, pr: 99, head, base: 'main' } as any) } }) };
|
||||
const otherPrEnv = { ...env, ...ghShim(repo, { 8: { headRefOid: head, body: placeReceiptFirst('Prose.\n', { schema_version: 1, pr: 99, head, base: 'main' } as any) } }) };
|
||||
const otherPr = runBin(repo, 'gstack-ship-receipt', ['result', '--pr', '8'], otherPrEnv);
|
||||
expect([otherPr.status, otherPr.out.trim()]).toEqual([EXIT.refused, 'GSTACK_RESULT: skill=ship status=refused run=-']);
|
||||
expect(otherPr.err).toContain("PR #8's ship receipt names pr=99, not this PR");
|
||||
@@ -229,10 +247,10 @@ describe('gstack-ship-receipt result (/ship terminal line, GBRA-108)', () => {
|
||||
expect(JSON.parse(fs.readFileSync(path.join(repo.root, 'pinned-out', 'ship-run.json'), 'utf8')).head).toBe('c'.repeat(40));
|
||||
const badHead = runBin(repo, 'gstack-ship-receipt', ['result', '--body', pinned, '--head', '../../etc'], env);
|
||||
expect([badHead.status, badHead.out]).toEqual([EXIT.usage, '']);
|
||||
expect(badHead.err).toContain('--head must be a hex commit id of at least 12 characters');
|
||||
const shortHead = runBin(repo, 'gstack-ship-receipt', ['result', '--body', pinned, '--head', 'abcdef1'], env);
|
||||
expect(badHead.err).toContain('--head must be a full hex commit id');
|
||||
const shortHead = runBin(repo, 'gstack-ship-receipt', ['result', '--body', pinned, '--head', head.slice(0, 12)], env);
|
||||
expect([shortHead.status, shortHead.out]).toEqual([EXIT.usage, '']);
|
||||
expect(shortHead.err).toContain('--head must be a hex commit id of at least 12 characters');
|
||||
expect(shortHead.err).toContain('--head must be a full hex commit id');
|
||||
const noGit = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-receipt-nogit-'));
|
||||
const outside = runBin(repo, 'gstack-ship-receipt', ['result', '--body', pinned], env, noGit);
|
||||
expect([outside.status, outside.err.trim()]).toEqual([EXIT.usage, 'gstack-ship-receipt result: --head <sha> is required outside a git repository']);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* prune, ready's screen choice, and the policy gate.
|
||||
*/
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
@@ -215,6 +216,16 @@ describe('ready under a train policy: the screen choice', () => {
|
||||
const missing = queue(fx, ['ready', '--pr', '1', '--pregate', pregate2, '--screen']);
|
||||
expect(missing.status).toBe(EXIT.refused);
|
||||
expect(missing.err).toMatch(/has no refs\/pull\/1\/merge.*\(QUEUE_SCREEN_STALE_REF\)/);
|
||||
const tree = repo.git('rev-parse', `${moved}^{tree}`);
|
||||
const main = repo.git('rev-parse', 'origin/main');
|
||||
const badMerge = spawnSync('git', ['-C', repo.origin, 'hash-object', '-t', 'commit', '-w', '--stdin', '--literally'], { input: `tree ${tree}\nparent ${main}\nparent ${moved}\nauthor A <a@example.com> 1700000000 +0000\ncommitter B 1700000000 +0000\n\nmerge without a committer email\n`, encoding: 'utf8' }).stdout.trim();
|
||||
spawnSync('git', ['-C', repo.origin, 'update-ref', 'refs/pull/1/merge', badMerge]);
|
||||
repo.git('config', 'fetch.fsckObjects', 'true');
|
||||
const unfetchable = queue(fx, ['ready', '--pr', '1', '--pregate', pregate2, '--screen']);
|
||||
expect(unfetchable.status).toBe(EXIT.refused);
|
||||
expect(unfetchable.err).toMatch(/could not fetch refs\/pull\/1\/merge from origin: .*\(QUEUE_SCREEN_STALE_REF\)/);
|
||||
expect(line(unfetchable.out, 'NEXT:')).toBe('NEXT: check the remote, then gstack-ship-queue ready --pr 1 --screen');
|
||||
repo.git('config', '--unset', 'fetch.fsckObjects');
|
||||
expect(dispatches(fx)).toEqual([]);
|
||||
expect(fs.existsSync(path.join(fx.state, 'ship-queue', 'ready'))).toBe(false);
|
||||
repo.git('push', '-q', '-f', 'origin', `${mergeRefCommit(fx, moved)}:refs/pull/1/merge`);
|
||||
@@ -247,6 +258,11 @@ describe('ready under a train policy: the screen choice', () => {
|
||||
expect(followed.status).toBe(EXIT.ok);
|
||||
expect(line(followed.out, 'SCREEN_DISPATCH:')).toBe('SCREEN_DISPATCH: following run 102 (same merge SHA, base and policy; no duplicate)');
|
||||
expect(dispatches(fx, 'evals.yml').length).toBe(3);
|
||||
const liveBuild = queue(fx, ['train', 'build', '--prs', `1@${head}`, '--dry-run']);
|
||||
expect([liveBuild.status, line(liveBuild.out, 'ORDER:'), line(liveBuild.out, 'SCREEN:')]).toEqual([EXIT.ok, 'ORDER: 1:#1(screened)', '']);
|
||||
fx.write(d => { d.rules['evals.yml'] = { status: 'in_progress', conclusion: null }; });
|
||||
const runningBuild = queue(fx, ['train', 'build', '--prs', `1@${head}`, '--dry-run']);
|
||||
expect([runningBuild.status, line(runningBuild.out, 'ORDER:'), line(runningBuild.out, 'SCREEN:')]).toEqual([EXIT.ok, 'ORDER: 1:#1(screened)', '']);
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user