diff --git a/test/dev-setup-render-isolation.test.ts b/test/dev-setup-render-isolation.test.ts index d6c815b71..fbfeb790c 100644 --- a/test/dev-setup-render-isolation.test.ts +++ b/test/dev-setup-render-isolation.test.ts @@ -75,3 +75,17 @@ describe('.gitignore: render dir is declared untracked', () => { expect(read('.gitignore')).toContain('.claude/gstack-rendered/'); }); }); + +describe('dev-skill: refreshes the render on template change', () => { + const devSkill = read('scripts/dev-skill.ts'); + + test('re-renders the :user variant into the workspace render dir', () => { + expect(devSkill).toContain('gstack-rendered'); + expect(devSkill).toContain('--out-dir'); + expect(devSkill).toContain('--respect-detection'); + }); + + test('only refreshes when the render dir already exists (never creates it during plain dev)', () => { + expect(devSkill).toContain('fs.existsSync(RENDER_DIR)'); + }); +}); diff --git a/test/gbrain-detect-shape.test.ts b/test/gbrain-detect-shape.test.ts index 465e55623..e2f67ee07 100644 --- a/test/gbrain-detect-shape.test.ts +++ b/test/gbrain-detect-shape.test.ts @@ -16,7 +16,7 @@ */ import { describe, it, expect } from "bun:test"; -import { execFileSync } from "child_process"; +import { execFileSync, spawnSync } from "child_process"; import { mkdtempSync, mkdirSync, @@ -47,6 +47,16 @@ function runDetect(env: Partial): string { }); } +/** Run detect with --is-ok and return its exit code (never throws). */ +function runIsOk(env: Partial): number { + const r = spawnSync(BUN_BIN, ["run", DETECT_BIN, "--is-ok"], { + timeout: 15_000, + stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, ...env }, + }); + return r.status ?? 1; +} + interface DetectShape { gbrain_on_path: boolean; gbrain_version: string | null; @@ -244,3 +254,66 @@ exit 0 } }); }); + +describe("bin/gstack-gbrain-detect --is-ok — live gate", () => { + it("exits non-zero when gbrain is not on PATH (no-cli)", () => { + const tmp = mkdtempSync(join(tmpdir(), "detect-isok-")); + try { + const code = runIsOk({ + HOME: tmp, + PATH: "/usr/bin:/bin", // no gbrain + GSTACK_HOME: tmp, + GSTACK_DETECT_NO_CACHE: "1", + }); + expect(code).not.toBe(0); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + }); + + it("exits 0 when a fake gbrain reports a healthy engine (ok)", () => { + const tmp = mkdtempSync(join(tmpdir(), "detect-isok-")); + const bindir = join(tmp, "bin"); + const home = join(tmp, "home"); + const configDir = join(home, ".gbrain"); + try { + mkdirSync(bindir, { recursive: true }); + mkdirSync(configDir, { recursive: true }); + writeFileSync(join(configDir, "config.json"), JSON.stringify({ engine: "pglite" })); + const fake = `#!/bin/sh +case "$1 $2" in + "--version ") echo "gbrain 0.33.1.0"; exit 0 ;; + "sources list") echo '{"sources":[]}'; exit 0 ;; + "doctor "*) echo '{"status":"ok","checks":[]}'; exit 0 ;; +esac +exit 0 +`; + const gbrainPath = join(bindir, "gbrain"); + writeFileSync(gbrainPath, fake); + chmodSync(gbrainPath, 0o755); + + const code = runIsOk({ + HOME: home, + PATH: `${bindir}:/usr/bin:/bin`, + GSTACK_HOME: tmp, + GSTACK_DETECT_NO_CACHE: "1", + }); + expect(code).toBe(0); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + }); + + it("exit code agrees with the JSON gbrain_local_status (no skew)", () => { + // Run both surfaces against the same env and assert they never disagree. + const tmp = mkdtempSync(join(tmpdir(), "detect-isok-")); + try { + const env = { HOME: tmp, PATH: "/usr/bin:/bin", GSTACK_HOME: tmp, GSTACK_DETECT_NO_CACHE: "1" }; + const status = (JSON.parse(runDetect(env)) as DetectShape).gbrain_local_status; + const code = runIsOk(env); + expect(code === 0).toBe(status === "ok"); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + }); +});