Files
gstack/test/helpers/ship-bin-fixture.ts
Garry Tanandcapy-ai[bot] b6cb70aea1 v1.91.355.0 ship: poll readers in-program, 32 KB returns, gated fix program, unattended push defers the guard offer, calls meter (GSTA-167 calls PR 1) (#3662)
* ship: poll readers in-program, never wait on wakes; 32 KB returns; gated fix program; unattended push defers the guard offer; calls meter (GSTA-167 calls PR 1)

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

* fix: pre-landing review fixes (inventory lib/ship-release.ts as a GSTACK_SESSION_KIND reader)

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

* docs(ship-budget): document calls and dotted --step notes

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

* ship: judge treats a missing severity as invalid; Capy passes rendered briefs via cat (replay C1-34 breaks)

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

* fix: keep the Capy skill under its context budget after the main merge

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-10-31 07:22:56 +00:00

86 lines
5.3 KiB
TypeScript

/**
* Fixture for gstack-ship-preflight and gstack-ship-release tests (GSTA-167 PR 4): a bare
* origin and a clone under the OS temp directory, outside this checkout, with an explicit
* seed identity on every repository, a private state root, and a stub gh on PATH whose
* output a test sets.
*/
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
const ROOT = path.resolve(import.meta.dir, '..', '..');
export const PREFLIGHT = path.join(ROOT, 'bin', 'gstack-ship-preflight');
export const RELEASE = path.join(ROOT, 'bin', 'gstack-ship-release');
export interface ShipFx {
root: string; work: string; origin: string; state: string;
git(...args: string[]): { status: number | null; stdout: string; stderr: string };
write(files: Record<string, string>): void;
commit(msg: string): void;
/** Push to origin/main from a second clone, moving the base under the feature branch. */
advanceBase(files: Record<string, string>): void;
gh(script: string): void;
run(bin: string, args: string[], extraEnv?: Record<string, string>): { status: number | null; stdout: string; stderr: string };
cleanup(): void;
}
/** origin's URL as a GitHub remote; the clone's insteadOf maps it to the bare repository, so gh calls see a GitHub repo and git still works offline. */
export const GITHUB_URL = 'https://github.com/fx/fx.git';
const IDENTITY = ['-c', 'user.email=fixture@example.com', '-c', 'user.name=Fixture', '-c', 'commit.gpgsign=false'];
export function shipFixture(base: Record<string, string> = {}, feat: Record<string, string> = { 'src/b.ts': 'export const b = 2;\n' }): ShipFx {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ship-bin-')));
if (!path.relative(ROOT, root).startsWith('..')) throw new Error('fixture must live outside the checkout');
const origin = path.join(root, 'origin.git');
const work = path.join(root, 'work');
const state = path.join(root, 'state');
const stub = path.join(root, 'stub');
fs.mkdirSync(stub, { recursive: true });
const gitIn = (cwd: string, args: string[]) => {
const r = spawnSync('git', [...IDENTITY, ...args], { cwd, encoding: 'utf8', timeout: 30_000 });
return { status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
};
const writeIn = (dir: string, files: Record<string, string>) => {
for (const [p, t] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(dir, p)), { recursive: true }); fs.writeFileSync(path.join(dir, p), t); }
};
const must = (r: { status: number | null; stderr: string }, what: string) => { if (r.status !== 0) throw new Error(`${what}: ${r.stderr}`); };
must(gitIn(root, ['init', '-q', '--bare', '-b', 'main', origin]), 'init origin');
must(gitIn(root, ['clone', '-q', origin, work]), 'clone');
writeIn(work, { VERSION: '1.0.0.0\n', 'CHANGELOG.md': '# Changelog\n\n## [Unreleased]\n\n- pending\n', 'package.json': '{"name":"fx","version":"1.0.0"}\n',
'README.md': '# Fx\n', 'src/a.ts': 'export const a = 1;\n', ...base });
must(gitIn(work, ['add', '-A']), 'add'); must(gitIn(work, ['commit', '-qm', 'base']), 'seed commit');
must(gitIn(work, ['push', '-q', 'origin', 'main']), 'push base'); gitIn(work, ['remote', 'set-head', 'origin', 'main']);
must(gitIn(work, ['config', `url.${origin}.insteadOf`, GITHUB_URL]), 'insteadOf'); must(gitIn(work, ['config', 'remote.origin.url', GITHUB_URL]), 'github origin');
must(gitIn(work, ['checkout', '-qb', 'feat']), 'branch');
writeIn(work, feat);
must(gitIn(work, ['add', '-A']), 'add feat'); must(gitIn(work, ['commit', '-qm', 'feat']), 'feat commit');
const { GSTACK_SESSION_KIND: _k, GSTACK_HEADLESS: _h, OPENCLAW_SESSION: _o, CI: _c, GITHUB_ACTIONS: _g, ...ambient } = process.env;
const env = { ...ambient, PATH: `${stub}:${process.env.PATH}`, GSTACK_STATE_ROOT: state, GSTACK_HOME: state, GIT_AUTHOR_NAME: 'Fixture', GIT_AUTHOR_EMAIL: 'fixture@example.com',
GIT_COMMITTER_NAME: 'Fixture', GIT_COMMITTER_EMAIL: 'fixture@example.com', GIT_CONFIG_GLOBAL: path.join(root, 'gitconfig'), GSTACK_SESSION_KIND: 'interactive' };
fs.writeFileSync(path.join(root, 'gitconfig'), '[commit]\n\tgpgsign = false\n');
const fx: ShipFx = {
root, work, origin, state,
git: (...args) => gitIn(work, args),
write: files => writeIn(work, files),
commit: msg => { must(gitIn(work, ['add', '-A']), 'add'); must(gitIn(work, ['commit', '-qm', msg]), msg); },
advanceBase: files => {
const other = fs.mkdtempSync(path.join(root, 'other-'));
must(gitIn(root, ['clone', '-q', origin, other]), 'clone other');
writeIn(other, files);
must(gitIn(other, ['add', '-A']), 'add'); must(gitIn(other, ['commit', '-qm', 'base moves']), 'base moves'); must(gitIn(other, ['push', '-q', 'origin', 'main']), 'push');
},
gh: script => { fs.writeFileSync(path.join(stub, 'gh'), `#!/bin/sh\n${script}\n`, { mode: 0o755 }); },
run: (bin, args, extraEnv = {}) => {
const r = spawnSync(bin, args, { cwd: work, encoding: 'utf8', timeout: 300_000, env: { ...env, ...extraEnv } });
return { status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
},
cleanup: () => fs.rmSync(root, { recursive: true, force: true }),
};
fx.gh('exit 0');
return fx;
}
export const lineOf = (out: string, key: string) => out.split('\n').find(l => l.startsWith(`${key}: `)) ?? '';