diff --git a/test/dev-206-windows-gbrain.test.ts b/test/dev-206-windows-gbrain.test.ts index f44d0deab..37a093f5c 100644 --- a/test/dev-206-windows-gbrain.test.ts +++ b/test/dev-206-windows-gbrain.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; -import { dirname, join, resolve } from "path"; +import { delimiter, dirname, join, resolve } from "path"; import { spawnSync } from "child_process"; import codex from "../hosts/codex"; @@ -106,6 +106,18 @@ describe("DEV-206 Windows Codex runtime and GBrain regressions", () => { expect(plan.shell).toBe(false); }); + test("Windows source registration rejects shell-only launchers", () => { + for (const extension of ["cmd", "bat", "ps1"]) { + expect(() => + planGbrainSpawn( + ["sources", "list", "--json"], + `C:\\Program Files\\GBrain\\gbrain.${extension}`, + "win32", + ), + ).toThrow("unsafe Windows gbrain launcher"); + } + }); + test("dream capability planning refuses a brain-wide fallback", () => { const supported = planSourceScopedDream( "gstack-code-test", @@ -125,4 +137,85 @@ describe("DEV-206 Windows Codex runtime and GBrain regressions", () => { expect(brainWideOnly.error).toContain("refusing to widen scope"); } }); + + test("dream execution probes capability and never launches a brain-wide fallback", () => { + const tmp = mkdtempSync(join(tmpdir(), "gstack-dev-206-dream-")); + try { + const binDir = join(tmp, "bin"); + const gbrainHome = join(tmp, ".gbrain"); + const gstackHome = join(tmp, ".gstack"); + const logPath = join(tmp, "gbrain-calls.jsonl"); + mkdirSync(binDir, { recursive: true }); + mkdirSync(gbrainHome, { recursive: true }); + mkdirSync(gstackHome, { recursive: true }); + writeFileSync( + join(gbrainHome, "config.json"), + JSON.stringify({ engine: "pglite", database_url: "pglite:///synthetic" }), + ); + + const fakeSource = join(tmp, "fake-gbrain.ts"); + const fakeBinary = join(binDir, process.platform === "win32" ? "gbrain.exe" : "gbrain"); + writeFileSync( + fakeSource, + `import { appendFileSync } from "fs"; +const args = process.argv.slice(2); +appendFileSync(process.env.FAKE_GBRAIN_LOG!, JSON.stringify(args) + "\\n"); +if (args.join(" ") === "--version") { console.log("gbrain 0.41.0"); process.exit(0); } +if (args.join(" ") === "sources list --json") { console.log('{"sources":[]}'); process.exit(0); } +if (args.join(" ") === "dream --help") { console.log("Usage: gbrain dream"); process.exit(0); } +process.exit(91); +`, + ); + const built = spawnSync( + process.execPath, + ["build", fakeSource, "--compile", "--outfile", fakeBinary], + { encoding: "utf-8", timeout: 30_000 }, + ); + expect(built.status).toBe(0); + chmodSync(fakeBinary, 0o755); + + const childEnv: NodeJS.ProcessEnv = { ...process.env }; + for (const key of Object.keys(childEnv)) { + if (key.toUpperCase() === "PATH") delete childEnv[key]; + } + Object.assign(childEnv, { + HOME: tmp, + GBRAIN_HOME: gbrainHome, + GSTACK_HOME: gstackHome, + PATH: `${binDir}${delimiter}${process.env.PATH || ""}`, + FAKE_GBRAIN_LOG: logPath, + }); + + const result = spawnSync( + process.execPath, + [ + join(ROOT, "bin", "gstack-gbrain-sync.ts"), + "--dream", + "--no-code", + "--no-memory", + "--no-brain-sync", + ], + { + cwd: ROOT, + encoding: "utf-8", + timeout: 15_000, + env: childEnv, + }, + ); + const output = `${result.stdout || ""}${result.stderr || ""}`; + expect(result.status).toBe(1); + expect(output).toContain("does not support --source"); + expect(output).toContain("refusing to widen scope"); + + const calls = readFileSync(logPath, "utf-8") + .trim() + .split("\n") + .map((line) => JSON.parse(line) as string[]); + expect(calls).toContainEqual(["dream", "--help"]); + expect(calls).not.toContainEqual(["dream"]); + expect(calls.some((call) => call[0] === "dream" && call[1] !== "--help")).toBe(false); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + }); });