test: satisfy test-isolation gate for wave-B tests

check-test-isolation R1 forbids direct process.env mutation in non-serial
unit tests (env leaks across files sharing a shard process). Route the
GBRAIN_HOME / GBRAIN_CHAT_MODEL / GBRAIN_PGLITE_SNAPSHOT overrides through
the canonical withEnv() helper instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sinabina
2026-07-17 11:17:28 -07:00
co-authored by Claude Fable 5
parent 51d873c972
commit 70bc3690d7
2 changed files with 53 additions and 52 deletions
+46 -47
View File
@@ -4,69 +4,74 @@
* runtime-effective key in ~/.gbrain/config.json reported not-found while
* the runtime happily used it.
*
* Hermetic: GBRAIN_HOME points at a tmp dir (configDir() honors it) and the
* engine is a getConfig-only stub — the `get` path touches nothing else.
* Hermetic: GBRAIN_HOME points at a tmp dir (configDir() honors it) via
* withEnv (check-test-isolation R1), and the engine is a getConfig-only
* stub — the `get` path touches nothing else.
*/
import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test';
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
import { describe, test, expect, spyOn } from 'bun:test';
import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { runConfig } from '../src/commands/config.ts';
import { withEnv } from './helpers/with-env.ts';
import type { BrainEngine } from '../src/core/engine.ts';
const home = mkdtempSync(join(tmpdir(), 'gbrain-config-get-'));
mkdirSync(join(home, '.gbrain'), { recursive: true });
function stubEngine(dbValues: Record<string, string>): BrainEngine {
return {
getConfig: async (key: string) => dbValues[key] ?? null,
} as unknown as BrainEngine;
}
describe('#2120 — config get resolves file plane with DB fallback', () => {
let home: string;
let savedHome: string | undefined;
let savedChatModel: string | undefined;
let logs: string[];
let errs: string[];
let logSpy: ReturnType<typeof spyOn>;
let errSpy: ReturnType<typeof spyOn>;
function writeFileConfig(cfg: Record<string, unknown>): void {
writeFileSync(join(home, '.gbrain', 'config.json'), JSON.stringify(cfg));
}
beforeEach(() => {
home = mkdtempSync(join(tmpdir(), 'gbrain-config-get-'));
mkdirSync(join(home, '.gbrain'), { recursive: true });
savedHome = process.env.GBRAIN_HOME;
process.env.GBRAIN_HOME = home;
// loadConfig() overlays this env var onto the file plane — keep it out.
savedChatModel = process.env.GBRAIN_CHAT_MODEL;
delete process.env.GBRAIN_CHAT_MODEL;
logs = [];
errs = [];
logSpy = spyOn(console, 'log').mockImplementation((...a: unknown[]) => { logs.push(a.join(' ')); });
errSpy = spyOn(console, 'error').mockImplementation((...a: unknown[]) => { errs.push(a.join(' ')); });
});
afterEach(() => {
/** Run `config get <key>` with GBRAIN_HOME pinned to the tmp brain and the
* chat-model env overlay cleared, capturing output + exit code. */
async function runGet(
dbValues: Record<string, string>,
key: string,
): Promise<{ logs: string[]; errs: string[]; exit: number | null }> {
const logs: string[] = [];
const errs: string[] = [];
let exit: number | null = null;
const logSpy = spyOn(console, 'log').mockImplementation((...a: unknown[]) => { logs.push(a.join(' ')); });
const errSpy = spyOn(console, 'error').mockImplementation((...a: unknown[]) => { errs.push(a.join(' ')); });
const exitSpy = spyOn(process, 'exit').mockImplementation(((code?: number) => {
exit = code ?? 0;
throw new Error(`EXIT:${code}`);
}) as never);
try {
await withEnv(
{ GBRAIN_HOME: home, GBRAIN_CHAT_MODEL: undefined },
() => runConfig(stubEngine(dbValues), ['get', key]),
);
} catch (e) {
if (!(e as Error).message.startsWith('EXIT:')) throw e;
} finally {
logSpy.mockRestore();
errSpy.mockRestore();
if (savedHome === undefined) delete process.env.GBRAIN_HOME;
else process.env.GBRAIN_HOME = savedHome;
if (savedChatModel !== undefined) process.env.GBRAIN_CHAT_MODEL = savedChatModel;
rmSync(home, { recursive: true, force: true });
});
function writeFileConfig(cfg: Record<string, unknown>): void {
writeFileSync(join(home, '.gbrain', 'config.json'), JSON.stringify(cfg));
exitSpy.mockRestore();
}
return { logs, errs, exit };
}
describe('#2120 — config get resolves file plane with DB fallback', () => {
test('file-plane key with no DB row is found (the pre-fix not-found bug)', async () => {
writeFileConfig({ engine: 'pglite', chat_model: 'anthropic:claude-sonnet-4-6' });
await runConfig(stubEngine({}), ['get', 'chat_model']);
const { logs, errs, exit } = await runGet({}, 'chat_model');
expect(exit).toBeNull();
expect(logs).toContain('anthropic:claude-sonnet-4-6');
expect(errs.join('\n')).toContain('file/env plane');
});
test('file plane wins over DB plane (matches runtime precedence) and reports the shadow', async () => {
writeFileConfig({ engine: 'pglite', chat_model: 'anthropic:claude-sonnet-4-6' });
await runConfig(stubEngine({ chat_model: 'openai:gpt-5' }), ['get', 'chat_model']);
const { logs, errs } = await runGet({ chat_model: 'openai:gpt-5' }, 'chat_model');
expect(logs).toContain('anthropic:claude-sonnet-4-6');
expect(logs).not.toContain('openai:gpt-5');
expect(errs.join('\n')).toContain('shadowed');
@@ -74,21 +79,15 @@ describe('#2120 — config get resolves file plane with DB fallback', () => {
test('DB-plane-only key still resolves (no regression for dotted keys)', async () => {
writeFileConfig({ engine: 'pglite' });
await runConfig(stubEngine({ 'search.mode': 'balanced' }), ['get', 'search.mode']);
const { logs, errs } = await runGet({ 'search.mode': 'balanced' }, 'search.mode');
expect(logs).toContain('balanced');
expect(errs.join('\n')).toContain('db plane');
});
test('key in neither plane is still not-found (exit 1)', async () => {
writeFileConfig({ engine: 'pglite' });
const exitSpy = spyOn(process, 'exit').mockImplementation(((code?: number) => {
throw new Error(`EXIT:${code}`);
}) as never);
try {
await expect(runConfig(stubEngine({}), ['get', 'chat_model'])).rejects.toThrow('EXIT:1');
expect(errs.join('\n')).toContain('Config key not found: chat_model');
} finally {
exitSpy.mockRestore();
}
const { errs, exit } = await runGet({}, 'chat_model');
expect(exit).toBe(1);
expect(errs.join('\n')).toContain('Config key not found: chat_model');
});
});
+7 -5
View File
@@ -13,16 +13,18 @@
import { describe, test, expect, beforeAll, afterAll, spyOn } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { runSources } from '../src/commands/sources.ts';
// Cold-init schema path so the archive columns exist (same as destructive-guard tests).
delete process.env.GBRAIN_PGLITE_SNAPSHOT;
import { withEnv } from './helpers/with-env.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
// Cold-init schema path so the archive columns exist (same intent as
// destructive-guard tests, but env-isolated per check-test-isolation R1).
await withEnv({ GBRAIN_PGLITE_SNAPSHOT: undefined }, async () => {
await engine.connect({});
await engine.initSchema();
});
await engine.executeRaw(
`INSERT INTO sources (id, name) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING`,
['arch-idem', 'arch-idem'],