test(ship): the PR check precedes the Greptile dispatch, and each skip names its reason

Runs Step 10's generated check against stub gh for gh missing, not logged
in, a non-GitHub remote, no PR, a failed lookup and an existing PR. Fails on
the previous text (which printed PR: unknown and dispatched).

Co-authored-by: dgrant <dgrant@users.noreply.github.com>
This commit is contained in:
garrytananddgrant committed 2026-10-06 20:43:08 +00:00
1 parent edd011185c
commit 195aa4eeda
1 file changed
+54
+54
View File
@@ -0,0 +1,54 @@
/**
* /ship Step 10 (CEO-19, ENG-1): the PR check runs before the Greptile triage
* dispatch, and every outcome except an existing PR skips the dispatch with
* its reason. The check block is executed from the generated section against
* stub `gh` binaries.
*/
import { afterAll, describe, expect, test } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
const ROOT = path.resolve(import.meta.dir, '..');
const SECTION = fs.readFileSync(path.join(ROOT, 'ship/sections/greptile.md'), 'utf8');
const CHECK = SECTION.match(/```bash\n([\s\S]*?)\n```/)![1];
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-ship-greptile-'));
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
/** Run the check with a PATH holding only `head` and, unless null, a stub gh. */
function check(gh: string | null): string {
const bin = fs.mkdtempSync(path.join(tmp, 'bin-'));
fs.symlinkSync(Bun.which('head')!, path.join(bin, 'head'));
if (gh !== null) fs.writeFileSync(path.join(bin, 'gh'), `#!${Bun.which('bash')}\n${gh}\n`, { mode: 0o755 });
const r = spawnSync(Bun.which('bash')!, ['-c', CHECK], { cwd: tmp, encoding: 'utf8', timeout: 10_000, env: { PATH: bin } });
expect(r.status, r.stderr).toBe(0);
return r.stdout.trim();
}
describe('/ship Step 10 Greptile PR check', () => {
test('the PR check precedes the dispatch, and only an existing PR dispatches', () => {
const checkIdx = SECTION.indexOf('gh pr view --json number');
const dispatchIdx = SECTION.indexOf('Dispatch a subagent through Agent');
expect(checkIdx).toBeGreaterThan(-1);
expect(dispatchIdx).toBeGreaterThan(checkIdx);
const between = SECTION.slice(checkIdx, dispatchIdx);
expect(between).toContain('Only `PR: exists` dispatches.');
expect(between).toContain('"Greptile: not run (<reason>); runs on the PR once it exists"');
expect(SECTION).not.toContain('PR: unknown');
});
const ok = 'exit 0';
const cases: Array<[string, string | null, string]> = [
['gh missing', null, 'PR: skip (gh not installed)'],
['gh not logged in', `[ "$1" = auth ] && { echo "You are not logged into any GitHub hosts." >&2; exit 1; }; ${ok}`, 'PR: skip (gh not logged in)'],
['non-GitHub remote', `[ "$1" = repo ] && { echo "none of the git remotes configured for this repository point to a known GitHub host" >&2; exit 1; }; ${ok}`,
'PR: skip (not a GitHub remote: none of the git remotes configured for this repository point to a known GitHub host)'],
['no PR yet', `[ "$1" = pr ] && { echo 'no pull requests found for branch "feat"' >&2; exit 1; }; ${ok}`, 'PR: skip (no PR yet)'],
['other lookup error', `[ "$1" = pr ] && { printf 'HTTP 502: bad gateway\\nretry later\\n' >&2; exit 1; }; ${ok}`, 'PR: skip (PR lookup failed: HTTP 502: bad gateway)'],
['PR exists', `[ "$1" = pr ] && { echo 12; exit 0; }; ${ok}`, 'PR: exists'],
];
for (const [name, gh, expected] of cases) {
test(name, () => expect(check(gh)).toBe(expected));
}
});