mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 21:19:18 +00:00
fix(context): env-plane window knob works config-less; harden two gateway-state-leak victims
Three CI-only check failures, two root causes: 1. windowTurnCount ignored GBRAIN_RETRIEVAL_REFLEX_WINDOW_TURNS when loadConfig() returned null (no config file AND no DATABASE_URL — a clean CI shard with no brain). loadConfig drops its env→config mapping in that case, so the documented escape hatch silently died and the window fell back to 4 → windowed extraction widened when the test set window=1 → prior-turn entity leaked. Fixed: read the env var directly in windowTurnCount, mirroring reflexEnabled's direct process.env read. This is a real product bug, not just a test artifact — any config-less host using the env hatch was affected. Regression test pins it. 2. sync-cost-preview + doctor-federation-health failed only IN-SHARD: a sibling test configured a non-legacy (ZeroEntropy 1280-d / $0.05) gateway and never reset it. The legacy-embedding preload only restores the OpenAI/1536 default when the gateway slot is EMPTY, so a non-empty foreign config survives into the next file — and a file's beforeAll runs BEFORE the preload's restoring beforeEach, so federation-health built a vector(1280) column and its 1536-d fixture hit CheckExpectedDim. My new test files reshuffled the deterministic file→shard assignment, exposing this latent ordering bug. Hardened both victims to establish the gateway state they assert (sync-cost-preview resets to the unconfigured fallback; federation-health pins legacy 1536 before initSchema) so they're order-independent. Verified against a simulated leaker run before them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
07d3e45427
commit
82cc7fff90
@@ -79,6 +79,18 @@ export interface ReflexParams {
|
||||
export const DEFAULT_WINDOW_TURNS = 4;
|
||||
|
||||
export function windowTurnCount(cfg: GBrainConfig | null): number {
|
||||
// Env plane is read DIRECTLY here (mirroring reflexEnabled's direct
|
||||
// process.env read), not just via loadConfig's env→config mapping. When
|
||||
// there's no config file AND no DATABASE_URL, loadConfig() returns null and
|
||||
// drops that mapping entirely — so without this, the documented
|
||||
// GBRAIN_RETRIEVAL_REFLEX_WINDOW_TURNS escape hatch would be silently
|
||||
// ignored and the window would fall back to the default of 4 (a real
|
||||
// config-less-environment bug, e.g. a clean CI shard with no brain).
|
||||
const env = process.env.GBRAIN_RETRIEVAL_REFLEX_WINDOW_TURNS;
|
||||
if (env != null && env !== '') {
|
||||
const e = Number(env);
|
||||
if (Number.isFinite(e) && e >= 1) return Math.floor(e);
|
||||
}
|
||||
const n = cfg?.retrieval_reflex_window_turns;
|
||||
if (typeof n === 'number' && Number.isFinite(n) && n >= 1) return Math.floor(n);
|
||||
return DEFAULT_WINDOW_TURNS;
|
||||
|
||||
@@ -8,11 +8,24 @@
|
||||
*/
|
||||
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
|
||||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||||
import { configureGateway } from '../src/core/ai/gateway.ts';
|
||||
import { checkFederationHealth } from '../src/commands/doctor.ts';
|
||||
|
||||
let engine: PGLiteEngine;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Pin the legacy OpenAI/1536 embedding shape BEFORE initSchema builds the
|
||||
// content_chunks vector column. beforeAll runs before the legacy-embedding
|
||||
// preload's restoring beforeEach fires, so the gateway here is whatever the
|
||||
// PRIOR file in this shard left it — if that file configured a 1280-d model
|
||||
// and didn't reset, initSchema would build a vector(1280) column and the
|
||||
// 1536-d coverage fixture below would fail CheckExpectedDim. Establish the
|
||||
// dimension this file's fixtures assume rather than inheriting it.
|
||||
configureGateway({
|
||||
embedding_model: 'openai:text-embedding-3-large',
|
||||
embedding_dimensions: 1536,
|
||||
env: { ...process.env },
|
||||
});
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({});
|
||||
await engine.initSchema();
|
||||
|
||||
@@ -415,4 +415,27 @@ describe('windowTurnCount — knob edge semantics', () => {
|
||||
expect(windowTurnCount({ retrieval_reflex_window_turns: 1 } as never)).toBe(1);
|
||||
expect(windowTurnCount({ retrieval_reflex_window_turns: 6.9 } as never)).toBe(6);
|
||||
});
|
||||
|
||||
test('the env escape hatch is honored even when config is null (no config file / DB)', async () => {
|
||||
const { windowTurnCount } = await import('../src/core/context/reflex.ts');
|
||||
const prev = process.env.GBRAIN_RETRIEVAL_REFLEX_WINDOW_TURNS;
|
||||
try {
|
||||
// loadConfig() returns null in a config-less environment (clean CI shard,
|
||||
// no brain) and drops its env→config mapping — windowTurnCount must still
|
||||
// read the env var directly, or the documented escape hatch is dead and
|
||||
// the window silently defaults to 4.
|
||||
process.env.GBRAIN_RETRIEVAL_REFLEX_WINDOW_TURNS = '1';
|
||||
expect(windowTurnCount(null)).toBe(1);
|
||||
process.env.GBRAIN_RETRIEVAL_REFLEX_WINDOW_TURNS = '7';
|
||||
expect(windowTurnCount(null)).toBe(7);
|
||||
// Env wins over a config value too (env is the higher-precedence plane).
|
||||
expect(windowTurnCount({ retrieval_reflex_window_turns: 3 } as never)).toBe(7);
|
||||
// Garbage env falls through to config / default, not a crash.
|
||||
process.env.GBRAIN_RETRIEVAL_REFLEX_WINDOW_TURNS = 'not-a-number';
|
||||
expect(windowTurnCount(null)).toBe(4);
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.GBRAIN_RETRIEVAL_REFLEX_WINDOW_TURNS;
|
||||
else process.env.GBRAIN_RETRIEVAL_REFLEX_WINDOW_TURNS = prev;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* envelope paths don't depend on DB state.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { describe, test, expect, beforeEach } from 'bun:test';
|
||||
import {
|
||||
EMBEDDING_COST_PER_1K_TOKENS,
|
||||
estimateEmbeddingCostUsd,
|
||||
@@ -21,9 +21,20 @@ import {
|
||||
shouldBlockSync,
|
||||
} from '../src/core/embedding.ts';
|
||||
import { lookupEmbeddingPrice } from '../src/core/embedding-pricing.ts';
|
||||
import { resetGateway } from '../src/core/ai/gateway.ts';
|
||||
import { estimateTokens } from '../src/core/chunkers/code.ts';
|
||||
|
||||
describe('Layer 8 D1 — embedding cost model', () => {
|
||||
// The estimateEmbeddingCostUsd tests assert the gateway-UNCONFIGURED OpenAI
|
||||
// fallback ($0.13/Mtok). That fallback only fires when no gateway is
|
||||
// configured — so guarantee that precondition rather than depending on
|
||||
// ambient state. Without this, a sibling test file in the same shard that
|
||||
// configured (and didn't reset) a cheaper model leaks its rate in here and
|
||||
// these assertions flip (the legacy-embedding preload only restores when the
|
||||
// gateway slot is EMPTY, so a non-empty foreign config survives).
|
||||
beforeEach(() => resetGateway());
|
||||
|
||||
|
||||
test('EMBEDDING_COST_PER_1K_TOKENS back-compat constant is the OpenAI 3-large rate', () => {
|
||||
// Retained only for back-compat imports. Live cost math now resolves the
|
||||
// CONFIGURED model's rate via embedding-pricing.ts (see model-aware test
|
||||
|
||||
Reference in New Issue
Block a user