Compare commits

..
Author SHA1 Message Date
Garry TanandClaude Fable 5 d98e6b507f test(config): use withEnv() for TTL env mutation to satisfy check-test-isolation R1
CI verify failed: test/postgres-engine-config-cache.test.ts mutated
process.env.GBRAIN_CONFIG_CACHE_TTL_MS directly in beforeEach/afterEach.
Wrap each test body in withEnv() (test/helpers/with-env.ts) instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 10:58:53 -07:00
d4bbf6eeae perf(config): batch + cache engine.getConfig to kill ~85 round-trips per query
Takeover of #1694: a single search fires ~85 serial getConfig() reads
(loadConfigWithEngine x2 plus the mode/cache/intent/rerank/graph-signals
resolvers); on a remote pooler each read is a round-trip, dominating query
latency and risking cli.ts's 10s disconnect force-exit truncating stdout.

The first read now batch-loads the whole config table into a process-
lifetime Map (single-flight under concurrency); setConfig/unsetConfig
write through; a 30s TTL bounds multi-writer staleness and
GBRAIN_CONFIG_CACHE_TTL_MS=0 restores per-key reads.

Rebased onto current master: unlike the original diff, both the batch
load and the TTL=0 per-key fallback stay inside connRetry() so the
#1603/#1891 retry+reconnect posture (pooler-drop self-heal) is preserved.
PGLite stays uncached (in-process, zero round-trips; raw-SQL test
fixtures rely on fresh reads). E2E helpers pin the cache off since those
suites seed config via raw SQL.

