This commit is contained in:
maxpetrusenkoagent 2026-07-10 08:00:02 -04:00 committed by GitHub
commit 6d28cacf74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 60 additions and 7 deletions

View File

@ -38,6 +38,8 @@ import { spawnSync, spawn, execFileSync, type SpawnSyncReturns, type ChildProces
interface GbrainConfig {
database_url?: string;
engine?: string;
database_path?: string;
}
export interface BuildGbrainEnvOptions {
@ -80,9 +82,14 @@ export function isTransactionModePooler(url: string): boolean {
* unchanged when:
* - `GSTACK_RESPECT_ENV_DATABASE_URL=1` (intentional opt-out),
* - the config file is missing or unparseable,
* - the config has no `database_url`,
* - the config has no `database_url` and is not a local PGLite config,
* - the caller already set DATABASE_URL to the same value.
*
* For PGLite configs, there is no DATABASE_URL to seed. In that case we
* strip caller-supplied database URLs so Bun's project `.env` autoload
* cannot make gbrain probe an unrelated app database and classify a healthy
* file-backed brain as `broken-db`.
*
* GBRAIN_PREPARE is never set here (#1965): gbrain auto-disables prepared
* statements on transaction-mode poolers itself, and forcing them on breaks
* every write with "prepared statement does not exist". A caller-set
@ -109,7 +116,13 @@ export function buildGbrainEnv(opts: BuildGbrainEnvOptions = {}): NodeJS.Process
} catch {
return out;
}
if (!cfg.database_url) return out;
if (!cfg.database_url) {
if (cfg.engine === "pglite" || cfg.database_path) {
delete out.DATABASE_URL;
delete out.GBRAIN_DATABASE_URL;
}
return out;
}
const hadCaller = baseEnv.DATABASE_URL !== undefined;
const alreadyMatch = baseEnv.DATABASE_URL === cfg.database_url;

View File

@ -70,13 +70,28 @@ describe("buildGbrainEnv", () => {
expect(result.DATABASE_URL).toBe("postgresql://app/db");
});
it("returns caller env unchanged when config has no database_url field", () => {
writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ engine: "pglite" }));
it("returns caller env unchanged when config has no database_url field and no local-engine signal", () => {
writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ profile: "legacy" }));
const baseEnv = { HOME: home, DATABASE_URL: "postgresql://app/db" };
const result = buildGbrainEnv({ baseEnv });
expect(result.DATABASE_URL).toBe("postgresql://app/db");
});
it("strips caller database URLs for PGLite configs without a database_url", () => {
writeFileSync(
join(gbrainHome, "config.json"),
JSON.stringify({ engine: "pglite", database_path: join(gbrainHome, "gbrain.db") }),
);
const baseEnv = {
HOME: home,
DATABASE_URL: "sqlite:///app.db",
GBRAIN_DATABASE_URL: "postgresql://wrong/db",
};
const result = buildGbrainEnv({ baseEnv });
expect(result.DATABASE_URL).toBeUndefined();
expect(result.GBRAIN_DATABASE_URL).toBeUndefined();
});
it("honors GBRAIN_HOME when set (config aligned with detectEngineTier)", () => {
// Move the config to an alternate dir; set GBRAIN_HOME to point at it.
const altGbrainHome = join(home, "alt-gbrain");

View File

@ -61,8 +61,9 @@ interface FakeEnv {
*/
function makeEnv(opts: {
withGbrain?: boolean;
gbrainBehavior?: "ok" | "broken-db" | "broken-config" | "throws" | "slow";
gbrainBehavior?: "ok" | "broken-db" | "broken-config" | "throws" | "fail-on-database-url" | "slow";
withConfig?: boolean;
configJson?: Record<string, unknown>;
}): FakeEnv {
const tmp = mkdtempSync(join(tmpdir(), "gbrain-local-status-test-"));
const bindir = join(tmp, "bin");
@ -79,7 +80,7 @@ function makeEnv(opts: {
if (opts.withConfig) {
writeFileSync(
configPath,
JSON.stringify({ engine: "pglite", database_url: "pglite:///fake" }),
JSON.stringify(opts.configJson ?? { engine: "pglite", database_url: "pglite:///fake" }),
);
}
@ -102,7 +103,7 @@ function makeEnv(opts: {
}
function makeFakeGbrainScript(
behavior: "ok" | "broken-db" | "broken-config" | "throws" | "slow",
behavior: "ok" | "broken-db" | "broken-config" | "throws" | "fail-on-database-url" | "slow",
): string {
// "slow": healthy engine on a cold pooler connection (#1964) — sleeps past
// the (test-lowered) probe timeout, then would answer fine.
@ -135,6 +136,14 @@ if [ "$1" = "--version" ]; then
exit 0
fi
if [ "$1 $2" = "sources list" ]; then
if [ "${behavior}" = "fail-on-database-url" ]; then
if [ -n "$DATABASE_URL" ] || [ -n "$GBRAIN_DATABASE_URL" ]; then
echo "Cannot connect to database: leaked DATABASE_URL" >&2
exit 1
fi
echo '{"sources":[]}'
exit 0
fi
if [ ${exitCode} -eq 0 ]; then
echo '{"sources":[]}'
exit 0
@ -158,6 +167,8 @@ function applyEnv(env: FakeEnv): () => void {
HOME: process.env.HOME,
PATH: process.env.PATH,
GSTACK_HOME: process.env.GSTACK_HOME,
DATABASE_URL: process.env.DATABASE_URL,
GBRAIN_DATABASE_URL: process.env.GBRAIN_DATABASE_URL,
GBRAIN_HOME: process.env.GBRAIN_HOME,
GSTACK_GBRAIN_PROBE_TIMEOUT_MS: process.env.GSTACK_GBRAIN_PROBE_TIMEOUT_MS,
};
@ -231,6 +242,20 @@ describe("lib/gbrain-local-status — status classification", () => {
expect(localEngineStatus({ noCache: true })).toBe("ok");
});
it("does not misclassify a PGLite brain as broken-db when caller env leaks DATABASE_URL", () => {
env = makeEnv({
withGbrain: true,
gbrainBehavior: "fail-on-database-url",
withConfig: true,
configJson: { engine: "pglite", database_path: "local/pglite.db" },
});
restoreEnv = applyEnv(env);
process.env.DATABASE_URL = "sqlite:///app.db";
process.env.GBRAIN_DATABASE_URL = "postgresql://wrong/db";
expect(localEngineStatus({ noCache: true })).toBe("ok");
});
it("returns 'timeout' (not broken-config) when the probe exceeds the deadline (#1964)", () => {
env = makeEnv({ withGbrain: true, gbrainBehavior: "slow", withConfig: true });
restoreEnv = applyEnv(env);