fix: pre-landing review fixes

Red team (/ship Step 9): a Drive copy at a bootstrapper revision below GSTACK_CAPY_MIN_WRAPPER is a WARN with the owner action in a direct (no-provenance) run too; only a pre-bootstrapper copy fails. A --for none session never self-upgrades. Stale fail-behind comment in gstack-capy-session; SKILL.md says ok not-needed is not an install verdict. Tests: UPGRADE_SKIPPED collapse, silent in-place message, direct-run WARN, wrapper-2 --for none handover.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-26 16:05:39 +00:00
1 parent 6a0f9e47bb
commit fc69d3ff0b
10 files changed
+72 -14

No files matched your search

+12
View File
@@ -393,6 +393,18 @@ describe('capy skill: the session hands the installer GSTACK_CAPY_BEHIND', () =>
}
});
test('an older bootstrapper handing --for none to the session in install mode: the session certifies nothing and moves nothing (no self-upgrade, HEAD unchanged), and reports behind like --check', () => {
const f = fixture({ cloneAt: 'B' });
write(path.join(f.root, 'bin/gstack-capy-install'), ENV_INSTALLER, 0o755);
f.git(['update-index', '--assume-unchanged', 'bin/gstack-capy-install']);
f.git(['fetch', '-q', 'origin', 'main']);
const r = spawnSync('bash', [path.join(f.root, 'bin/gstack-capy-session'), '--root', f.root, '--for', 'none'], { cwd: f.host.home, encoding: 'utf8', env: { ...f.host.env, GSTACK_CAPY_REMOTE_MIN: '1.0.1', GSTACK_CAPY_WRAPPER_REV: '2', GSTACK_CAPY_WRAPPER_PATH: BOOT }, timeout: 120_000 });
expect(r.status, r.stdout + r.stderr).toBe(0);
expect(r.stdout).not.toMatch(/^SELF_UPGRADE:/m);
expect(r.stdout.trim().split('\n').at(-1)).toBe(`gstack: ok 1.0.1 for=autoplan behind=1 to=1.0.2 fix=bash ${BOOT} --for none`);
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b);
});
test('current, or the fetch failed: nothing is exported, and values inherited from a parent session are dropped', () => {
const f = fixture({ cloneAt: 'B' });
f.git(['fetch', '-q', 'origin', 'main']);
+10
View File
@@ -293,6 +293,11 @@ describe('gstack-capy-install', () => {
const bare = run(f, [], { ...busy, GSTACK_CAPY_BEHIND_BLOCKED: 'gstack-upgrade exit 9' });
expect(bare.status).toBe(0);
expect(lastLine(bare.stdout)).toBe(`gstack: ok 1.2.3 for=autoplan (behind=3 (latest: 1.2.6); upgrade skipped: gstack-upgrade exit 9 (UPGRADE_REFUSED); fix: ${f.root}/bin/gstack-upgrade)`);
// A code the installer does not know collapses to UPGRADE_SKIPPED, whose anchor exists; the reason and fix are kept.
const other = run(f, [], { ...busy, GSTACK_CAPY_BEHIND_BLOCKED: 'origin unreachable; fix: retry when online (UPGRADE_FETCH_FAILED)' });
expect(other.status).toBe(0);
expect(lastLine(other.stdout)).toBe('gstack: ok 1.2.3 for=autoplan (behind=3 (latest: 1.2.6); upgrade skipped: origin unreachable (UPGRADE_SKIPPED); fix: retry when online)');
expect(other.stdout).toMatch(/^WARN behind — .*; fix: retry when online \(UPGRADE_SKIPPED; .*#upgrade-skipped\)$/m);
expect(lastLine(run(f, [], { ...busy, STUB_DOCTOR_FAIL: '1' }).stdout)).toBe('gstack: fail codex');
});
@@ -338,6 +343,11 @@ describe('gstack-capy-install', () => {
const verdict = full.stdout.match(/GSTACK_INSTALL_VERDICT=(\S+)/)![1]!;
expect(JSON.parse(fs.readFileSync(verdict, 'utf8'))).toMatchObject({ for: 'autoplan', ok: true });
expect(lastLine(full.stdout)).toBe('gstack: ok 1.2.3 for=autoplan');
// The message is for the review-voice -> workflow step only: a rerun of the same profile, a --check and a second review-voice install stay silent.
const upgrading = /upgrading the review-voice install in place/;
expect(run(f, ['--for', 'autoplan', '--project', project]).stdout).not.toMatch(upgrading);
expect(run(f, ['--check', '--for', 'autoplan', '--project', project]).stdout).not.toMatch(upgrading);
expect(run(f, ['--for', 'review-voice', '--project', project]).stdout).not.toMatch(upgrading);
});
test('--root clones when the directory is absent and refuses a non-gstack directory', () => {
+16
View File
@@ -885,6 +885,22 @@ describe('gstack-doctor --check: wrapper and runtime drift', () => {
expect(fresh.status, fresh.out).toBe(0);
});
posixPathTest('direct doctor run: a bootstrapper Drive copy below the minimum still works, so it is an owner-action WARN with an ok trailer (only a pre-bootstrapper copy fails)', () => {
const f = makeFixture();
withSupport(f, Number(currentRev) + 1);
const old = drive(f, 'user-garry-tan', current);
const r = parse(f, []);
expect(r.status, r.out).toBe(0);
expect(r.trailer).toMatch(/^gstack: ok /);
expect(r.line('wrapper')).toBe(`WARN wrapper — every Capy skill copy is below the minimum wrapper ${Number(currentRev) + 1} (it still works): ${old} (wrapper ${currentRev}); owner action: copy ${f.root}/capy/skill/gstack/ over the Drive skill folder, or have Capy re-sync it; this recurs every session until the Drive copy changes (CAPY_WRAPPER_STALE; https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#capy-wrapper-stale)`);
const json = JSON.parse(doctor(f, ['--check', '--json']).out);
expect(json.ok).toBe(true);
expect(json.rows.find((x: { id: string }) => x.id === 'wrapper')).toMatchObject({ status: 'WARN', code: 'CAPY_WRAPPER_STALE' });
// A pre-bootstrapper copy beside it keeps the FAIL: that copy cannot hand over at all.
drive(f, 'org', PRE_BOOTSTRAPPER);
expect(parse(f, []).trailer).toBe('gstack: fail wrapper');
});
posixPathTest('two Drive copies, one current: PASS naming the stale one; GSTACK_CAPY_WRAPPER_PATH alone decides when set without a revision', () => {
const f = makeFixture();
withSupport(f);