Co-authored-by: Omerbahari <Omerbahari@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:26:28 -07:00
8 changed files with 181 additions and 273 deletions
+2 -11
View File
@@ -808,20 +808,12 @@ async function makeContext(engine: BrainEngine, params: Record<string, unknown>)
// 'default'. Wrapped in try/catch so a doctor / single-source brain that
// never set up sources still returns 'default' silently.
let sourceId: string | undefined;
// #2561: when the source resolved via a NON-explicit tier (path-match /
// brain default / sole-non-default / seed default), unqualified search-shaped
// reads span every `config.federated = true` source. Computed here (the
// trusted local boundary) and consumed by federatedSearchScope in
// operations.ts, which additionally gates on ctx.remote === false.
let localFederated: string[] | undefined;
try {
const { resolveSourceWithTier, localFederatedSourceIds } = await import('./core/source-resolver.ts');
const { resolveSourceId } = await import('./core/source-resolver.ts');
// params.source is set when a CLI flag was parsed for the op (rare; most
// CLI ops don't take --source). Falls through to env/dotfile/path-match.
const explicit = (params.source as string | undefined) ?? null;
const resolved = await resolveSourceWithTier(engine, explicit);
sourceId = resolved.source_id;
localFederated = await localFederatedSourceIds(engine, resolved.source_id, resolved.tier);
sourceId = await resolveSourceId(engine, explicit);
} catch {
// Source resolution failed (e.g. sources table doesn't exist on a fresh
// pre-init brain). Leave sourceId unset; engine read methods fall through
@@ -842,7 +834,6 @@ async function makeContext(engine: BrainEngine, params: Record<string, unknown>)
// table). Matches dispatch.ts's auto-fill so the contract holds across
// every transport.
sourceId: sourceId ?? 'default',
...(localFederated ? { localFederatedSourceIds: localFederated } : {}),
};
}
+2 -61
View File
@@ -424,23 +424,6 @@ export interface OperationContext {
* satisfied even on single-source brains.
*/
sourceId: string;
/**
* #2561 — federated read scope for UNQUALIFIED local CLI reads.
*
* Set ONLY by the local CLI's context builder (src/cli.ts makeContext), and
* only when the source resolved via a non-explicit tier (local_path /
* brain_default / sole_non_default / seed_default — NOT --source, NOT
* GBRAIN_SOURCE, NOT a .gbrain-source dotfile). Contains the resolved
* source first, then every other `config.federated = true` source, so an
* unqualified `gbrain search "X"` spans federated sources as
* docs/guides/multi-source-brains.md promises.
*
* Consumed exclusively by `federatedSearchScope` and ONLY when
* `ctx.remote === false` — a remote caller's scope stays governed by
* `ctx.auth.allowedSources` / scalar `ctx.sourceId` (source-isolation
* invariant, fail-closed).
*/
localFederatedSourceIds?: string[];
}
/**
@@ -556,45 +539,6 @@ export function resolveRequestedScope(
return sourceScopeOpts(ctx);
}
/**
* #2561 — source scope for the search-shaped read ops (`search`, `query`).
*
* Delegates to `resolveRequestedScope` (the single trust+grant resolver), then
* widens an UNQUALIFIED trusted-local scalar scope to the CLI-computed
* federated set (`ctx.localFederatedSourceIds`, resolved source first). This is
* what makes `sources add --federated` mean something for local search: a
* federated source participates in unqualified `gbrain search "X"` results.
*
* The expansion NEVER applies when:
* - the caller is not strictly trusted-local (`ctx.remote !== false`) —
* remote scope stays grant-governed (fail-closed source isolation);
* - a per-call `source_id` was passed (explicit wins, including `__all__`);
* - the resolver already produced a federated array (OAuth grant);
* - the CLI resolved the source from an explicit signal (--source / env /
* dotfile) — makeContext leaves `localFederatedSourceIds` unset then.
*
* Deliberately NOT inside `sourceScopeOpts`: code-intel ops collapse a
* multi-element scope to an error (`resolveCodeIntelScope`), and non-search
* reads (get_page, get_links, …) keep their long-standing scalar behavior.
*/
export function federatedSearchScope(
ctx: OperationContext,
sourceIdParam?: string,
): { sourceId?: string; sourceIds?: string[] } {
const scope = resolveRequestedScope(ctx, sourceIdParam);
if (
ctx.remote === false &&
sourceIdParam === undefined &&
scope.sourceId !== undefined &&
scope.sourceIds === undefined &&
ctx.localFederatedSourceIds !== undefined &&
ctx.localFederatedSourceIds.length > 1
) {
return { sourceIds: ctx.localFederatedSourceIds };
}
return scope;
}
/**
* Code-intel adapter for `resolveRequestedScope`. Graph traversal
* (code_callers/code_callees/code_blast/code_flow) is single-source by design —
@@ -1504,8 +1448,7 @@ const search: Operation = {
const queryText = p.query as string;
const limit = (p.limit as number) || 20;
const offset = (p.offset as number) || 0;
// #2561: unqualified trusted-local search spans federated sources.
const scope = federatedSearchScope(ctx);
const scope = sourceScopeOpts(ctx);
// T4/D5 — per-call mode honored ONLY for trusted/local callers so a remote
// OAuth client can't escalate to the costly tokenmax bundle. Local + unknown
@@ -1667,9 +1610,7 @@ const query: Operation = {
// is spread into BOTH the image-similarity searchVector path and the text
// hybridSearch path below, so both honor the same grant.
const sourceIdParam = typeof p.source_id === 'string' ? p.source_id : undefined;
// #2561: unqualified trusted-local query spans federated sources (per-call
// source_id / remote grants still resolve through resolveRequestedScope).
const querySourceScope = federatedSearchScope(ctx, sourceIdParam);
const querySourceScope = resolveRequestedScope(ctx, sourceIdParam);
// v0.27.1: image-similarity branch. Bypasses hybridSearch (which is
// text-only); embeds the image via embedMultimodal and runs a direct
+55 -7
View File
@@ -5565,30 +5565,78 @@ export class PostgresEngine implements BrainEngine {
});
}
/**
* perf (#1694 by @Omerbahari): process-lifetime config cache. A single
* search fires ~85 getConfig() reads (loadConfigWithEngine x2, plus
* mode/cache/intent/rerank/graph-signals resolvers). On a remote pooler
* each read is a round-trip; serial they dominate query latency and can
* push the op handler past cli.ts's 10s disconnect force-exit, truncating
* stdout. First read batch-loads the whole `config` table into this Map
* (inside the same connRetry posture as the per-key read #1603/#1891);
* setConfig/unsetConfig write through. TTL bounds staleness for
* multi-writer processes; GBRAIN_CONFIG_CACHE_TTL_MS=0 disables.
* Only present keys are stored Map.has() distinguishes known-absent.
*/
private _configCache: Map<string, string> | null = null;
private _configCacheLoadedAt = 0;
private _configCacheLoad: Promise<void> | null = null;
private get _configCacheTtlMs(): number {
const raw = process.env.GBRAIN_CONFIG_CACHE_TTL_MS;
if (raw !== undefined) {
const n = parseInt(raw, 10);
if (Number.isFinite(n) && n >= 0) return n;
}
return 30_000;
}
async getConfig(key: string): Promise<string | null> {
// #1603: a transient pooler drop on this read used to throw / fall through
// to defaults silently — which on remote Postgres surfaces as the wrong
// search mode/knobs and empty-stdout queries.
return this.connRetry(async () => {
const rows = await this.sql`SELECT value FROM config WHERE key = ${key}`;
return rows.length > 0 ? (rows[0].value as string) : null;
});
// search mode/knobs and empty-stdout queries. Both the batch load and the
// cache-off per-key read keep the connRetry reconnect posture.
const ttl = this._configCacheTtlMs;
if (ttl === 0) {
return this.connRetry(async () => {
const rows = await this.sql`SELECT value FROM config WHERE key = ${key}`;
return rows.length > 0 ? (rows[0].value as string) : null;
});
}
if (this._configCache === null || Date.now() - this._configCacheLoadedAt >= ttl) {
// Single-flight: concurrent cold reads share one batch load.
this._configCacheLoad ??= this.connRetry(async () => {
const rows = await this.sql`SELECT key, value FROM config` as unknown as
Array<{ key: string; value: string | null }>;
const map = new Map<string, string>();
for (const r of rows) if (r.value != null) map.set(r.key, r.value);
this._configCache = map;
this._configCacheLoadedAt = Date.now();
}).finally(() => {
this._configCacheLoad = null;
});
await this._configCacheLoad;
}
return this._configCache!.has(key) ? this._configCache!.get(key)! : null;
}
async setConfig(key: string, value: string): Promise<void> {
return this.connRetry(async () => {
await this.connRetry(async () => {
await this.sql`
INSERT INTO config (key, value) VALUES (${key}, ${value})
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
`;
});
// Write-through so a long-lived process never serves stale config.
this._configCache?.set(key, value);
}
async unsetConfig(key: string): Promise<number> {
return this.connRetry(async () => {
const count = await this.connRetry(async () => {
const result = await this.sql`DELETE FROM config WHERE key = ${key}` as unknown as { count: number };
return result.count ?? 0;
});
// Write-through: known-absent, so the cache doesn't serve a stale value.
this._configCache?.delete(key);
return count;
}
async listConfigKeys(prefix: string): Promise<string[]> {
-39
View File
@@ -353,45 +353,6 @@ export async function resolveSourceWithTier(
return { source_id: 'default', tier: 'seed_default' };
}
/**
* #2561 — compute the federated read scope for an UNQUALIFIED local CLI call.
*
* `sources add --federated` promises that a `config.federated = true` source
* "participates in unqualified `gbrain search` results"
* (docs/guides/multi-source-brains.md). This helper turns that promise into a
* scope: given the resolved source and WHICH tier resolved it, return
* `[resolvedSource, ...other federated source ids]` — or `undefined` when the
* expansion must not apply:
*
* - explicit tiers (`flag` / `env` / `dotfile`): the user named a source;
* scalar scope stands (that IS the qualified case);
* - no other federated source exists: keep the scalar fast path unchanged.
*
* Archived sources are excluded (same rationale as pickSoleNonDefaultSource);
* the archived column is v34+, so fall back to the un-archived query on older
* brains. Callers put the result on `OperationContext.localFederatedSourceIds`
* — consumed only by `federatedSearchScope` and only when `remote === false`.
*/
export async function localFederatedSourceIds(
engine: BrainEngine,
sourceId: string,
tier: SourceTier,
): Promise<string[] | undefined> {
if (tier === 'flag' || tier === 'env' || tier === 'dotfile') return undefined;
let rows: Array<{ id: string }>;
try {
rows = await engine.executeRaw<{ id: string }>(
`SELECT id FROM sources WHERE config->>'federated' = 'true' AND archived = false ORDER BY id`,
);
} catch {
rows = await engine.executeRaw<{ id: string }>(
`SELECT id FROM sources WHERE config->>'federated' = 'true' ORDER BY id`,
);
}
const ids = [sourceId, ...rows.map((r) => r.id).filter((id) => id !== sourceId)];
return ids.length > 1 ? ids : undefined;
}
/** Exposed for tests. */
export const __testing = {
readDotfileWalk,
+7
View File
@@ -29,6 +29,13 @@ if (existsSync(envPath)) {
}
}
// E2E suites seed/rewrite the config table via raw SQL and expect engine
// reads to see it immediately; disable the process-lifetime config cache
// (#1694) so read semantics match pre-cache behavior. Spawned CLI
// subprocesses inherit this. Cache semantics are pinned by
// test/postgres-engine-config-cache.test.ts.
process.env.GBRAIN_CONFIG_CACHE_TTL_MS ??= '0';
const DATABASE_URL = process.env.DATABASE_URL;
const FIXTURES_DIR = resolve(import.meta.dir, 'fixtures');
-154
View File
@@ -1,154 +0,0 @@
/**
* #2561 — sources.config.federated participates in UNQUALIFIED local CLI
* search/query.
*
* Pre-fix: the local CLI always emitted a scalar `{sourceId}` scope (required
* field, auto-filled 'default'), so a source registered with
* `gbrain sources add --federated` was invisible to an unqualified
* `gbrain search "X"` — contradicting docs/guides/multi-source-brains.md
* ("Source participates in unqualified `gbrain search` results").
*
* Fix: the CLI context builder computes `ctx.localFederatedSourceIds`
* (resolved source + every other federated source) whenever the source
* resolved via a NON-explicit tier; `federatedSearchScope` widens the scalar
* scope to that set for the `search` / `query` ops — trusted-local only
* (`ctx.remote === false`), never for remote callers, never when a per-call
* `source_id` or an explicit --source/env/dotfile was given.
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { localFederatedSourceIds } from '../src/core/source-resolver.ts';
import {
federatedSearchScope,
operations,
type OperationContext,
} from '../src/core/operations.ts';
let engine: PGLiteEngine;
const search = operations.find((o) => o.name === 'search')!;
function ctxOf(overrides: Partial<OperationContext> = {}): OperationContext {
return {
engine: engine as any,
config: {} as any,
logger: console as any,
dryRun: false,
remote: false,
sourceId: 'default',
...overrides,
};
}
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
// Seeded 'default' source is federated=true. Add:
// wiki — federated (must join unqualified search)
// private — NOT federated (must stay invisible unless explicitly named)
// oldnews — federated but archived (must stay excluded)
await engine.executeRaw(
`INSERT INTO sources (id, name, local_path, config) VALUES ('wiki', 'wiki', '/tmp/wiki', '{"federated": true}'::jsonb)`,
);
await engine.executeRaw(
`INSERT INTO sources (id, name, local_path, config) VALUES ('private', 'private', '/tmp/private', '{}'::jsonb)`,
);
await engine.executeRaw(
`INSERT INTO sources (id, name, local_path, config, archived) VALUES ('oldnews', 'oldnews', '/tmp/oldnews', '{"federated": true}'::jsonb, true)`,
);
const pages: Array<[slug: string, sourceId: string, where: string]> = [
['notes/home', 'default', 'default'],
['wiki/topic', 'wiki', 'wiki'],
['private/topic', 'private', 'private'],
['old/topic', 'oldnews', 'oldnews'],
];
for (const [slug, sourceId, where] of pages) {
await engine.putPage(slug, {
type: 'note', title: `Topic in ${where}`, compiled_truth: `the zebra telescope in ${where}`, frontmatter: {},
}, { sourceId });
await engine.upsertChunks(slug, [
{ chunk_index: 0, chunk_text: `the zebra telescope in ${where}`, chunk_source: 'compiled_truth' },
], { sourceId });
}
// Keyword-only search path: no embedding provider needed in tests.
await engine.setConfig('search.mcp_keyword_only', 'true');
}, 60_000);
afterAll(async () => {
if (engine) await engine.disconnect();
}, 60_000);
describe('localFederatedSourceIds — CLI-side scope computation', () => {
test('non-explicit tier: resolved source first, then other federated, archived excluded', async () => {
expect(await localFederatedSourceIds(engine, 'default', 'seed_default')).toEqual(['default', 'wiki']);
});
test('non-federated resolved source still joins its own scope', async () => {
expect(await localFederatedSourceIds(engine, 'private', 'brain_default')).toEqual(['private', 'default', 'wiki']);
});
test('explicit tiers (--source / env / dotfile) never expand', async () => {
expect(await localFederatedSourceIds(engine, 'default', 'flag')).toBeUndefined();
expect(await localFederatedSourceIds(engine, 'default', 'env')).toBeUndefined();
expect(await localFederatedSourceIds(engine, 'default', 'dotfile')).toBeUndefined();
});
test('single federated source (the resolved one) keeps the scalar fast path', async () => {
const solo = { executeRaw: async () => [{ id: 'default' }] } as any;
expect(await localFederatedSourceIds(solo, 'default', 'seed_default')).toBeUndefined();
});
});
describe('federatedSearchScope — trust + explicitness matrix', () => {
test('trusted local + unqualified widens to the federated set', () => {
const ctx = ctxOf({ localFederatedSourceIds: ['default', 'wiki'] });
expect(federatedSearchScope(ctx)).toEqual({ sourceIds: ['default', 'wiki'] });
});
test('remote caller NEVER widens (fail-closed), even if the field is set', () => {
const ctx = ctxOf({ remote: true, localFederatedSourceIds: ['default', 'wiki'] });
expect(federatedSearchScope(ctx)).toEqual({ sourceId: 'default' });
});
test('per-call source_id wins over the federated set', () => {
const ctx = ctxOf({ localFederatedSourceIds: ['default', 'wiki'] });
expect(federatedSearchScope(ctx, 'wiki')).toEqual({ sourceId: 'wiki' });
});
test('per-call __all__ keeps the whole-brain semantics for trusted local', () => {
const ctx = ctxOf({ localFederatedSourceIds: ['default', 'wiki'] });
expect(federatedSearchScope(ctx, '__all__')).toEqual({});
});
test('a federated OAuth grant wins over the local set', () => {
const ctx = ctxOf({
localFederatedSourceIds: ['default', 'wiki'],
auth: { allowedSources: ['a', 'b'] } as OperationContext['auth'],
});
expect(federatedSearchScope(ctx)).toEqual({ sourceIds: ['a', 'b'] });
});
test('no local federated set → unchanged scalar scope', () => {
expect(federatedSearchScope(ctxOf())).toEqual({ sourceId: 'default' });
});
});
describe('search op — unqualified local search spans federated sources', () => {
test('federated source results appear; non-federated + archived stay invisible', async () => {
const ctx = ctxOf({
localFederatedSourceIds: await localFederatedSourceIds(engine, 'default', 'seed_default'),
});
const results = (await search.handler(ctx, { query: 'zebra telescope' })) as Array<{ slug: string }>;
const slugs = results.map((r) => r.slug);
expect(slugs).toContain('notes/home');
expect(slugs).toContain('wiki/topic'); // pre-#2561 this was missing
expect(slugs).not.toContain('private/topic');
expect(slugs).not.toContain('old/topic');
});
test('explicit source resolution (no federated set on ctx) stays single-source', async () => {
const results = (await search.handler(ctxOf(), { query: 'zebra telescope' })) as Array<{ slug: string }>;
const slugs = results.map((r) => r.slug);
expect(slugs).toEqual(['notes/home']);
});
});
+112
View File
@@ -0,0 +1,112 @@
/**
* Process-lifetime config cache (#1694 takeover, by @Omerbahari).
*
* A single search fires ~85 getConfig() reads; on a remote pooler each is a
* round-trip. The first read now batch-loads the whole `config` table into a
* Map; setConfig/unsetConfig write through; TTL bounds multi-writer
* staleness; GBRAIN_CONFIG_CACHE_TTL_MS=0 restores per-key reads.
*
* Pure: stubs `_sql` with a call-counting fake; no real DB.
*/
import { describe, it, expect } from 'bun:test';
import { PostgresEngine } from '../src/core/postgres-engine.ts';
import { withEnv } from './helpers/with-env.ts';
const FAST_RETRY = { maxRetries: 3, delayMs: 1, delayMaxMs: 1, jitter: 'none' as const };
/** Engine whose `sql` records every query's template strings and returns `rows`. */
function makeEngine(rows: unknown[]) {
const e = new PostgresEngine();
const calls: string[] = [];
(e as unknown as { _connectionStyle: string })._connectionStyle = 'instance';
(e as unknown as { _bulkRetryOptsCache: unknown })._bulkRetryOptsCache = FAST_RETRY;
(e as unknown as { _sql: unknown })._sql = (strings: TemplateStringsArray) => {
calls.push(strings.join('?'));
return Promise.resolve(rows);
};
return { engine: e, calls };
}
/** Run `fn` with GBRAIN_CONFIG_CACHE_TTL_MS set (or cleared when undefined). */
const withTtl = (ttl: string | undefined, fn: () => Promise<void>) =>
withEnv({ GBRAIN_CONFIG_CACHE_TTL_MS: ttl }, fn);
describe('PostgresEngine config cache (#1694)', () => {
it('batch-loads once and serves repeat reads from the cache', () => withTtl(undefined, async () => {
const { engine, calls } = makeEngine([
{ key: 'search.mode', value: 'balanced' },
{ key: 'embedding_multimodal', value: 'true' },
]);
expect(await engine.getConfig('search.mode')).toBe('balanced');
expect(await engine.getConfig('embedding_multimodal')).toBe('true');
expect(await engine.getConfig('search.mode')).toBe('balanced');
// One SELECT total — this is the whole point of the fix.
expect(calls.length).toBe(1);
expect(calls[0]).toContain('SELECT key, value FROM config');
}));
it('returns null for a known-absent key without an extra round-trip', () => withTtl(undefined, async () => {
const { engine, calls } = makeEngine([{ key: 'a', value: '1' }]);
expect(await engine.getConfig('missing.key')).toBeNull();
expect(await engine.getConfig('missing.key')).toBeNull();
expect(calls.length).toBe(1);
}));
it('setConfig writes through so subsequent reads see the new value', () => withTtl(undefined, async () => {
const { engine, calls } = makeEngine([{ key: 'k', value: 'old' }]);
expect(await engine.getConfig('k')).toBe('old');
await engine.setConfig('k', 'new');
expect(await engine.getConfig('k')).toBe('new');
expect(calls.length).toBe(2); // batch load + upsert; no re-read
}));
it('unsetConfig writes through so subsequent reads see absence', () => withTtl(undefined, async () => {
const { engine } = makeEngine([{ key: 'k', value: 'v' }]);
expect(await engine.getConfig('k')).toBe('v');
await engine.unsetConfig('k');
expect(await engine.getConfig('k')).toBeNull();
}));
it('concurrent cold reads share a single batch load (single-flight)', () => withTtl(undefined, async () => {
const { engine, calls } = makeEngine([{ key: 'k', value: 'v' }]);
const [a, b, c] = await Promise.all([
engine.getConfig('k'),
engine.getConfig('k'),
engine.getConfig('other'),
]);
expect([a, b, c]).toEqual(['v', 'v', null]);
expect(calls.length).toBe(1);
}));
it('GBRAIN_CONFIG_CACHE_TTL_MS=0 disables the cache (per-key reads)', () => withTtl('0', async () => {
const { engine, calls } = makeEngine([{ value: 'v' }]);
expect(await engine.getConfig('k')).toBe('v');
expect(await engine.getConfig('k')).toBe('v');
expect(calls.length).toBe(2);
expect(calls[0]).toContain('SELECT value FROM config WHERE key =');
}));
it('an expired TTL reloads from the database', () => withTtl('1', async () => {
const { engine, calls } = makeEngine([{ key: 'k', value: 'v' }]);
expect(await engine.getConfig('k')).toBe('v');
await new Promise((r) => setTimeout(r, 5));
expect(await engine.getConfig('k')).toBe('v');
expect(calls.length).toBe(2); // two batch loads
}));
it('the batch load keeps the connRetry reconnect posture (#1603/#1891)', () => withTtl(undefined, async () => {
const e = new PostgresEngine();
(e as unknown as { _connectionStyle: string })._connectionStyle = 'instance';
(e as unknown as { _sql: unknown })._sql = null; // torn-down pool → retryable
(e as unknown as { _bulkRetryOptsCache: unknown })._bulkRetryOptsCache = FAST_RETRY;
let reconnects = 0;
(e as unknown as { reconnect: () => Promise<void> }).reconnect = async () => {
reconnects++;
(e as unknown as { _sql: unknown })._sql = () =>
Promise.resolve([{ key: 'k', value: 'v' }]);
};
expect(await e.getConfig('k')).toBe('v');
expect(reconnects).toBe(1);
}));
});
@@ -43,7 +43,9 @@ function makeTornDownEngine(poolResult: unknown): { engine: PostgresEngine; reco
describe('PostgresEngine non-batch config accessors self-heal (PR #1891 takeover)', () => {
it('getConfig reconnects + retries a null instance pool, then returns the value', async () => {
const { engine, reconnects } = makeTornDownEngine([{ value: 'live-value' }]);
// Rows carry `key` too: getConfig's default cached path batch-loads
// `SELECT key, value FROM config` (#1694) through the same connRetry.
const { engine, reconnects } = makeTornDownEngine([{ key: 'some.key', value: 'live-value' }]);
expect(await engine.getConfig('some.key')).toBe('live-value');
expect(reconnects()).toBe(1); // exactly one reconnect closed the gap
});