mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 14:27:58 +00:00
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>
442 lines
20 KiB
TypeScript
442 lines
20 KiB
TypeScript
/**
|
|
* Retrieval Reflex — resolver + assemble() regression tests (#1981, T5).
|
|
*
|
|
* Encodes the motivating failure: a turn naming an entity with an existing brain
|
|
* page must surface a pointer BEFORE the agent answers. Runs against a hermetic
|
|
* in-memory PGLite engine (no file lock). The PGLite-in-production path is
|
|
* covered by exercising the resolver through an injected resolver (the same
|
|
* shape the serve IPC / host ctx.brainQuery supply).
|
|
*/
|
|
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
import { withEnv } from './helpers/with-env.ts';
|
|
import { normalizeAlias } from '../src/core/search/alias-normalize.ts';
|
|
import { resolveEntitiesToPointers } from '../src/core/context/retrieval-reflex.ts';
|
|
import { extractCandidates } from '../src/core/context/entity-salience.ts';
|
|
import { createGBrainContextEngine } from '../src/core/context-engine.ts';
|
|
import { disposeReflex } from '../src/core/context/reflex.ts';
|
|
import { TAKES_FENCE_BEGIN, TAKES_FENCE_END } from '../src/core/takes-fence.ts';
|
|
|
|
let engine: PGLiteEngine;
|
|
|
|
async function seed(slug: string, title: string, body: string, source = 'default') {
|
|
await engine.executeRaw(
|
|
`INSERT INTO pages (slug, source_id, type, title, compiled_truth, timeline)
|
|
VALUES ($1, $2, 'person', $3, $4, '')`,
|
|
[slug, source, title, body],
|
|
);
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({});
|
|
await engine.initSchema();
|
|
}, 60_000);
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
await disposeReflex();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await engine.executeRaw('DELETE FROM page_aliases').catch(() => {});
|
|
await engine.executeRaw('DELETE FROM pages');
|
|
});
|
|
|
|
describe('resolveEntitiesToPointers', () => {
|
|
test('namespaced slug resolves from a bare title (the recall fix, D6)', async () => {
|
|
await seed('people/alice-example', 'Alice Example', 'Alice is an early founder.');
|
|
const candidates = extractCandidates('what do you think about Alice Example?');
|
|
const block = await resolveEntitiesToPointers(engine, 'default', candidates, {});
|
|
expect(block).not.toBeNull();
|
|
expect(block!.pointers[0].slug).toBe('people/alice-example');
|
|
expect(block!.text).toContain('people/alice-example');
|
|
expect(block!.text).toContain('use get_page');
|
|
});
|
|
|
|
test('alias arm resolves an unambiguous single-slug hit', async () => {
|
|
await seed('people/swami-x', 'Swami X', 'A close friend.');
|
|
await engine.setPageAliases('people/swami-x', 'default', [normalizeAlias('Swami')]);
|
|
const block = await resolveEntitiesToPointers(engine, 'default', extractCandidates('Spoke with Swami today'), {});
|
|
expect(block).not.toBeNull();
|
|
expect(block!.pointers.some((p) => p.slug === 'people/swami-x')).toBe(true);
|
|
});
|
|
|
|
test('privacy (D5): takes-fence content never leaks into the synopsis', async () => {
|
|
const body = `${TAKES_FENCE_BEGIN}\nSECRET_HUNCH_DO_NOT_LEAK\n${TAKES_FENCE_END}\nAlice is a founder.`;
|
|
await seed('people/alice-example', 'Alice Example', body);
|
|
const block = await resolveEntitiesToPointers(engine, 'default', extractCandidates('about Alice Example'), {});
|
|
expect(block).not.toBeNull();
|
|
expect(block!.text).not.toContain('SECRET_HUNCH_DO_NOT_LEAK');
|
|
});
|
|
|
|
test('suppression: a slug already in PRIOR context is dropped', async () => {
|
|
await seed('people/alice-example', 'Alice Example', 'A founder.');
|
|
const candidates = extractCandidates('tell me about Alice Example');
|
|
const block = await resolveEntitiesToPointers(engine, 'default', candidates, {
|
|
priorContextText: 'earlier we already opened people/alice-example and read it',
|
|
});
|
|
expect(block).toBeNull();
|
|
});
|
|
|
|
test('empty candidates → null', async () => {
|
|
expect(await resolveEntitiesToPointers(engine, 'default', [], {})).toBeNull();
|
|
});
|
|
|
|
test('cap to maxPointers', async () => {
|
|
await seed('people/aa', 'Aa Bb', 'x');
|
|
await seed('people/cc', 'Cc Dd', 'y');
|
|
await seed('people/ee', 'Ee Ff', 'z');
|
|
const block = await resolveEntitiesToPointers(
|
|
engine,
|
|
'default',
|
|
extractCandidates('met Aa Bb, Cc Dd, and Ee Ff'),
|
|
{ maxPointers: 2 },
|
|
);
|
|
expect(block!.pointers.length).toBe(2);
|
|
});
|
|
|
|
test('pre-v110 brains: alias-table absence does not break the slug arm', async () => {
|
|
await seed('people/alice-example', 'Alice Example', 'A founder.');
|
|
// Simulate no page_aliases table.
|
|
await engine.executeRaw('DROP TABLE IF EXISTS page_aliases');
|
|
const block = await resolveEntitiesToPointers(engine, 'default', extractCandidates('about Alice Example'), {});
|
|
expect(block).not.toBeNull();
|
|
expect(block!.pointers[0].slug).toBe('people/alice-example');
|
|
// restore for other tests
|
|
await engine.initSchema();
|
|
});
|
|
});
|
|
|
|
describe('context-engine assemble() — Retrieval Reflex integration', () => {
|
|
// Each test wraps its body in withEnv (NOT a beforeEach env mutation) so the
|
|
// flag is restored even on throw — required by check-test-isolation rule R1.
|
|
const REFLEX_ON = { GBRAIN_RETRIEVAL_REFLEX: 'true' };
|
|
|
|
test('regression: a named entity with a page surfaces a pointer (host resolver path)', async () => {
|
|
await withEnv(REFLEX_ON, async () => {
|
|
await seed('people/alice-example', 'Alice Example', 'Alice is a founder.');
|
|
// Inject a resolver the way the OpenClaw host (ctx.brainQuery) or serve IPC would.
|
|
const ce = createGBrainContextEngine({
|
|
workspaceDir: '/tmp/rr-test-ws',
|
|
resolveEntities: (candidates, opts) =>
|
|
resolveEntitiesToPointers(engine, 'default', candidates, opts),
|
|
});
|
|
const res = await ce.assemble({
|
|
sessionId: 's1',
|
|
messages: [{ role: 'user', content: 'what do you think about Alice Example?' }],
|
|
});
|
|
expect(res.systemPromptAddition).toContain('Brain pages mentioned this turn');
|
|
expect(res.systemPromptAddition).toContain('people/alice-example');
|
|
expect(res.systemPromptAddition).toContain('use get_page');
|
|
});
|
|
});
|
|
|
|
test('no resolver available (PGLite, no serve/host) → no throw, live context still present', async () => {
|
|
await withEnv(REFLEX_ON, async () => {
|
|
const ce = createGBrainContextEngine({ workspaceDir: '/tmp/rr-test-ws-2' });
|
|
const res = await ce.assemble({
|
|
sessionId: 's2',
|
|
messages: [{ role: 'user', content: 'what about Alice Example?' }],
|
|
});
|
|
// Live Context block always ships; no pointer block (nothing resolved).
|
|
expect(res.systemPromptAddition).toContain('Live Context');
|
|
expect(res.systemPromptAddition).not.toContain('Brain pages mentioned this turn');
|
|
});
|
|
});
|
|
|
|
test('zero salient candidates → no brain touch, no pointer block', async () => {
|
|
await withEnv(REFLEX_ON, async () => {
|
|
let called = false;
|
|
const ce = createGBrainContextEngine({
|
|
workspaceDir: '/tmp/rr-test-ws-3',
|
|
resolveEntities: async () => { called = true; return null; },
|
|
});
|
|
const res = await ce.assemble({
|
|
sessionId: 's3',
|
|
messages: [{ role: 'user', content: 'can you help me with this?' }],
|
|
});
|
|
expect(called).toBe(false);
|
|
expect(res.systemPromptAddition).not.toContain('Brain pages mentioned this turn');
|
|
});
|
|
});
|
|
|
|
test('suppression uses PRIOR turns only, not the current message', async () => {
|
|
await withEnv(REFLEX_ON, async () => {
|
|
await seed('people/alice-example', 'Alice Example', 'A founder.');
|
|
const ce = createGBrainContextEngine({
|
|
workspaceDir: '/tmp/rr-test-ws-4',
|
|
resolveEntities: (candidates, opts) =>
|
|
resolveEntitiesToPointers(engine, 'default', candidates, opts),
|
|
});
|
|
// The current message names Alice Example; prior context does NOT. Must fire.
|
|
const res = await ce.assemble({
|
|
sessionId: 's4',
|
|
messages: [
|
|
{ role: 'user', content: 'hello' },
|
|
{ role: 'assistant', content: 'hi there' },
|
|
{ role: 'user', content: 'what do you think about Alice Example?' },
|
|
],
|
|
});
|
|
expect(res.systemPromptAddition).toContain('people/alice-example');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('v0.43 (#2095) — rolling window extraction through assemble()', () => {
|
|
const REFLEX_ON = { GBRAIN_RETRIEVAL_REFLEX: 'true' };
|
|
|
|
test('entity named ONLY in a previous assistant turn yields a pointer now', async () => {
|
|
await withEnv(REFLEX_ON, async () => {
|
|
await seed('people/alice-example', 'Alice Example', 'Alice is a founder.');
|
|
const ce = createGBrainContextEngine({
|
|
workspaceDir: '/tmp/rr-test-ws-w1',
|
|
resolveEntities: (candidates, opts) =>
|
|
resolveEntitiesToPointers(engine, 'default', candidates, opts),
|
|
});
|
|
// Current turn is a pronoun follow-up; the antecedent was NAMED two
|
|
// turns back by the ASSISTANT. Pre-window this never fired.
|
|
const res = await ce.assemble({
|
|
sessionId: 'w1',
|
|
messages: [
|
|
{ role: 'user', content: 'who should I talk to about the seed round?' },
|
|
{ role: 'assistant', content: 'Alice Example led a similar round last year.' },
|
|
{ role: 'user', content: 'what did she invest in?' },
|
|
],
|
|
});
|
|
expect(res.systemPromptAddition).toContain('people/alice-example');
|
|
});
|
|
});
|
|
|
|
test('window=1 reproduces the legacy current-turn-only behavior', async () => {
|
|
await withEnv({ ...REFLEX_ON, GBRAIN_RETRIEVAL_REFLEX_WINDOW_TURNS: '1' }, async () => {
|
|
await seed('people/alice-example', 'Alice Example', 'Alice is a founder.');
|
|
const ce = createGBrainContextEngine({
|
|
workspaceDir: '/tmp/rr-test-ws-w2',
|
|
resolveEntities: (candidates, opts) =>
|
|
resolveEntitiesToPointers(engine, 'default', candidates, opts),
|
|
});
|
|
const res = await ce.assemble({
|
|
sessionId: 'w2',
|
|
messages: [
|
|
{ role: 'assistant', content: 'Alice Example led a similar round last year.' },
|
|
{ role: 'user', content: 'what did she invest in?' },
|
|
],
|
|
});
|
|
// Current turn has no extractable entity; window=1 must NOT widen.
|
|
expect(res.systemPromptAddition).not.toContain('people/alice-example');
|
|
});
|
|
});
|
|
|
|
test('windowed suppression is slug-only: a prior-turn MENTION does not suppress (codex D7)', async () => {
|
|
await withEnv(REFLEX_ON, async () => {
|
|
await seed('people/alice-example', 'Alice Example', 'A founder.');
|
|
const ce = createGBrainContextEngine({
|
|
workspaceDir: '/tmp/rr-test-ws-w3',
|
|
resolveEntities: (candidates, opts) =>
|
|
resolveEntitiesToPointers(engine, 'default', candidates, opts),
|
|
});
|
|
// "Alice Example" appears in a PRIOR turn (a bare mention — prior
|
|
// context contains the TITLE). Under the legacy title rule the pointer
|
|
// would be suppressed; slug-only windowing must still fire.
|
|
const res = await ce.assemble({
|
|
sessionId: 'w3',
|
|
messages: [
|
|
{ role: 'user', content: 'I met Alice Example yesterday' },
|
|
{ role: 'assistant', content: 'How did the meeting with Alice Example go?' },
|
|
{ role: 'user', content: 'she wants to invest — thoughts?' },
|
|
],
|
|
});
|
|
expect(res.systemPromptAddition).toContain('people/alice-example');
|
|
});
|
|
});
|
|
|
|
test('windowed suppression still drops an already-surfaced page (slug in prior context)', async () => {
|
|
await withEnv(REFLEX_ON, async () => {
|
|
await seed('people/alice-example', 'Alice Example', 'A founder.');
|
|
const ce = createGBrainContextEngine({
|
|
workspaceDir: '/tmp/rr-test-ws-w4',
|
|
resolveEntities: (candidates, opts) =>
|
|
resolveEntitiesToPointers(engine, 'default', candidates, opts),
|
|
});
|
|
const res = await ce.assemble({
|
|
sessionId: 'w4',
|
|
messages: [
|
|
{ role: 'assistant', content: 'Pointer: **Alice Example** → `people/alice-example` (use get_page)' },
|
|
{ role: 'user', content: 'tell me more about Alice Example' },
|
|
],
|
|
});
|
|
expect(res.systemPromptAddition).not.toContain('Brain pages mentioned this turn');
|
|
});
|
|
});
|
|
|
|
test('fail-open: a throwing resolver under windowing never breaks the turn', async () => {
|
|
await withEnv(REFLEX_ON, async () => {
|
|
await seed('people/alice-example', 'Alice Example', 'A founder.');
|
|
const ce = createGBrainContextEngine({
|
|
workspaceDir: '/tmp/rr-test-ws-w5',
|
|
resolveEntities: async () => { throw new Error('resolver exploded'); },
|
|
});
|
|
const res = await ce.assemble({
|
|
sessionId: 'w5',
|
|
messages: [
|
|
{ role: 'assistant', content: 'Alice Example is relevant here.' },
|
|
{ role: 'user', content: 'ok tell me about her' },
|
|
],
|
|
});
|
|
expect(res.systemPromptAddition).toContain('Live Context');
|
|
expect(res.systemPromptAddition).not.toContain('Brain pages mentioned this turn');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('ambient-channel event logging (codex D11 — accept-side logDeliveredReflexPointers)', () => {
|
|
test('logDeliveredReflexPointers logs channel=reflex events through the drained sink', async () => {
|
|
const { logDeliveredReflexPointers } = await import('../src/core/context/retrieval-reflex.ts');
|
|
const { awaitPendingVolunteerEventWrites, _resetPendingVolunteerEventWritesForTests } =
|
|
await import('../src/core/context/volunteer-events.ts');
|
|
_resetPendingVolunteerEventWritesForTests();
|
|
await engine.executeRaw('DELETE FROM context_volunteer_events').catch(() => {});
|
|
await seed('people/alice-example', 'Alice Example', 'A founder.');
|
|
|
|
const block = await resolveEntitiesToPointers(
|
|
engine,
|
|
'default',
|
|
extractCandidates('what do you think about Alice Example?'),
|
|
{},
|
|
);
|
|
expect(block).not.toBeNull();
|
|
logDeliveredReflexPointers(engine, block!.pointers);
|
|
const { unfinished } = await awaitPendingVolunteerEventWrites(5_000);
|
|
expect(unfinished).toBe(0);
|
|
const rows = await engine.executeRaw<{ channel: string; slug: string; match_arm: string }>(
|
|
'SELECT channel, slug, match_arm FROM context_volunteer_events',
|
|
[],
|
|
);
|
|
expect(rows.length).toBe(1);
|
|
expect(rows[0].channel).toBe('reflex');
|
|
expect(rows[0].slug).toBe('people/alice-example');
|
|
expect(rows[0].match_arm).toBe('title');
|
|
});
|
|
|
|
test('the bare resolver logs nothing — delivery is the only logging seam', async () => {
|
|
await engine.executeRaw('DELETE FROM context_volunteer_events').catch(() => {});
|
|
await seed('people/alice-example', 'Alice Example', 'A founder.');
|
|
const block = await resolveEntitiesToPointers(
|
|
engine,
|
|
'default',
|
|
extractCandidates('about Alice Example'),
|
|
{},
|
|
);
|
|
expect(block).not.toBeNull();
|
|
const { awaitPendingVolunteerEventWrites } = await import('../src/core/context/volunteer-events.ts');
|
|
await awaitPendingVolunteerEventWrites(5_000);
|
|
const rows = await engine.executeRaw<{ channel: string }>('SELECT channel FROM context_volunteer_events', []);
|
|
expect(rows.length).toBe(0);
|
|
});
|
|
|
|
test('logDeliveredReflexPointers with an empty pointer list is a no-op', async () => {
|
|
const { logDeliveredReflexPointers } = await import('../src/core/context/retrieval-reflex.ts');
|
|
const { awaitPendingVolunteerEventWrites } = await import('../src/core/context/volunteer-events.ts');
|
|
await engine.executeRaw('DELETE FROM context_volunteer_events').catch(() => {});
|
|
logDeliveredReflexPointers(engine, []);
|
|
await awaitPendingVolunteerEventWrites(5_000);
|
|
const rows = await engine.executeRaw<{ channel: string }>('SELECT channel FROM context_volunteer_events', []);
|
|
expect(rows.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('serve IPC wiring — suppression passthrough + reflex-channel logging (review hardening)', () => {
|
|
test('the IPC round-trip honors slug-only suppression and logs channel=reflex', async () => {
|
|
const { startResolveIpcServer, resolveViaIpc, resolveSocketPath, IPC_UNAVAILABLE } =
|
|
await import('../src/core/context/resolve-ipc.ts');
|
|
const { awaitPendingVolunteerEventWrites, _resetPendingVolunteerEventWritesForTests } =
|
|
await import('../src/core/context/volunteer-events.ts');
|
|
const { mkdtempSync, rmSync } = await import('fs');
|
|
const { join } = await import('path');
|
|
const { tmpdir } = await import('os');
|
|
|
|
_resetPendingVolunteerEventWritesForTests();
|
|
await engine.executeRaw('DELETE FROM context_volunteer_events').catch(() => {});
|
|
await seed('people/alice-example', 'Alice Example', 'A founder.');
|
|
|
|
const dir = mkdtempSync(join(tmpdir(), 'rr-ipc-'));
|
|
const sock = resolveSocketPath(dir);
|
|
// The SAME wiring shape src/mcp/server.ts uses for serve: forwards
|
|
// suppression from the request; logging happens at DELIVERY via the
|
|
// onDelivered hook (post-write), never inside the resolver.
|
|
const { logDeliveredReflexPointers } = await import('../src/core/context/retrieval-reflex.ts');
|
|
const server = await startResolveIpcServer(
|
|
sock,
|
|
(req) =>
|
|
resolveEntitiesToPointers(engine, req.sourceId || 'default', req.candidates ?? [], {
|
|
priorContextText: req.priorContextText,
|
|
maxPointers: req.maxPointers,
|
|
suppression: req.suppression,
|
|
}),
|
|
(block) => logDeliveredReflexPointers(engine, block.pointers),
|
|
);
|
|
expect(server).not.toBeNull();
|
|
try {
|
|
// slug-only suppression: a TITLE mention in prior context must NOT
|
|
// suppress (the windowing contract), and the resolve must log.
|
|
const block = await resolveViaIpc(sock, {
|
|
candidates: extractCandidates('tell me about Alice Example'),
|
|
priorContextText: 'earlier turn merely mentioned Alice Example',
|
|
suppression: 'slug-only',
|
|
});
|
|
expect(block).not.toBe(IPC_UNAVAILABLE);
|
|
expect(block).not.toBeNull();
|
|
expect((block as { pointers: Array<{ slug: string }> }).pointers[0].slug).toBe('people/alice-example');
|
|
|
|
const { unfinished } = await awaitPendingVolunteerEventWrites(5_000);
|
|
expect(unfinished).toBe(0);
|
|
const rows = await engine.executeRaw<{ channel: string }>(
|
|
'SELECT channel FROM context_volunteer_events', [],
|
|
);
|
|
expect(rows.length).toBe(1);
|
|
expect(rows[0].channel).toBe('reflex');
|
|
} finally {
|
|
server!.close();
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('windowTurnCount — knob edge semantics', () => {
|
|
test('0, negative, NaN, and absent all fall back to the default of 4 (1 = legacy off)', async () => {
|
|
const { windowTurnCount, DEFAULT_WINDOW_TURNS } = await import('../src/core/context/reflex.ts');
|
|
expect(DEFAULT_WINDOW_TURNS).toBe(4);
|
|
expect(windowTurnCount(null)).toBe(4);
|
|
expect(windowTurnCount({ retrieval_reflex_window_turns: 0 } as never)).toBe(4);
|
|
expect(windowTurnCount({ retrieval_reflex_window_turns: -3 } as never)).toBe(4);
|
|
expect(windowTurnCount({ retrieval_reflex_window_turns: Number.NaN } as never)).toBe(4);
|
|
// The documented "off" switch is 1 (legacy single-turn), not 0.
|
|
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;
|
|
}
|
|
});
|
|
});
|