mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-12 19:44:48 +01:00
feat(capy-install): project floor, --for none, WARN for behind and Node-only pins, in-place review-voice upgrade, /cso env hygiene (harness lane F)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
5755601dac
commit
4564de2ddb
21 files changed
+495
-78
No files matched your search
@@ -129,7 +129,7 @@ describe('--for review-voice through the no-Drive brief', () => {
|
||||
const next = run(`"${root}/bin/gstack-autoplan" next --out "${path.join(host.base, 'run')}" --plan "${plan}"`, sub, env, 120_000);
|
||||
expect(next.status, next.stdout + next.stderr).toBe(3);
|
||||
const real = fs.realpathSync(root);
|
||||
const fix = `run GSTACK_CAPY_ROOT=${real} bash ${real}/capy/skill/gstack/scripts/install.sh --for autoplan (the full install: ./setup and host profiles)`;
|
||||
const fix = `run GSTACK_CAPY_ROOT=${real} bash ${real}/capy/skill/gstack/scripts/install.sh --for autoplan (upgrades this checkout in place: ./setup and host profiles, no second clone)`;
|
||||
expect(next.stderr + next.stdout).toContain('which does not cover claude that autoplan needs (review-voice certifies only the read-only helpers)');
|
||||
const bash = run([
|
||||
`. "${root}/bin/gstack-bun-version.sh"`, `. "${root}/bin/gstack-doctor-components.sh"`, `. "${root}/bin/gstack-install-verdict.sh"`,
|
||||
|
||||
@@ -139,13 +139,13 @@ describe('capy skill bootstrapper (capy/skill/gstack/scripts/install.sh)', () =>
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b);
|
||||
expect(f.git(['rev-parse', '--is-shallow-repository'])).toBe('true');
|
||||
expect(install.last).toBe('gstack: ok 1.0.1 for=autoplan');
|
||||
expect(f.calls()).toEqual([`installer --root ${f.root} --for autoplan lock=1 rev=2 min=1.0.1 from=1.0.0`]);
|
||||
expect(f.calls()).toEqual([`installer --root ${f.root} --for autoplan lock=1 rev=3 min=1.0.1 from=1.0.0`]);
|
||||
|
||||
const next = f.run(['--check', '--for', 'autoplan']);
|
||||
expect(next.status, next.stdout + next.stderr).toBe(0);
|
||||
expect(next.stdout).not.toContain('upgraded');
|
||||
expect(next.last).toBe('gstack: ok 1.0.1 for=autoplan');
|
||||
expect(f.calls().at(-1)).toBe(`installer --check --root ${f.root} --for autoplan lock= rev=2 min=1.0.1 from=`);
|
||||
expect(f.calls().at(-1)).toBe(`installer --check --root ${f.root} --for autoplan lock= rev=3 min=1.0.1 from=`);
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b);
|
||||
});
|
||||
|
||||
@@ -229,6 +229,25 @@ describe('capy skill bootstrapper (capy/skill/gstack/scripts/install.sh)', () =>
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b);
|
||||
expect(install.last).toBe('gstack: ok 1.0.1 for=autoplan');
|
||||
});
|
||||
|
||||
test('--for none: a coordinator machine gets ok not-needed (exit 0) with or without a checkout, nothing is cloned, moved or certified, and the trailer admits no workflow', () => {
|
||||
const f = fixture();
|
||||
const withCheckout = f.run(['--check', '--for', 'none']);
|
||||
expect(withCheckout.status, withCheckout.stdout + withCheckout.stderr).toBe(0);
|
||||
expect(withCheckout.last).toBe(`gstack: ok not-needed for=none (checkout 1.0.0 at ${f.root}, not certified; latest: 1.0.1; before the first workflow run: bash ${BOOT} --for <skill>)`);
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.a);
|
||||
expect(f.calls()).toEqual([]);
|
||||
// Install mode with --for none is check-only too: an old checkout below the minimum is reported, never fast-forwarded.
|
||||
const installMode = f.run(['--for', 'none']);
|
||||
expect(installMode.status).toBe(0);
|
||||
expect(installMode.last).toBe(withCheckout.last);
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.a);
|
||||
fs.rmSync(f.root, { recursive: true, force: true });
|
||||
const none = f.run(['--check', '--for', 'none']);
|
||||
expect(none.status).toBe(0);
|
||||
expect(none.last).toBe(`gstack: ok not-needed for=none (no checkout at ${f.root}; before the first workflow run: bash ${BOOT} --for <skill>)`);
|
||||
expect(fs.existsSync(f.root)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('capy skill: always current when online (the minimum is the floor, not the trigger)', () => {
|
||||
@@ -243,7 +262,7 @@ describe('capy skill: always current when online (the minimum is the floor, not
|
||||
expect(r.stdout).toMatch(/^SELF_UPGRADE: done 1\.0\.1 -> 1\.0\.2$/m);
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.c);
|
||||
expect(r.last).toBe('gstack: ok 1.0.2 for=autoplan');
|
||||
expect(f.calls()).toEqual([`installer --root ${f.root} --for autoplan lock=1 rev=2 min=1.0.1 from=1.0.1`]);
|
||||
expect(f.calls()).toEqual([`installer --root ${f.root} --for autoplan lock=1 rev=3 min=1.0.1 from=1.0.1`]);
|
||||
expect(fs.existsSync(path.join(f.root, '.git/gstack-upgrade.pending'))).toBe(false);
|
||||
|
||||
const next = f.run(['--for', 'autoplan']);
|
||||
@@ -251,7 +270,7 @@ describe('capy skill: always current when online (the minimum is the floor, not
|
||||
expect(next.stdout).toMatch(/^SELF_UPGRADE: none \(current\)$/m);
|
||||
expect(next.stdout).not.toContain('SELF_UPGRADE: done');
|
||||
expect(next.last).toBe('gstack: ok 1.0.2 for=autoplan');
|
||||
expect(f.calls().at(-1)).toBe(`installer --root ${f.root} --for autoplan lock=1 rev=2 min=1.0.1 from=`);
|
||||
expect(f.calls().at(-1)).toBe(`installer --root ${f.root} --for autoplan lock=1 rev=3 min=1.0.1 from=`);
|
||||
});
|
||||
|
||||
test('--check mode never moves a behind checkout: it names the gap and the fast-forwarding command, then certifies the installed revision', () => {
|
||||
@@ -262,7 +281,7 @@ describe('capy skill: always current when online (the minimum is the floor, not
|
||||
expect(r.stdout).not.toContain('SELF_UPGRADE');
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b);
|
||||
expect(r.last).toBe('gstack: ok 1.0.1 for=autoplan');
|
||||
expect(f.calls()).toEqual([`installer --check --root ${f.root} --for autoplan lock= rev=2 min=1.0.1 from=`]);
|
||||
expect(f.calls()).toEqual([`installer --check --root ${f.root} --for autoplan lock= rev=3 min=1.0.1 from=`]);
|
||||
});
|
||||
|
||||
test('a dirty behind checkout is skipped with gstack-upgrade\'s reason and still ends ok: above the floor, the trailer is the doctor\'s', () => {
|
||||
@@ -275,7 +294,7 @@ describe('capy skill: always current when online (the minimum is the floor, not
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b);
|
||||
expect(fs.readFileSync(path.join(f.root, 'README.md'), 'utf8')).toBe('local edit\n');
|
||||
expect(r.last).toBe('gstack: ok 1.0.1 for=autoplan');
|
||||
expect(f.calls()).toEqual([`installer --root ${f.root} --for autoplan lock=1 rev=2 min=1.0.1 from=`]);
|
||||
expect(f.calls()).toEqual([`installer --root ${f.root} --for autoplan lock=1 rev=3 min=1.0.1 from=`]);
|
||||
});
|
||||
|
||||
test('GSTACK_CAPY_SELF_UPGRADE=off and GSTACK_CAPY_AUTO_UPGRADE=0 each disable the fast-forward by name and leave HEAD where it is', () => {
|
||||
|
||||
Vendored
+2
-2
@@ -8,12 +8,12 @@
|
||||
"browse": 8943,
|
||||
"browser-skills/hackernews-frontpage": 371,
|
||||
"canary": 13441,
|
||||
"capy/skill/gstack": 3498,
|
||||
"capy/skill/gstack": 4075,
|
||||
"careful": 1239,
|
||||
"codex": 15594,
|
||||
"context-restore": 10909,
|
||||
"context-save": 10517,
|
||||
"cso": 4660,
|
||||
"cso": 4924,
|
||||
"design-consultation": 16188,
|
||||
"design-html": 14380,
|
||||
"design-review": 35808,
|
||||
|
||||
@@ -58,7 +58,7 @@ function fixture(opts: { bun?: string; jq?: boolean } = {}) {
|
||||
script(path.join(root, 'setup'), ['echo "setup $*" >> "$CAPY_LOG"', 'echo "gstack ready ($2)"', 'echo "env GSTACK_SKIP_PLAYWRIGHT=${GSTACK_SKIP_PLAYWRIGHT:-} GSTACK_SKIP_FONTS=${GSTACK_SKIP_FONTS:-} bun=$(bun --version)" >> "$CAPY_LOG"', 'exit "${STUB_SETUP_EXIT:-0}"']);
|
||||
script(path.join(root, 'bin/gstack-doctor'), [
|
||||
'echo "doctor $* GSTACK_EPHEMERAL=${GSTACK_EPHEMERAL:-}" >> "$CAPY_LOG"', 'echo "doctor-cwd $(pwd -P)" >> "$CAPY_LOG"', 'echo "doctor-node $(node --version 2>/dev/null)" >> "$CAPY_LOG"',
|
||||
'echo "required: claude codex patch"', 'echo "PASS claude ok"', 'echo "SKIP browser (lazy)"', '[ -n "${STUB_PINS_SKIP:-}" ] && echo "SKIP pins ($STUB_PINS_SKIP)"',
|
||||
'echo "required: claude codex patch"', 'echo "PASS claude ok"', 'echo "SKIP browser (lazy)"', '[ -n "${STUB_PINS_SKIP:-}" ] && echo "SKIP pins ($STUB_PINS_SKIP)"', '[ -n "${STUB_DOCTOR_EXTRA:-}" ] && echo "$STUB_DOCTOR_EXTRA"',
|
||||
'[ "${STUB_DOCTOR_FAIL:-}" = 1 ] && { echo "FAIL codex — missing; fix: x (COMPONENT_MISSING; d)"; echo "gstack: fail codex"; exit 1; }',
|
||||
'[ -n "${GSTACK_DOCTOR_RUNTIME_DRIFT:-}" ] && { echo "FAIL runtime — bun resolution drifted: $GSTACK_DOCTOR_RUNTIME_DRIFT; fix: $GSTACK_DOCTOR_RUNTIME_DRIFT_FIX (BUN_RESOLUTION_DRIFT; d)"; echo "gstack: fail runtime"; exit 1; }',
|
||||
'echo "gstack: ok $(cat "$(dirname "$0")/../VERSION") for=autoplan"',
|
||||
@@ -282,17 +282,64 @@ describe('gstack-capy-install', () => {
|
||||
expect(lastLine(run(f, [], { ...behind, STUB_DOCTOR_FAIL: '1' }).stdout)).toBe('gstack: fail codex');
|
||||
});
|
||||
|
||||
test('a skipped session-start upgrade is never ok: the trailer is `fail behind` with the reason, the fix and the code, exit 1, and the verdict records the failure', () => {
|
||||
test('a skipped session-start upgrade above the minimum is a WARN, never a fail: the trailer stays ok with behind=, the skip reason and the fix, exit 0, and a WARN behind row names the code', () => {
|
||||
const f = fixture();
|
||||
const busy = { GSTACK_CAPY_BEHIND: '3', GSTACK_CAPY_BEHIND_TO: '1.2.6', GSTACK_CAPY_BEHIND_FIX: `${f.root}/bin/gstack-upgrade`, GSTACK_CAPY_BEHIND_BLOCKED: 'live gstack sessions hold this installation: pid 379 (124m dbus-daemon); fix: wait for those sessions to finish, or re-run with GSTACK_UPGRADE_FORCE=1 when you know they are idle (UPGRADE_BUSY)' };
|
||||
const r = run(f, [], busy);
|
||||
expect(r.status, r.stdout + r.stderr).toBe(1);
|
||||
expect(lastLine(r.stdout)).toBe('gstack: fail behind — installed 1.2.3 is 3 commits behind origin (latest 1.2.6) and the session-start upgrade was skipped: live gstack sessions hold this installation: pid 379 (124m dbus-daemon); fix: wait for those sessions to finish, or re-run with GSTACK_UPGRADE_FORCE=1 when you know they are idle (UPGRADE_BUSY)');
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(lastLine(r.stdout)).toBe('gstack: ok 1.2.3 for=autoplan (behind=3 (latest: 1.2.6); upgrade skipped: live gstack sessions hold this installation: pid 379 (124m dbus-daemon) (UPGRADE_BUSY); fix: wait for those sessions to finish, or re-run with GSTACK_UPGRADE_FORCE=1 when you know they are idle)');
|
||||
expect(r.stdout).toMatch(/^WARN behind — installed 1\.2\.3 is 3 commits behind origin \(latest 1\.2\.6\) and the session-start upgrade was skipped: live gstack sessions hold this installation: pid 379 \(124m dbus-daemon\); fix: wait for those sessions to finish, or re-run with GSTACK_UPGRADE_FORCE=1 when you know they are idle \(UPGRADE_BUSY; .*#upgrade-busy\)$/m);
|
||||
expect(r.stdout).toMatch(/^GSTACK_LAUNCH_FILE: /m);
|
||||
const bare = run(f, [], { ...busy, GSTACK_CAPY_BEHIND_BLOCKED: 'gstack-upgrade exit 9' });
|
||||
expect(lastLine(bare.stdout)).toBe(`gstack: fail behind — installed 1.2.3 is 3 commits behind origin (latest 1.2.6) and the session-start upgrade was skipped: gstack-upgrade exit 9; fix: ${f.root}/bin/gstack-upgrade (UPGRADE_REFUSED)`);
|
||||
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)`);
|
||||
expect(lastLine(run(f, [], { ...busy, STUB_DOCTOR_FAIL: '1' }).stdout)).toBe('gstack: fail codex');
|
||||
});
|
||||
|
||||
test('a WARN floor row from the doctor adds floor=<v> (<file>) and the fast-forwarding fix to an ok trailer; behind keeps one fix note', () => {
|
||||
const f = fixture();
|
||||
const row = 'WARN floor — installed 1.2.3 is below the floor 1.2.9 that /p/.gstack/ship-policy.json declares (min_gstack_version); fix: bash /s/install.sh --for autoplan (fast-forwards a clean checkout to origin and reinstalls), or /r/bin/gstack-upgrade (PROJECT_FLOOR_UNMET; d)';
|
||||
const alone = run(f, ['--for', 'autoplan'], { STUB_DOCTOR_EXTRA: row, GSTACK_CAPY_WRAPPER_PATH: '/s/install.sh' });
|
||||
expect(alone.status, alone.stdout + alone.stderr).toBe(0);
|
||||
expect(lastLine(alone.stdout)).toBe('gstack: ok 1.2.3 for=autoplan (floor=1.2.9 (/p/.gstack/ship-policy.json); fix: bash /s/install.sh --for autoplan (fast-forwards))');
|
||||
const behind = run(f, [], { STUB_DOCTOR_EXTRA: row, GSTACK_CAPY_BEHIND: '5', GSTACK_CAPY_BEHIND_TO: '1.2.9', GSTACK_CAPY_BEHIND_FIX: 'bash /s/install.sh --for autoplan' });
|
||||
expect(lastLine(behind.stdout)).toBe('gstack: ok 1.2.3 for=autoplan (behind=5 (latest: 1.2.9); floor=1.2.9 (/p/.gstack/ship-policy.json); fix: bash /s/install.sh --for autoplan)');
|
||||
expect(lastLine(run(f, [], { STUB_DOCTOR_EXTRA: row, STUB_DOCTOR_FAIL: '1' }).stdout)).toBe('gstack: fail codex');
|
||||
});
|
||||
|
||||
test('--for none certifies nothing and installs nothing: ok not-needed, exit 0, no setup, no verdict, with or without a checkout', () => {
|
||||
const f = fixture();
|
||||
const r = run(f, ['--for', 'none']);
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(lastLine(r.stdout)).toBe(`gstack: ok not-needed for=none (checkout 1.2.3 at ${f.root}, not certified; before the first workflow run: test -f "${f.root}/setup" || git clone --quiet --single-branch --depth 1 https://github.com/garrytan/gstack.git "${f.root}" && GSTACK_CAPY_ROOT="${f.root}" bash "${f.root}/capy/skill/gstack/scripts/install.sh" --for <skill>)`);
|
||||
expect(f.calls()).toEqual([]);
|
||||
expect(fs.existsSync(path.join(f.home, '.gstack-state'))).toBe(false);
|
||||
const missing = path.join(f.base, 'absent');
|
||||
const none = run(f, ['--check', '--for', 'none', '--root', missing], { GSTACK_CAPY_WRAPPER_PATH: '/s/install.sh', GSTACK_CAPY_BEHIND: '2', GSTACK_CAPY_BEHIND_TO: '1.2.5' });
|
||||
expect(none.status).toBe(0);
|
||||
expect(lastLine(none.stdout)).toBe(`gstack: ok not-needed for=none (no checkout at ${missing}; behind=2 (latest: 1.2.5); before the first workflow run: bash /s/install.sh --for <skill>)`);
|
||||
expect(fs.existsSync(missing)).toBe(false);
|
||||
});
|
||||
|
||||
test('a review-voice verdict for this HEAD and project is upgraded in place by --for <skill>: the installer says so, runs setup, and the verdict becomes the workflow\'s', () => {
|
||||
const f = fixture();
|
||||
const project = path.join(f.base, 'proj');
|
||||
fs.mkdirSync(project, { recursive: true });
|
||||
sh('git init -q', project);
|
||||
const voice = run(f, ['--for', 'review-voice', '--project', project]);
|
||||
expect(voice.status, voice.stdout + voice.stderr).toBe(0);
|
||||
expect(voice.stdout).toContain('review-voice: no ./setup and no host profiles');
|
||||
expect(f.calls().some(l => l.startsWith('setup '))).toBe(false);
|
||||
const full = run(f, ['--for', 'autoplan', '--project', project]);
|
||||
expect(full.status, full.stdout + full.stderr).toBe(0);
|
||||
expect(full.stdout).toContain(`gstack-capy-install: upgrading the review-voice install in place to --for autoplan (same checkout at ${f.root}; the project's pins apply, ./setup and both host profiles run now; no second clone)`);
|
||||
expect(full.stdout).toContain(`gstack-capy-install: reusing ${f.root} (1.2.3); never reset or pulled`);
|
||||
expect(f.calls().filter(l => l.startsWith('setup ')).length).toBe(2);
|
||||
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');
|
||||
});
|
||||
|
||||
test('--root clones when the directory is absent and refuses a non-gstack directory', () => {
|
||||
const f = fixture();
|
||||
const clone = path.join(f.base, 'clone');
|
||||
|
||||
+51
-18
@@ -461,8 +461,8 @@ describe('gstack-doctor --check', () => {
|
||||
const row = (f: Fixture, args: string[], extra: Record<string, string> = {}) => {
|
||||
const r = doctor(f, ['--check', ...args], extra);
|
||||
const lines = r.out.split('\n');
|
||||
const rows = Object.fromEntries(lines.filter(l => /^(PASS|FAIL|SKIP) /.test(l)).map(l => {
|
||||
const m = l.match(/^(PASS|FAIL|SKIP) ([a-z-]+)(?:\s(.*))?$/)!;
|
||||
const rows = Object.fromEntries(lines.filter(l => /^(PASS|FAIL|SKIP|WARN) /.test(l)).map(l => {
|
||||
const m = l.match(/^(PASS|FAIL|SKIP|WARN) ([a-z-]+)(?:\s(.*))?$/)!;
|
||||
return [m[2], { status: m[1], rest: m[3] ?? '' }];
|
||||
}));
|
||||
const trailer = lines.filter(l => l.startsWith('gstack: ')).at(-1) ?? '';
|
||||
@@ -488,10 +488,11 @@ describe('gstack-doctor --check', () => {
|
||||
installCodex(f);
|
||||
const r = row(f, []);
|
||||
expect(r.status, r.out).toBe(0);
|
||||
expect(r.required).toEqual(['claude', 'codex', 'runtime', 'pins', 'state-root', 'privacy', 'cores', 'revision', 'wrapper']);
|
||||
for (const id of r.required.filter(x => x !== 'pins' && x !== 'wrapper')) expect(r.rows[id]?.status, `${id}\n${r.out}`).toBe('PASS');
|
||||
// The work dir is not a git repository, so there is no project to read pins from: SKIP with the reason, never silent.
|
||||
expect(r.required).toEqual(['claude', 'codex', 'runtime', 'pins', 'state-root', 'privacy', 'cores', 'revision', 'floor', 'wrapper']);
|
||||
for (const id of r.required.filter(x => x !== 'pins' && x !== 'floor' && x !== 'wrapper')) expect(r.rows[id]?.status, `${id}\n${r.out}`).toBe('PASS');
|
||||
// The work dir is not a git repository, so there is no project to read pins or a floor from: SKIP with the reason, never silent.
|
||||
expect(r.rows.pins).toEqual({ status: 'SKIP', rest: expect.stringContaining('no project') });
|
||||
expect(r.rows.floor).toEqual({ status: 'SKIP', rest: expect.stringContaining('no project to read .gstack/ship-policy.json from') });
|
||||
// No ~/.capy/drive skill copy and no active wrapper: not a Capy machine.
|
||||
expect(r.rows.wrapper).toEqual({ status: 'SKIP', rest: expect.stringContaining('not a Capy machine') });
|
||||
expect(r.rows.patch.status).toBe('PASS');
|
||||
@@ -512,7 +513,7 @@ describe('gstack-doctor --check', () => {
|
||||
fs.rmSync(path.join(f.stub, 'codex'));
|
||||
const r = row(f, ['--for', 'autoplan']);
|
||||
expect(r.status, r.out).toBe(0);
|
||||
expect(r.required).toEqual(['claude', 'codex', 'patch', 'runtime', 'pins', 'state-root', 'privacy', 'cores', 'revision', 'wrapper']);
|
||||
expect(r.required).toEqual(['claude', 'codex', 'patch', 'runtime', 'pins', 'state-root', 'privacy', 'cores', 'revision', 'floor', 'wrapper']);
|
||||
expect(r.rows.patch).toEqual({ status: 'PASS', rest: 'gpt-6-astra (gstack-review/SKILL.md)' });
|
||||
expect(r.rows['codex-cli'].status).toBe('SKIP');
|
||||
expect(r.rows['codex-cli'].rest).toContain('optional');
|
||||
@@ -639,21 +640,53 @@ describe('gstack-doctor --check', () => {
|
||||
expect(plain.rows.pins.rest).not.toContain(wrapper);
|
||||
});
|
||||
|
||||
test('project pins: a Node pin mismatch names the node it read and a fix that installs the pinned Node', () => {
|
||||
test('project pins: a Node-only pin mismatch is a WARN (the trailer stays ok) that names the node it read and a fix that installs the pinned Node', () => {
|
||||
const f = makeFixture();
|
||||
const project = path.join(f.base, 'project');
|
||||
write(path.join(project, '.github/workflows/native-locks.yml'), 'jobs:\n locks:\n steps:\n - uses: actions/setup-node@v4\n with:\n node-version: 24.18.0\n');
|
||||
script(path.join(f.stub, 'node'), ['echo v24.21.0']);
|
||||
const wrapper = '/drive/skills/gstack/scripts/install.sh';
|
||||
const capy = row(f, ['--project', project, '--for', 'review'], { GSTACK_CAPY_WRAPPER_PATH: wrapper });
|
||||
expect(capy.rows.pins.status).toBe('FAIL');
|
||||
expect(capy.rows.pins.rest).toContain(`node 24.21.0 outside CI 24.18.0 (.github/workflows/native-locks.yml:6); node read from ${bashPath(f.stub)}/node`);
|
||||
expect(capy.rows.pins.rest).toContain(`fix: bash ${wrapper} --for review (installs the project's pinned Node 24.18.0 into gstack's runtime store and launches it through the GSTACK_LAUNCH PATH), or change the pin in ${project}`);
|
||||
expect(capy.rows.pins.rest).toContain('(PROJECT_PIN_MISMATCH; ');
|
||||
const plain = row(f, ['--project', project, '--for', 'review']);
|
||||
const capy = row(f, ['--project', project, '--for', 'health'], { GSTACK_CAPY_WRAPPER_PATH: wrapper });
|
||||
expect(capy.rows.pins.status).toBe('WARN');
|
||||
expect(capy.rows.pins.rest).toContain(`node 24.21.0 outside CI 24.18.0 (.github/workflows/native-locks.yml:6); node read from ${bashPath(f.stub)}/node (node pin only; bun pins pass)`);
|
||||
expect(capy.rows.pins.rest).toContain(`fix: bash ${wrapper} --for health (installs the project's pinned Node 24.18.0 into gstack's runtime store and launches it through the GSTACK_LAUNCH PATH), or change the pin in ${project}`);
|
||||
expect(capy.rows.pins.rest).toContain('(PROJECT_NODE_PIN_UNMET; ');
|
||||
expect(capy.trailer).toBe(`gstack: ok ${VERSION} for=health`);
|
||||
const plain = row(f, ['--project', project, '--for', 'health']);
|
||||
expect(plain.rows.pins.rest).toContain(`fix: nvm install 24.18.0 and put its bin directory first on PATH`);
|
||||
// A Bun pin that also fails keeps the FAIL and its code: only a Node-only gap is advisory.
|
||||
write(path.join(project, 'package.json'), '{\n "engines": {\n "bun": ">=1.4.2"\n }\n}\n');
|
||||
const both = row(f, ['--project', project, '--for', 'health'], { STUB_BUN_VERSION: '1.3.14' });
|
||||
expect(both.rows.pins.status).toBe('FAIL');
|
||||
expect(both.rows.pins.rest).toContain('(PROJECT_PIN_MISMATCH; ');
|
||||
fs.rmSync(path.join(project, 'package.json'));
|
||||
script(path.join(f.stub, 'node'), ['echo v24.18.0']);
|
||||
expect(row(f, ['--project', project, '--for', 'review']).rows.pins.status).toBe('PASS');
|
||||
expect(row(f, ['--project', project, '--for', 'health']).rows.pins.status).toBe('PASS');
|
||||
});
|
||||
|
||||
test('floor: min_gstack_version in the project\'s .gstack/ship-policy.json PASSes at or below the installed version, WARNs above it with the upgrade as the fix, and SKIPs when absent or null', () => {
|
||||
const f = makeFixture();
|
||||
const project = path.join(f.base, 'project');
|
||||
fs.mkdirSync(project, { recursive: true });
|
||||
const none = row(f, ['--project', project]);
|
||||
expect(none.rows.floor).toEqual({ status: 'SKIP', rest: `(${project} declares none (.gstack/ship-policy.json absent; set min_gstack_version there to require a gstack version))` });
|
||||
write(path.join(project, '.gstack/ship-policy.json'), '{\n "bump": "patch",\n "min_gstack_version": null\n}\n');
|
||||
expect(row(f, ['--project', project]).rows.floor).toEqual({ status: 'SKIP', rest: `(${project}/.gstack/ship-policy.json sets no min_gstack_version (null or absent))` });
|
||||
write(path.join(project, '.gstack/ship-policy.json'), `{\n "min_gstack_version": "${VERSION}"\n}\n`);
|
||||
const met = row(f, ['--project', project]);
|
||||
expect(met.rows.floor).toEqual({ status: 'PASS', rest: `${VERSION} declared by ${project}/.gstack/ship-policy.json (installed ${VERSION})` });
|
||||
write(path.join(project, '.gstack/ship-policy.json'), '{\n "min_gstack_version": "999.0.0.0"\n}\n');
|
||||
const wrapper = '/drive/skills/gstack/scripts/install.sh';
|
||||
const unmet = row(f, ['--project', project, '--for', 'health'], { GSTACK_CAPY_WRAPPER_PATH: wrapper });
|
||||
expect(unmet.status, unmet.out).toBe(0);
|
||||
expect(unmet.rows.floor.status).toBe('WARN');
|
||||
expect(unmet.rows.floor.rest).toContain(`installed ${VERSION} is below the floor 999.0.0.0 that ${project}/.gstack/ship-policy.json declares (min_gstack_version); fix: bash ${wrapper} --for health (fast-forwards a clean checkout to origin and reinstalls), or `);
|
||||
expect(unmet.rows.floor.rest).toContain('(PROJECT_FLOOR_UNMET; ');
|
||||
expect(unmet.trailer).toBe(`gstack: ok ${VERSION} for=health`);
|
||||
const plain = row(f, ['--project', project]);
|
||||
expect(plain.rows.floor.rest).toContain('fix: ');
|
||||
expect(plain.rows.floor.rest).toContain('bin/gstack-upgrade (fast-forwards to the remote default branch');
|
||||
expect(plain.rows.floor.rest).not.toContain(wrapper);
|
||||
});
|
||||
|
||||
test('--revision: a matching VERSION passes, an unmet revision is never ok', () => {
|
||||
@@ -728,8 +761,8 @@ describe('gstack-doctor --check', () => {
|
||||
expect(json.for).toBe('autoplan');
|
||||
expect(json.ok).toBe(false);
|
||||
expect(json.failed).toEqual(['runtime']);
|
||||
expect(json.required).toEqual(['claude', 'codex', 'patch', 'runtime', 'pins', 'state-root', 'privacy', 'cores', 'revision', 'wrapper']);
|
||||
expect(json.rows.map((x: { id: string }) => x.id)).toEqual(['claude', 'codex', 'patch', 'codex-cli', 'browse-bundle', 'browser', 'cso', 'helpers', 'runtime', 'pins', 'state-root', 'privacy', 'cores', 'revision', 'wrapper']);
|
||||
expect(json.required).toEqual(['claude', 'codex', 'patch', 'runtime', 'pins', 'state-root', 'privacy', 'cores', 'revision', 'floor', 'wrapper']);
|
||||
expect(json.rows.map((x: { id: string }) => x.id)).toEqual(['claude', 'codex', 'patch', 'codex-cli', 'browse-bundle', 'browser', 'cso', 'helpers', 'runtime', 'pins', 'state-root', 'privacy', 'cores', 'revision', 'floor', 'wrapper']);
|
||||
const runtime = json.rows.find((x: { id: string }) => x.id === 'runtime');
|
||||
expect(runtime.status).toBe('FAIL');
|
||||
expect(runtime.code).toBe('RUNTIME_BELOW_MINIMUM');
|
||||
@@ -760,8 +793,8 @@ describe('gstack-doctor --check: launch Bun, supported minimum, planning-only pi
|
||||
const check = (f: Fixture, args: string[], extra: Record<string, string> = {}) => {
|
||||
const r = doctor(f, ['--check', ...args], extra);
|
||||
const lines = r.out.split('\n');
|
||||
const rows = Object.fromEntries(lines.filter(l => /^(PASS|FAIL|SKIP) /.test(l)).map(l => {
|
||||
const m = l.match(/^(PASS|FAIL|SKIP) ([a-z-]+)(?:\s(.*))?$/)!;
|
||||
const rows = Object.fromEntries(lines.filter(l => /^(PASS|FAIL|SKIP|WARN) /.test(l)).map(l => {
|
||||
const m = l.match(/^(PASS|FAIL|SKIP|WARN) ([a-z-]+)(?:\s(.*))?$/)!;
|
||||
return [m[2], { status: m[1], rest: m[3] ?? '' }];
|
||||
}));
|
||||
return { status: r.status, out: r.out, rows, trailer: lines.filter(l => l.startsWith('gstack: ')).at(-1) ?? '' };
|
||||
|
||||
@@ -449,7 +449,7 @@ do not launch the downstream skill or open a browser.`,
|
||||
// The full-audit E2E asserts actual section loading alongside report/proof behavior.
|
||||
behavioral: 'external',
|
||||
externalTest: 'test/skill-e2e-cso.test.ts',
|
||||
maxSkeletonBytes: 18_100, // + Oct 10 multi-agent wave PR D2 (sideways review): Phase 12's root-cause-variants sentence becomes the sibling sweep (severity follows the worst sibling), two neighbouring phrases tightened to offset most of it; measured skeleton 18,044 (2026-10-10).
|
||||
maxSkeletonBytes: 18_900, // + Oct 10 multi-agent wave PR D2 (sideways review): Phase 12's root-cause-variants sentence becomes the sibling sweep (severity follows the worst sibling), two neighbouring phrases tightened to offset most of it; measured skeleton 18,044 (2026-10-10). + Oct 26 harness lane F (TRUT 5/6, FOUN 13): the environment-hygiene paragraph (set/unset checks only; no env, printenv, set -x, bare set/export, export $(...) in a fence) and the tooling-error paragraph (bounded retries, then not assessed with the command, exit code and stderr tail; never a quiet partial), both in the always-loaded startup block because they govern every helper call; measured skeleton 18,843 (2026-10-26).
|
||||
minUnionBytes: 30_000, // v3 deliberately removes the shared export/startup preamble.
|
||||
mustContain: ['OWASP', 'STRIDE', 'daily', 'comprehensive', 'verif'],
|
||||
// Existing baseline comparison remains an upper bound; absolute limits above
|
||||
|
||||
Reference in new issue
Block a user