mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 00:03:15 +01:00
test(review): review-plan and gate-diff plan the same with a stale local main or a shadowing tag (GST-883, failing first)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
589ff24286
commit
7bc4f0ea06
1 file changed
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* gstack-review-plan and gstack-gate-diff diff against origin/<base> (GST-883): with a local <base> that lags
|
||||
* origin/<base>, review-plan handed the raw name to gstack-gate-diff, which merged against the stale local ref and
|
||||
* scanned upstream commits the branch only inherited, so a code diff planned as gate class at full depth.
|
||||
*/
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
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 PLAN = path.join(import.meta.dir, '..', 'bin', 'gstack-review-plan');
|
||||
const GATE_DIFF = path.join(import.meta.dir, '..', 'bin', 'gstack-gate-diff');
|
||||
const FIXTURES = path.join(import.meta.dir, 'fixtures', 'gate-diff');
|
||||
const dirs: string[] = [];
|
||||
afterEach(() => { while (dirs.length) fs.rmSync(dirs.pop()!, { recursive: true, force: true }); });
|
||||
|
||||
function fixture() {
|
||||
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'rp-origin-')));
|
||||
dirs.push(root);
|
||||
const work = path.join(root, 'work');
|
||||
const env = { ...process.env, GSTACK_STATE_ROOT: path.join(root, 'state'), GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t', GIT_CONFIG_GLOBAL: path.join(root, 'gitconfig') };
|
||||
fs.writeFileSync(path.join(root, 'gitconfig'), '[commit]\n\tgpgsign = false\n');
|
||||
const sh = (cwd: string, c: string) => { const r = spawnSync('bash', ['-c', c], { cwd, env, encoding: 'utf8', timeout: 60_000 }); if (r.status !== 0) throw new Error(`${c}: ${r.stderr}`); return r.stdout.trim(); };
|
||||
sh(root, 'git init -q --bare -b main origin.git && git clone -q origin.git work 2>/dev/null');
|
||||
sh(work, `git apply '${FIXTURES}/rh1-skip.base.patch' && git add -A && git commit -qm base && git push -q origin main`);
|
||||
sh(root, `git clone -q origin.git other && cd other && git apply '${FIXTURES}/rh1-skip.patch' && git commit -qam upstream && git push -q origin main`);
|
||||
sh(work, "git fetch -q origin && git checkout -qb feat origin/main && echo 'export const v = 1;' > src/v.ts && git add -A && git commit -qm feat");
|
||||
return { work, env, sh };
|
||||
}
|
||||
|
||||
const planOf = (work: string, env: NodeJS.ProcessEnv) => {
|
||||
const r = spawnSync(PLAN, ['--base', 'main', '--json'], { cwd: work, env, encoding: 'utf8', timeout: 180_000 });
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
const p = JSON.parse(r.stdout) as { class: string; depth: string; gate_scan: unknown };
|
||||
return { class: p.class, depth: p.depth, gate_scan: p.gate_scan };
|
||||
};
|
||||
const gateDiff = (work: string, env: NodeJS.ProcessEnv, base: string) => {
|
||||
const r = spawnSync(GATE_DIFF, [base], { cwd: work, env, encoding: 'utf8', timeout: 120_000 });
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
return { tagged: /tagged=\{([^}]*)\}/.exec(r.stdout)?.[1], candidate: /candidate=(\S+)/.exec(r.stdout)?.[1], stderr: r.stderr };
|
||||
};
|
||||
|
||||
describe('review-plan and gate-diff read the origin merge base (GST-883)', () => {
|
||||
// Value: protects=a stale local <base> never turns inherited upstream commits into gate hunks that force full depth;
|
||||
// fails_when=gstack-gate-diff merges against the local <base> while origin/<base> is ahead, or review-plan hands it a base the plan's own diff did not use;
|
||||
// why_new=GEVA-117 paid ~15-20M tokens of full-depth readers for a gate hunk already on origin/main; seam=bin/gstack-review-plan, bin/gstack-gate-diff;
|
||||
// no_claim=which gate rules fire
|
||||
test('local main behind origin/main plans the same class, depth and gate-tagged count as a fresh main', () => {
|
||||
const { work, env, sh } = fixture();
|
||||
expect(sh(work, 'git merge-base main HEAD')).not.toBe(sh(work, 'git merge-base origin/main HEAD'));
|
||||
const stale = { plan: planOf(work, env), tags: gateDiff(work, env, 'main').tagged };
|
||||
sh(work, 'git branch -f main origin/main');
|
||||
const fresh = { plan: planOf(work, env), tags: gateDiff(work, env, 'main').tagged };
|
||||
expect(fresh).toEqual({ plan: { class: 'code', depth: 'light', gate_scan: expect.anything() }, tags: '' });
|
||||
expect(stale).toEqual(fresh);
|
||||
}, 240_000);
|
||||
|
||||
// Value: protects=a tag cannot stand in for the base branch, so it cannot move the merge base to HEAD and hide committed edits;
|
||||
// fails_when=resolveBase or collectLive read `origin/main` or `main` by short name while a tag of that name exists;
|
||||
// why_new=outside-voice finding on the whole diff (shadow tag reproduced); seam=bin/gstack-review-plan, bin/gstack-gate-diff
|
||||
for (const [tag, branch] of [['origin/main', 'refs/remotes/origin/main'], ['main', 'refs/heads/main']] as const) {
|
||||
test(`a tag named ${tag} at HEAD does not shadow ${branch}`, () => {
|
||||
const { work, env, sh } = fixture();
|
||||
if (branch === 'refs/heads/main') sh(work, 'git remote remove origin');
|
||||
const before = { plan: planOf(work, env), candidate: gateDiff(work, env, 'main').candidate };
|
||||
sh(work, `git tag '${tag}' HEAD`);
|
||||
const after = { plan: planOf(work, env), candidate: gateDiff(work, env, 'main').candidate };
|
||||
expect(after).toEqual(before);
|
||||
expect(after.candidate?.startsWith(sh(work, `git merge-base ${branch} HEAD`).slice(0, 12))).toBe(true);
|
||||
}, 240_000);
|
||||
}
|
||||
|
||||
// Value: protects=without a remote-tracking twin the local base still works and the fallback is named on stderr;
|
||||
// fails_when=the origin preference turns a remote-less branch into no_base, or falls back silently; why_new=the fallback is new; seam=bin/gstack-gate-diff
|
||||
test('no origin/<base>: gate-diff falls back to the local ref and says so', () => {
|
||||
const { work, env, sh } = fixture();
|
||||
sh(work, 'git branch trunk origin/main');
|
||||
const r = gateDiff(work, env, 'trunk');
|
||||
expect(r.tagged).toBe('');
|
||||
expect(r.stderr).toContain('gstack-gate-diff: no remote-tracking branch for trunk; base is the local trunk, which may lag its remote');
|
||||
}, 240_000);
|
||||
|
||||
// Value: protects=the fallback note names why the local ref stands, and a remote-less repository stays quiet;
|
||||
// fails_when=an unrelated origin/<base> is reported as missing, or a repository with no remote prints a lag note; why_new=review finding on the fallback; seam=bin/gstack-gate-diff
|
||||
test('an unrelated origin/<base> is named in the note; no remote at all uses the local base quietly', () => {
|
||||
const { work, env, sh } = fixture();
|
||||
sh(work, 'git branch -f main origin/main && git update-ref refs/remotes/origin/main "$(git commit-tree "HEAD^{tree}" -m orphan)"');
|
||||
expect(gateDiff(work, env, 'main').stderr).toContain('gstack-gate-diff: origin/main shares no history with HEAD; base is the local main, which may lag its remote');
|
||||
sh(work, 'git remote remove origin');
|
||||
const none = gateDiff(work, env, 'main');
|
||||
expect(none.tagged).toBe('');
|
||||
expect(none.stderr).not.toContain('base is');
|
||||
}, 240_000);
|
||||
});
|
||||
Reference in new issue
Block a user