From 82cc7fff90429ef05585705b3d91a74afeffb4ce Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 14 Jun 2026 08:16:06 -0700 Subject: [PATCH] fix(context): env-plane window knob works config-less; harden two gateway-state-leak victims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/core/context/reflex.ts | 12 ++++++++++++ test/doctor-federation-health.test.ts | 13 +++++++++++++ test/retrieval-reflex.test.ts | 23 +++++++++++++++++++++++ test/sync-cost-preview.test.ts | 13 ++++++++++++- 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/core/context/reflex.ts b/src/core/context/reflex.ts index c5c37227c..188dbea21 100644 --- a/src/core/context/reflex.ts +++ b/src/core/context/reflex.ts @@ -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; diff --git a/test/doctor-federation-health.test.ts b/test/doctor-federation-health.test.ts index 091ef92f9..71a808fd8 100644 --- a/test/doctor-federation-health.test.ts +++ b/test/doctor-federation-health.test.ts @@ -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(); diff --git a/test/retrieval-reflex.test.ts b/test/retrieval-reflex.test.ts index 23c7d05b8..01e9d20b8 100644 --- a/test/retrieval-reflex.test.ts +++ b/test/retrieval-reflex.test.ts @@ -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; + } + }); }); diff --git a/test/sync-cost-preview.test.ts b/test/sync-cost-preview.test.ts index 7f492c032..52d4dba6b 100644 --- a/test/sync-cost-preview.test.ts +++ b/test/sync-cost-preview.test.ts @@ -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