/** * Unit tests for `buildGbrainEnv` in lib/gbrain-exec.ts. * * The helper is the single source of truth for "what DATABASE_URL does * gbrain see when spawned from gstack." The bug it prevents: gbrain's * dotenv autoload pulls a host project's `.env.local` `DATABASE_URL` * instead of gbrain's own `~/.gbrain/config.json`. Every helper test * asserts on the **effective value** of the returned env, never object * identity — Codex review #11 flagged that returning the same mutable * object can leak later mutation. */ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { spawnSync } from "child_process"; import { buildGbrainEnv, isTransactionModePooler } from "../lib/gbrain-exec"; describe("buildGbrainEnv", () => { let home: string; let gbrainHome: string; beforeEach(() => { home = mkdtempSync(join(tmpdir(), "gstack-build-env-")); gbrainHome = join(home, ".gbrain"); mkdirSync(gbrainHome, { recursive: true }); }); afterEach(() => { rmSync(home, { recursive: true, force: true }); }); it("seeds DATABASE_URL from ~/.gbrain/config.json when caller env has no DATABASE_URL", () => { writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: "postgresql://gbrain/db" })); const baseEnv = { HOME: home }; const result = buildGbrainEnv({ baseEnv }); expect(result.DATABASE_URL).toBe("postgresql://gbrain/db"); }); it("overrides caller's DATABASE_URL when config differs", () => { writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: "postgresql://gbrain/db" })); const baseEnv = { HOME: home, DATABASE_URL: "postgresql://app-local/wrong" }; const result = buildGbrainEnv({ baseEnv }); expect(result.DATABASE_URL).toBe("postgresql://gbrain/db"); }); it("leaves DATABASE_URL untouched when GSTACK_RESPECT_ENV_DATABASE_URL=1", () => { writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: "postgresql://gbrain/db" })); const baseEnv = { HOME: home, DATABASE_URL: "postgresql://intentional/app-db", GSTACK_RESPECT_ENV_DATABASE_URL: "1", }; const result = buildGbrainEnv({ baseEnv }); expect(result.DATABASE_URL).toBe("postgresql://intentional/app-db"); }); it("returns caller env unchanged when config file is missing", () => { // No config.json written. const baseEnv = { HOME: home, DATABASE_URL: "postgresql://app/db" }; const result = buildGbrainEnv({ baseEnv }); expect(result.DATABASE_URL).toBe("postgresql://app/db"); }); it("returns caller env unchanged when config file is unparseable", () => { writeFileSync(join(gbrainHome, "config.json"), "{not json"); const baseEnv = { HOME: home, DATABASE_URL: "postgresql://app/db" }; const result = buildGbrainEnv({ baseEnv }); expect(result.DATABASE_URL).toBe("postgresql://app/db"); }); it("returns caller env unchanged when config names neither a database_url nor PGLite", () => { writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ embedding_model: "x" })); const baseEnv = { HOME: home, DATABASE_URL: "postgresql://app/db" }; const result = buildGbrainEnv({ baseEnv }); expect(result.DATABASE_URL).toBe("postgresql://app/db"); }); for (const cfg of [{ engine: "pglite" }, { database_path: "/brain/db.pglite" }]) { it(`blanks inherited database URLs for a PGLite config ${JSON.stringify(cfg)} (#1917)`, () => { writeFileSync(join(gbrainHome, "config.json"), JSON.stringify(cfg)); const baseEnv = { HOME: home, DATABASE_URL: "postgresql://app/db", GBRAIN_DATABASE_URL: "postgresql://app/other" }; const result = buildGbrainEnv({ baseEnv }); expect(result.DATABASE_URL).toBe(""); expect(result.GBRAIN_DATABASE_URL).toBe(""); expect(baseEnv.DATABASE_URL).toBe("postgresql://app/db"); }); } it("keeps an inherited DATABASE_URL for a PGLite config under GSTACK_RESPECT_ENV_DATABASE_URL=1", () => { writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ engine: "pglite" })); const baseEnv = { HOME: home, DATABASE_URL: "postgresql://app/db", GSTACK_RESPECT_ENV_DATABASE_URL: "1" }; expect(buildGbrainEnv({ baseEnv }).DATABASE_URL).toBe("postgresql://app/db"); }); it("honors GBRAIN_HOME when set, with gbrain's parent-dir semantics (#2521)", () => { // Move the config to an alternate dir; set GBRAIN_HOME to point at its // PARENT — gbrain's configDir() appends `.gbrain` itself, so // GBRAIN_HOME=/x reads /x/.gbrain/config.json. const altGbrainHome = join(home, "alt-gbrain"); mkdirSync(join(altGbrainHome, ".gbrain"), { recursive: true }); writeFileSync( join(altGbrainHome, ".gbrain", "config.json"), JSON.stringify({ database_url: "postgresql://alt/db" }), ); // No file at the default ~/.gbrain location. const baseEnv = { HOME: home, GBRAIN_HOME: altGbrainHome }; const result = buildGbrainEnv({ baseEnv }); expect(result.DATABASE_URL).toBe("postgresql://alt/db"); }); it("ignores a config at $GBRAIN_HOME/config.json — gbrain never reads that file (#2521)", () => { const altGbrainHome = join(home, "alt-gbrain-flat"); mkdirSync(altGbrainHome, { recursive: true }); writeFileSync( join(altGbrainHome, "config.json"), JSON.stringify({ database_url: "postgresql://alt/db" }), ); const baseEnv = { HOME: home, GBRAIN_HOME: altGbrainHome }; const result = buildGbrainEnv({ baseEnv }); expect(result.DATABASE_URL).toBeUndefined(); }); it("returns a fresh env object — never the caller's env by identity", () => { // Codex review #11: object-identity equality lets later mutation of the // returned env leak back into the caller's view. The helper MUST clone. writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: "postgresql://gbrain/db" })); const baseEnv: NodeJS.ProcessEnv = { HOME: home, FOO: "bar" }; const result = buildGbrainEnv({ baseEnv }); expect(result).not.toBe(baseEnv); // Mutating result must not affect baseEnv. result.FOO = "changed"; expect(baseEnv.FOO).toBe("bar"); }); it("preserves unrelated env vars from the base env", () => { writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: "postgresql://gbrain/db" })); const baseEnv = { HOME: home, PATH: "/usr/bin", FOO: "bar" }; const result = buildGbrainEnv({ baseEnv }); expect(result.PATH).toBe("/usr/bin"); expect(result.FOO).toBe("bar"); expect(result.HOME).toBe(home); }); it("does not modify DATABASE_URL when caller's value already matches config", () => { // Subtle: helper should be a no-op when caller already has the right value. // Lets us skip the stderr announce on idempotent re-invocation. writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: "postgresql://gbrain/db" })); const baseEnv = { HOME: home, DATABASE_URL: "postgresql://gbrain/db" }; const result = buildGbrainEnv({ baseEnv }); expect(result.DATABASE_URL).toBe("postgresql://gbrain/db"); }); // --- GBRAIN_PREPARE is never auto-set (#1965) --- // gbrain auto-disables prepared statements on transaction-mode poolers; // forcing GBRAIN_PREPARE=true there breaks every write with "prepared // statement does not exist". The helper must leave the variable alone in // all cases — a caller-set value (the documented session-mode-on-6543 // override) passes through untouched. it("does not set GBRAIN_PREPARE when DATABASE_URL targets port 6543 (transaction-mode pooler)", () => { const poolerUrl = "postgresql://postgres.abc:pw@aws-0-us-east-1.pooler.supabase.com:6543/postgres"; writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: poolerUrl })); const baseEnv = { HOME: home }; const result = buildGbrainEnv({ baseEnv }); expect(result.DATABASE_URL).toBe(poolerUrl); expect(result.GBRAIN_PREPARE).toBeUndefined(); }); it("does not set GBRAIN_PREPARE when DATABASE_URL targets port 5432 (session-mode pooler)", () => { const sessionUrl = "postgresql://postgres.abc:pw@aws-0-us-east-1.pooler.supabase.com:5432/postgres"; writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: sessionUrl })); const baseEnv = { HOME: home }; const result = buildGbrainEnv({ baseEnv }); expect(result.GBRAIN_PREPARE).toBeUndefined(); }); it("does not set GBRAIN_PREPARE for pglite (no port in URL)", () => { writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: "postgresql://gbrain/db" })); const baseEnv = { HOME: home }; const result = buildGbrainEnv({ baseEnv }); expect(result.GBRAIN_PREPARE).toBeUndefined(); }); it("passes through caller's explicit GBRAIN_PREPARE=false", () => { const poolerUrl = "postgresql://postgres.abc:pw@aws-0-us-east-1.pooler.supabase.com:6543/postgres"; writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: poolerUrl })); const baseEnv = { HOME: home, GBRAIN_PREPARE: "false" }; const result = buildGbrainEnv({ baseEnv }); expect(result.GBRAIN_PREPARE).toBe("false"); }); it("passes through caller's explicit GBRAIN_PREPARE=true (session-mode-on-6543 override)", () => { const poolerUrl = "postgresql://postgres.abc:pw@aws-0-us-east-1.pooler.supabase.com:6543/postgres"; writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: poolerUrl })); const baseEnv = { HOME: home, GBRAIN_PREPARE: "true" }; const result = buildGbrainEnv({ baseEnv }); expect(result.GBRAIN_PREPARE).toBe("true"); }); }); describe("isTransactionModePooler", () => { it("returns true for Supabase transaction-mode pooler URL (port 6543)", () => { expect(isTransactionModePooler( "postgresql://postgres.abc:pw@aws-0-us-east-1.pooler.supabase.com:6543/postgres" )).toBe(true); }); it("returns false for session-mode pooler URL (port 5432)", () => { expect(isTransactionModePooler( "postgresql://postgres.abc:pw@aws-0-us-east-1.pooler.supabase.com:5432/postgres" )).toBe(false); }); it("returns false for pglite-style URL (no port)", () => { expect(isTransactionModePooler("postgresql://gbrain/db")).toBe(false); }); it("returns false for unparseable URL", () => { expect(isTransactionModePooler("not-a-url")).toBe(false); }); it("handles postgres:// scheme (without 'ql')", () => { expect(isTransactionModePooler( "postgres://postgres.abc:pw@host:6543/postgres" )).toBe(true); }); }); // #1917: a real gbrain child launched by a Bun parent whose cwd is a project // with conflicting dotenv files. Bun autoloads the project's .env into the // parent, and a Bun child started in the project would autoload it again. describe("gbrain children never see a project's dotenv database URL (#1917)", () => { let root: string; beforeEach(() => { root = mkdtempSync(join(tmpdir(), "gstack-gbrain-dotenv-")); }); afterEach(() => rmSync(root, { recursive: true, force: true })); it("blanks the URLs and runs gbrain outside the project for every spawn helper", () => { const home = join(root, "home"); const project = join(root, "project"); const bin = join(root, "bin"); for (const dir of [join(home, ".gbrain"), project, bin]) mkdirSync(dir, { recursive: true }); writeFileSync(join(home, ".gbrain", "config.json"), JSON.stringify({ engine: "pglite", database_path: join(home, ".gbrain", "brain.pglite") })); // Built from parts so the CI credential scan of added lines stays at 0 HIGH. const appUrl = (db: string) => ["postgresql://app", "secret@localhost:5432/" + db].join(":"); writeFileSync(join(project, ".env"), `DATABASE_URL=${appUrl("app_env")}\n`); writeFileSync(join(project, ".env.local"), `GBRAIN_DATABASE_URL=${appUrl("app_local")}\n`); writeFileSync(join(bin, "gbrain"), `#!${process.execPath} console.log(JSON.stringify({ db: process.env.DATABASE_URL ?? null, gdb: process.env.GBRAIN_DATABASE_URL ?? null, cwd: process.cwd() })); `, { mode: 0o755 }); const lib = join(import.meta.dir, "..", "lib", "gbrain-exec.ts"); const parent = ` import { spawnGbrain, execGbrainText, spawnGbrainAsync } from ${JSON.stringify(lib)}; const out = { inherited: process.env.DATABASE_URL ?? null }; out.sync = JSON.parse(spawnGbrain(["probe"]).stdout); out.text = JSON.parse(execGbrainText(["probe"])); const child = spawnGbrainAsync(["probe"]); let buf = ""; child.stdout.on("data", (c) => (buf += c)); child.on("close", () => { out.async = JSON.parse(buf); console.log(JSON.stringify(out)); }); `; const r = spawnSync(process.execPath, ["-e", parent], { cwd: project, encoding: "utf-8", timeout: 20_000, env: { PATH: `${bin}:${process.env.PATH}`, HOME: home }, }); expect(r.stderr).toBe(""); const out = JSON.parse(r.stdout); expect(out.inherited).toContain("app_env"); for (const key of ["sync", "text", "async"]) { expect(out[key].db ?? "").toBe(""); expect(out[key].gdb ?? "").toBe(""); expect(out[key].cwd.startsWith(project)).toBe(false); } }); });