import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
const ROOT = path.resolve(import.meta.dir, '..');
const roots: string[] = [];
afterEach(() => { for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); });
function git(cwd: string, ...args: string[]) {
const r = spawnSync('git', args, { cwd, encoding: 'utf8', timeout: 10_000 });
if (r.status !== 0) throw new Error(r.stderr);
return r.stdout.trim();
}
/** Step 1 item 3's commands from the /review template, run in order, with `` bound. */
function stepOneBlock(base: string): string {
const body = fs.readFileSync(path.join(ROOT, 'review/SKILL.md.tmpl'), 'utf8');
const item = body.slice(body.indexOf('3. Run `git fetch origin '), body.indexOf('{{SCOPE_DRIFT}}'));
const commands = [...item.matchAll(/`(git fetch [^`]+|DIFF_BASE=[^`]+)`/g)].map(m => m[1]!);
if (commands.length !== 2) throw new Error('Step 1 item 3 commands moved');
return commands.join('\n').replaceAll('', base);
}
function clone() {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'review-base-refresh-')));
roots.push(root);
const upstream = path.join(root, 'upstream');
git(root, 'init', '-q', '-b', 'main', upstream);
git(upstream, 'config', 'user.email', 't@t'); git(upstream, 'config', 'user.name', 't'); git(upstream, 'config', 'commit.gpgsign', 'false');
fs.writeFileSync(path.join(upstream, 'app.ts'), 'export const a = 1;\n');
git(upstream, 'add', '.'); git(upstream, 'commit', '-qm', 'base');
const repo = path.join(root, 'repo');
git(root, 'clone', '-q', upstream, repo);
git(repo, 'config', 'user.email', 't@t'); git(repo, 'config', 'user.name', 't'); git(repo, 'config', 'commit.gpgsign', 'false');
git(repo, 'checkout', '-qb', 'feature');
fs.writeFileSync(path.join(repo, 'app.ts'), 'export const a = 2;\n');
git(repo, 'commit', '-qam', 'change');
return { root, repo };
}
const run = (cwd: string, script: string) => spawnSync('bash', ['-c', script], { cwd, encoding: 'utf8', timeout: 20_000 });
describe('/review Step 1 base refresh', () => {
test('a failed fetch reports stale base coverage and still finds the diff', () => {
// E4: under Codex's read-only .git, `git fetch && merge-base && diff --stat` stopped at the fetch,
// so the empty output read as "Nothing to review".
const { repo } = clone();
git(repo, 'remote', 'set-url', 'origin', path.join(repo, 'no-such-remote'));
const result = run(repo, stepOneBlock('main'));
const rev = git(repo, 'rev-parse', '--short', 'origin/main');
expect(result.stdout).toContain(`BASE_REFRESH: stale ${rev}`);
expect(result.stdout).toMatch(/app\.ts \| 2 \+-/);
});
test('a working fetch reports fresh coverage and the same diff', () => {
const { repo } = clone();
const result = run(repo, stepOneBlock('main'));
expect(result.stdout).toContain('BASE_REFRESH: fresh');
expect(result.stdout).toMatch(/app\.ts \| 2 \+-/);
});
test('the review reports stale coverage instead of stopping', () => {
const body = fs.readFileSync(path.join(ROOT, 'review/SKILL.md.tmpl'), 'utf8').replace(/\s+/g, ' ');
expect(body).toContain('`stale` is not an empty diff: continue; report `Base coverage: stale at `.');
expect(body).not.toContain('git fetch origin --quiet && DIFF_BASE=');
});
});