mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* chore(test): preload gateway to OpenAI/1536 so 1536-dim test fixtures keep working The v0.37 fix wave changes the canonical gateway defaults to zeroentropyai:zembed-1 / 1280 (matching what v0.36 already chose as the system default). 20+ test files have hardcoded new Float32Array(1536) fixtures that match the OLD schema default. Without this preload, those tests fail with a vector-dim-mismatch on insert. The preload is gateway-only — it doesn't change which model gbrain ships to production users. Tests that want the new ZE/1280 defaults call configureGateway() explicitly in their own beforeAll. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ai): canonical embedding defaults + sweep across schema/engines/registry Closes the v0.36 defaults drift bug class. The gateway shipped zeroentropyai:zembed-1 / 1280 as the system default in v0.36 but eight other places kept hardcoding 1536 / text-embedding-3-large. Fresh gbrain init --pglite sized the column to 1536, the embed pipeline used ZE/1280, and every page failed with dim mismatch. - New src/core/ai/defaults.ts leaf module is the canonical source for DEFAULT_EMBEDDING_MODEL / DEFAULT_EMBEDDING_DIMENSIONS. Schema and registry helpers import from this lean module instead of pulling the full gateway (which loads every provider SDK). - src/core/ai/gateway.ts re-exports the constants for back-compat. - src/core/pglite-schema.ts getPGLiteSchema() defaults track gateway. - src/core/postgres-engine.ts getPostgresSchema() default args track gateway (same drift on the Postgres path — codex round 1 CDX-1). - Both engine.initSchema() fallbacks track gateway constants (no more stale OpenAI/1536 catch-block defaults). - Schema seed stops stripping the provider prefix; full provider:model is stored in the DB config table (codex round 1 CDX-4). - Chunk-row INSERT defaults track gateway (codex round 2 CDX2-4 — pglite-engine:1611 + postgres-engine:1647 were production write sites previously hardcoded to text-embedding-3-large). - src/core/search/embedding-column.ts loadRegistry + isCacheSafe gain the cfg > gateway > DEFAULT resolution chain (codex round 2 CDX2-3). The gateway tier matters because callers that configure the gateway (init paths, tests, programmatic SDK) expect the registry to mirror that state when cfg doesn't have an explicit embedding_model. Tests: - schema-templating: default expectation flips to ZE/1280 (v0.37 truth). - embedding-dim-check: 3 new engine-kind branching cases + updated fresh-brain expectation (under legacy preload). - embedding-column: registry + isCacheSafe expectations match new chain. - v0_28_5-fix-wave E2E: engineKind required arg propagated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(init+config+cli): always-configure gateway, file-only loader, honest config-set, sync/reinit help Closes the "fresh init doesn't work + config-set silently lies" bug class end-to-end. Six related changes that ship together because the file-plane/DB-plane contract only holds when init paths, config-set, the gateway env mapping, and the recipe text all agree. Lane B (init paths): - initPGLite, initPostgres, initMigrateOnly always configureGateway() before engine.initSchema(). Pre-fix the call was gated on flags, so bare `gbrain init --pglite` left the gateway unconfigured and the engine fell through to stale OpenAI/1536 defaults instead of the ZE/1280 the gateway would have resolved. - New configureGatewayWithMergedPrecedence() helper applies the locked precedence chain `CLI > env > existing file > gateway internal`. - printResolvedAIChoice() shows the resolved model/dim at init time + surfaces a ZE setup hint inline when the API key is missing. - B.4: saveConfig merge uses loadConfigFileOnly() so transient env state (DATABASE_URL, etc.) never poisons ~/.gbrain/config.json (codex round 2 CDX-5). - B.5: extend the v0.28.5 dim-mismatch detector so it fires when the gateway-resolved dim differs from the existing column, not only when --embedding-dimensions is explicit (codex round 2 CDX-6). Lane C (config plane): - New `loadConfigFileOnly()` reads ~/.gbrain/config.json only — no env merge, no DATABASE_URL inference. Safe write-back source for init. - GBrainConfig gains `zeroentropy_api_key?: string`. loadConfig merges process.env.ZEROENTROPY_API_KEY. buildGatewayConfig at cli.ts:1401 maps it into env.ZEROENTROPY_API_KEY so ZE recipes finally see it (codex round 2 CDX2-5+6 — the v1 fix landed in the wrong file). - `gbrain config set embedding_model` and `... embedding_dimensions` refuse unconditionally and print a paste-ready wipe-and-reinit recipe. No --force escape (codex round 2 CDX2-13). - migrate-engine.ts adds a contract comment at the DB-plane write site documenting "DB stores schema-applied metadata; file plane is canonical for runtime gateway config" + preserves the existing file-plane config across engine migration. Lane D.1 (recipe text): - embeddingMismatchMessage() takes an `engineKind` arg. PGLite branch emits a wipe-and-reinit recipe using gbrainPath('brain.pglite') or the caller's databasePath override. Postgres branch keeps the SQL ALTER recipe. - The PGLite recipe recommends `gbrain reinit-pglite` (new sugar command below) as the one-line path before falling back to the by-hand mv + init + sync sequence. Lane D.4 (sync help dispatch): - `sync` and `reinit-pglite` added to CLI_ONLY_SELF_HELP so their own --help branches reach the user (pre-fix the generic short-circuit fired first and the dedicated usage was unreachable; codex round 2 CDX2-12). - `gbrain sync --help` short-circuits BEFORE engine bind so users on a fresh tmpdir (no config) can read the help without hitting no-such-config errors. Sugar: - New `gbrain reinit-pglite --embedding-model X --embedding-dimensions N` wraps the wipe + init + sync dance into one command. Backs up the brain to <path>.bak. TTY confirmation unless --yes. --no-sync to defer the resync. --json for scripts. Tests: - test/cli.test.ts sync-help test rewritten for the new per-command-usage output (lists --no-embed which is the v0.37 user-visible flag the wave wanted to surface). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(embed+sync): pre-flight dim-mismatch guard + sync hint at both catch sites embedding-pipeline error UX. Pre-fix, a fresh-install dim mismatch produced raw Postgres "expected N dimensions, not M" errors page after page, surfacing only after the worker pool drained the entire corpus. Sync swallowed embed errors at TWO catch sites and never surfaced the recovery recipe. embed.ts: - New `EmbeddingDimMismatchError` tagged class with the paste-ready recipe baked in. - `runEmbedCore` pre-flights via `readContentChunksEmbeddingDim` + gateway.getEmbeddingDimensions() before the worker pool spins up. On mismatch, throws the typed error which the CLI wrapper catches and prints. Dry-run skips the check (no embed risk). - Catches the headline fresh-install bug class at first call instead of letting it hammer N parallel API calls into dim-rejected inserts. sync.ts: - Both embed catches at sync.ts:990 (incremental) and sync.ts:1129 (first-sync) detect EmbeddingDimMismatchError and surface the recipe + a `--no-embed` tip on stderr (codex round 2 CDX2-8: incremental path was previously silent; only the first-sync path was flagged). - Non-mismatch embed failures still stay best-effort (rate limits, transient network) — those shouldn't break sync. - Sync calls runEmbedCore directly instead of runEmbed (which calls process.exit on error and bypasses sync's catch). - Sync gets a proper --help block listing every meaningful flag: --no-embed, --workers, --source, --skip-failed, --retry-failed, --watch, --interval, --no-pull, --all, --json, --yes, --dry-run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(doctor): read gateway for schema-sizing checks + provider-aware key lookup Doctor's embedding checks were reading the DB config table for embedding_model / embedding_dimensions / zeroentropy_api_key. Post v0.37 the file plane is canonical (the DB plane is schema-applied metadata, not runtime gateway config) so those reads produced stale verdicts on fresh installs whose DB row hadn't been written. - checkEmbeddingWidthConsistency reads gateway.getEmbeddingDimensions() and gateway.getEmbeddingModel() instead of engine.getConfig(...). Reuses readContentChunksEmbeddingDim from the same shared helper init + embed use. On mismatch, the fix hint threads engineKind + databasePath into the new branched recipe (codex round 1 CDX-8 + Lane E.1/E.2). - checkZeEmbeddingHealth reads gateway for the model + loadConfigFileOnly for the key. Fires when (a) resolved model starts with zeroentropyai: AND (b) ZEROENTROPY_API_KEY is unset in env AND (c) file plane has no zeroentropy_api_key (codex round 2 CDX2-10). - loadRecommendationContext reads gateway for both fields and recognizes the ZE key alongside OpenAI/Anthropic in the hasEmbeddingApiKey check, so brains on ZE no longer look "healthy" just because OPENAI_API_KEY happens to be set (codex round 2 CDX2-11). Tests rewritten for the gateway-source-of-truth contract via configureGateway() in beforeAll. Added a "gateway unconfigured: skips with ok" case so doctor doesn't false-warn on cold-boot brains. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test+docs(v0.37): fix-wave unit coverage + PGLite-first migration recipe + TODOS Lands the v0.37 PGLite fresh-install fix wave's structural tests and the user-facing migration recipe overhaul. test/v0_37_fix_wave.test.ts (new): 22 unit cases pinning the lanes: - Lane A: defaults module exports, getPGLiteSchema/getPostgresSchema default-args, registry + isCacheSafe under the `cfg > gateway > DEFAULT` chain (both gateway-set and gateway-reset branches). - Lane B: loadConfigFileOnly env isolation + DATABASE_URL inference refusal + null-on-missing. - Lane C.3: buildGatewayConfig maps zeroentropy_api_key + process.env wins over config (operator escape hatch contract). - Lane D.2: EmbeddingDimMismatchError shape + tag. - Lane D.4: structural assertion that `sync` is in CLI_ONLY_SELF_HELP. - Deferred-TODO ship: reinit-pglite is registered correctly + embeddingMismatchMessage PGLite branch recommends it. docs/embedding-migrations.md: PGLite section moved to top (the default install). The recommended path is `gbrain reinit-pglite` one-liner; the by-hand mv + init + sync sequence stays as the fallback recipe. Postgres SQL ALTER recipe preserved. New section on `gbrain config set` refusal explains the file-plane vs DB-plane contract so users don't follow stale documentation. TODOS.md: 4 deferred follow-ups filed with concrete file pointers: - gbrain embed --try-fallback (provider auto-switch with consent gate) - Full plane unification for non-schema-sizing fields - Worker-pool shared AbortController for mid-run dim drift - Cleanup of back-compat constants in src/core/embedding.ts Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(v0.37): fill behavior gaps + headline fresh-install E2E The structural fix-wave tests in test/v0_37_fix_wave.test.ts pin lane-level invariants (exports, registry chain, signature shapes). The audit found 10+ END-TO-END behaviors that the structural tests didn't actually reach. This file fills the highest-leverage gaps. Unit coverage (test/v0_37_gap_fill.test.ts, 12 cases): - Lane A.7: chunk-row INSERT default tracks DEFAULT_EMBEDDING_MODEL constant (pre-fix this was the literal 'text-embedding-3-large' at pglite-engine.ts:1611 + postgres-engine.ts:1647 — production write sites that were never directly tested; codex round 2 CDX2-4). - Lane A.8: schema seed stores full provider:model in DB config (pre-fix the .split(':') strip dropped the prefix; codex round 1 CDX-4). Asserts a fresh ZE init stores `zeroentropyai:zembed-1` in the config table, not bare `zembed-1`. - Lane B precedence: explicit CLI > env > existing file > default test (codex round 2 CDX2-7 contradiction guard). - Lane C.3 env merge: process.env.ZEROENTROPY_API_KEY threads through loadConfig → cfg.zeroentropy_api_key; loadConfigFileOnly does NOT. - Lane D.2 end-to-end: schema=1536 + gateway=1280 → EmbeddingDimMismatchError fires AND the embed transport is never called (the whole point of pre-flight). Plus dry-run skips the check. - Lane D.3 source-text grep: both sync.ts catch sites detect the typed error + the `--no-embed` tip is present (CDX2-8). - Lane E.4 source-text grep: loadRecommendationContext is provider-aware (reads gateway + branches on ZE/OpenAI key). - reinit-pglite contract: refuses on non-PGLite engines + refuses when required flags are missing. E2E (test/e2e/fresh-install-pglite.test.ts, 2 cases): - Bare `gbrain init --pglite` produces a `vector(1280)` schema, prints the resolved choice, persists defaults to config.json — the headline scenario that v0.37 ships to fix. - init → seed page → embed end-to-end: chunks have non-null embeddings; no dim mismatch despite the wave's defaults change. Both E2E cases are IN-PROCESS (per CDX2-12: CLI-subprocess E2E can't inherit `__setEmbedTransportForTests`). They run with stubbed transport returning synthetic 1280-dim vectors so we never hit real provider APIs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(v0.37): defensive gateway restore in reinit-pglite describe block Adds an afterAll that restores the gateway to OpenAI/1536 (matching the bunfig preload) at the end of the reinit-pglite describe. Belt-and- suspenders: earlier describe blocks in this file already restore, but if the reinit-pglite tests ever start mutating the gateway in the future, this protects downstream test files in the same bun-test shard from inheriting a non-default state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.37.10.0) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: scrub stale config-set recipes for embedding model (v0.37.10.0) README + topologies + embedding-providers were still pointing users at `gbrain config set embedding_model X` / `embedding_dimensions N`. As of v0.37.10.0 those writes are refused — the schema column has to resize alongside the config. Point at `gbrain reinit-pglite` (PGLite) and the SQL recipe in `docs/embedding-migrations.md` (Postgres) instead. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: bump version to v0.37.11.0 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: quarantine v0.37 fix-wave tests to .serial.test.ts CI's `check:test-isolation` lint flagged R1 violations (direct `process.env.GBRAIN_HOME` mutation) in both new fix-wave test files. Per the documented quarantine pattern in CLAUDE.md, rename to `*.serial.test.ts` instead of refactoring through `withEnv()` — both files use beforeEach/afterEach env wiring that's already serial-safe. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
321 lines
14 KiB
TypeScript
321 lines
14 KiB
TypeScript
/**
|
|
* v0.37 fix wave — fresh-install PGLite embedding setup.
|
|
*
|
|
* Covers the multi-bug-class fix surfaced by the user's 9-bug report and
|
|
* the two codex outside-voice review rounds (26 findings folded). Each
|
|
* test pins a specific finding so future regressions surface fast.
|
|
*
|
|
* Test framework: bun:test. Hermetic — no network, no DATABASE_URL needed.
|
|
*/
|
|
|
|
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
|
|
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync } from 'fs';
|
|
import { tmpdir } from 'os';
|
|
import { join } from 'path';
|
|
|
|
// Lane A — defaults sweep
|
|
describe('v0.37 Lane A — defaults sweep', () => {
|
|
test('A.0: gateway re-exports DEFAULT_EMBEDDING_MODEL + DEFAULT_EMBEDDING_DIMENSIONS', async () => {
|
|
// CDX2-1: these were file-private const; Lane A consumers (schema
|
|
// helpers, registry) need them exported. Importing here is the test.
|
|
const { DEFAULT_EMBEDDING_MODEL, DEFAULT_EMBEDDING_DIMENSIONS } = await import('../src/core/ai/gateway.ts');
|
|
expect(DEFAULT_EMBEDDING_MODEL).toBe('zeroentropyai:zembed-1');
|
|
expect(DEFAULT_EMBEDDING_DIMENSIONS).toBe(1280);
|
|
});
|
|
|
|
test('A.0: ai/defaults.ts is the canonical source (leaf module, no SDK pulls)', async () => {
|
|
const defaults = await import('../src/core/ai/defaults.ts');
|
|
expect(defaults.DEFAULT_EMBEDDING_MODEL).toBe('zeroentropyai:zembed-1');
|
|
expect(defaults.DEFAULT_EMBEDDING_DIMENSIONS).toBe(1280);
|
|
});
|
|
|
|
// T-11 / T-12: registry + schema defaults track gateway constants.
|
|
test('A.1: getPGLiteSchema() default-args produce a vector(1280) column', async () => {
|
|
const { getPGLiteSchema } = await import('../src/core/pglite-schema.ts');
|
|
const sql = getPGLiteSchema(); // no args — uses defaults
|
|
expect(sql).toContain('vector(1280)');
|
|
expect(sql).not.toContain('vector(1536)');
|
|
});
|
|
|
|
test('A.2: getPostgresSchema() default-args produce a vector(1280) column', async () => {
|
|
const { getPostgresSchema } = await import('../src/core/postgres-engine.ts');
|
|
const sql = getPostgresSchema();
|
|
expect(sql).toContain('vector(1280)');
|
|
expect(sql).not.toContain('vector(1536)');
|
|
});
|
|
|
|
test('A.2: getPostgresSchema() with explicit args still routes the override', async () => {
|
|
const { getPostgresSchema } = await import('../src/core/postgres-engine.ts');
|
|
const sql = getPostgresSchema(2048, 'voyage:voyage-4-large');
|
|
expect(sql).toContain('vector(2048)');
|
|
expect(sql).not.toContain('vector(1280)');
|
|
expect(sql).toContain('voyage:voyage-4-large');
|
|
});
|
|
|
|
test('A.5: embedding-column registry builtin defaults to ZE/1280 on empty config + gateway', async () => {
|
|
// The registry's resolution chain is cfg > gateway > DEFAULT. With
|
|
// no cfg AND no gateway, it should fall through to the canonical
|
|
// default (ZE/1280). Reset gateway first to exercise that path.
|
|
const { resetGateway } = await import('../src/core/ai/gateway.ts');
|
|
const { getEmbeddingColumnRegistry } = await import('../src/core/search/embedding-column.ts');
|
|
resetGateway();
|
|
try {
|
|
const reg = getEmbeddingColumnRegistry({ engine: 'pglite' } as any);
|
|
expect(reg['embedding']).toBeDefined();
|
|
expect(reg['embedding'].provider).toBe('zeroentropyai:zembed-1');
|
|
expect(reg['embedding'].dimensions).toBe(1280);
|
|
} finally {
|
|
// Re-apply legacy preload defaults so the rest of the file's tests
|
|
// (and subsequent files in this shard) see a configured gateway.
|
|
const { configureGateway } = await import('../src/core/ai/gateway.ts');
|
|
configureGateway({
|
|
embedding_model: 'openai:text-embedding-3-large',
|
|
embedding_dimensions: 1536,
|
|
env: { ...process.env },
|
|
});
|
|
}
|
|
});
|
|
|
|
test('A.5: registry tracks gateway when cfg is empty (gateway as fallback)', async () => {
|
|
// The new "gateway tier" of the resolution chain. Tests configure
|
|
// the gateway to OpenAI/1536 (via preload); registry reflects that
|
|
// even with empty cfg. Lets test fixtures avoid duplicating the
|
|
// model config in two places.
|
|
const { getEmbeddingColumnRegistry } = await import('../src/core/search/embedding-column.ts');
|
|
const reg = getEmbeddingColumnRegistry({ engine: 'pglite' } as any);
|
|
expect(reg['embedding']).toBeDefined();
|
|
expect(reg['embedding'].provider).toBe('openai:text-embedding-3-large');
|
|
expect(reg['embedding'].dimensions).toBe(1536);
|
|
});
|
|
|
|
test('A.6: isCacheSafe baselines against gateway state (not stale constants)', async () => {
|
|
// With the preload setting gateway to OpenAI/1536, isCacheSafe
|
|
// considers a 1536/OpenAI resolved column safe even when cfg has
|
|
// no embedding_model.
|
|
const { isCacheSafe } = await import('../src/core/search/embedding-column.ts');
|
|
const resolved1536 = {
|
|
name: 'embedding',
|
|
dimensions: 1536,
|
|
embeddingModel: 'openai:text-embedding-3-large',
|
|
type: 'vector' as const,
|
|
provider: 'openai:text-embedding-3-large',
|
|
};
|
|
expect(isCacheSafe(resolved1536 as any, { engine: 'pglite' } as any)).toBe(true);
|
|
|
|
// Wrong dim → unsafe.
|
|
const wrongDim = { ...resolved1536, dimensions: 1280 };
|
|
expect(isCacheSafe(wrongDim as any, { engine: 'pglite' } as any)).toBe(false);
|
|
|
|
// Wrong model → unsafe.
|
|
const wrongModel = { ...resolved1536, embeddingModel: 'voyage:voyage-3-large' };
|
|
expect(isCacheSafe(wrongModel as any, { engine: 'pglite' } as any)).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Lane B — init paths + B.4 file-plane merge
|
|
describe('v0.37 Lane B — init paths', () => {
|
|
let tmpHome: string;
|
|
let origHome: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
tmpHome = mkdtempSync(join(tmpdir(), 'gbrain-v37-test-'));
|
|
origHome = process.env.GBRAIN_HOME;
|
|
process.env.GBRAIN_HOME = tmpHome;
|
|
});
|
|
|
|
afterAll(() => {
|
|
if (origHome === undefined) delete process.env.GBRAIN_HOME;
|
|
else process.env.GBRAIN_HOME = origHome;
|
|
});
|
|
|
|
test('B.4 / T-3: loadConfigFileOnly ignores env overrides', async () => {
|
|
const cfgPath = join(tmpHome, '.gbrain', 'config.json');
|
|
require('fs').mkdirSync(join(tmpHome, '.gbrain'), { recursive: true });
|
|
writeFileSync(cfgPath, JSON.stringify({
|
|
engine: 'pglite',
|
|
database_path: '/file/plane/path',
|
|
embedding_model: 'openai:text-embedding-3-large',
|
|
embedding_dimensions: 1536,
|
|
}));
|
|
|
|
process.env.GBRAIN_EMBEDDING_MODEL = 'voyage:voyage-3-large';
|
|
process.env.GBRAIN_EMBEDDING_DIMENSIONS = '2048';
|
|
process.env.OPENAI_API_KEY = 'sk-from-env';
|
|
|
|
// Force re-import to pick up env state (the module-level resolver in
|
|
// config.ts reads process.env at call time, so this is safe).
|
|
delete require.cache[require.resolve('../src/core/config.ts')];
|
|
const { loadConfigFileOnly, loadConfig } = await import('../src/core/config.ts');
|
|
|
|
const fileOnly = loadConfigFileOnly();
|
|
expect(fileOnly?.embedding_model).toBe('openai:text-embedding-3-large');
|
|
expect(fileOnly?.embedding_dimensions).toBe(1536);
|
|
// CDX-5 regression: env keys must NOT leak into file-only loader.
|
|
expect(fileOnly?.openai_api_key).toBeUndefined();
|
|
|
|
// Control: loadConfig() DOES merge env.
|
|
const merged = loadConfig();
|
|
expect(merged?.embedding_model).toBe('voyage:voyage-3-large');
|
|
expect(merged?.embedding_dimensions).toBe(2048);
|
|
expect(merged?.openai_api_key).toBe('sk-from-env');
|
|
|
|
delete process.env.GBRAIN_EMBEDDING_MODEL;
|
|
delete process.env.GBRAIN_EMBEDDING_DIMENSIONS;
|
|
delete process.env.OPENAI_API_KEY;
|
|
});
|
|
|
|
test('B.4 / CDX-5: loadConfigFileOnly does NOT infer engine from DATABASE_URL', async () => {
|
|
const cfgPath = join(tmpHome, '.gbrain', 'config.json');
|
|
require('fs').mkdirSync(join(tmpHome, '.gbrain'), { recursive: true });
|
|
writeFileSync(cfgPath, JSON.stringify({
|
|
engine: 'pglite',
|
|
database_path: '/pglite/path',
|
|
}));
|
|
|
|
process.env.DATABASE_URL = 'postgres://transient@host/db';
|
|
delete require.cache[require.resolve('../src/core/config.ts')];
|
|
const { loadConfigFileOnly, loadConfig } = await import('../src/core/config.ts');
|
|
|
|
const fileOnly = loadConfigFileOnly();
|
|
expect(fileOnly?.engine).toBe('pglite');
|
|
expect(fileOnly?.database_path).toBe('/pglite/path');
|
|
expect(fileOnly?.database_url).toBeUndefined();
|
|
|
|
// Control: loadConfig() WOULD infer postgres from the env URL.
|
|
const merged = loadConfig();
|
|
expect(merged?.engine).toBe('postgres');
|
|
|
|
delete process.env.DATABASE_URL;
|
|
});
|
|
|
|
test('B.4: loadConfigFileOnly returns null when no file exists', async () => {
|
|
delete require.cache[require.resolve('../src/core/config.ts')];
|
|
const { loadConfigFileOnly } = await import('../src/core/config.ts');
|
|
expect(loadConfigFileOnly()).toBeNull();
|
|
});
|
|
});
|
|
|
|
// Lane C.3 — ZE key plumbing
|
|
describe('v0.37 Lane C.3 — ZE key reaches buildGatewayConfig', () => {
|
|
test('CDX2-5+6: buildGatewayConfig maps zeroentropy_api_key into env dict', async () => {
|
|
// process.env wins over config (intentional — operator escape hatch).
|
|
// Unset the env key so the test exercises the config-only path.
|
|
const savedZe = process.env.ZEROENTROPY_API_KEY;
|
|
const savedOai = process.env.OPENAI_API_KEY;
|
|
const savedAnth = process.env.ANTHROPIC_API_KEY;
|
|
delete process.env.ZEROENTROPY_API_KEY;
|
|
delete process.env.OPENAI_API_KEY;
|
|
delete process.env.ANTHROPIC_API_KEY;
|
|
try {
|
|
const { buildGatewayConfig } = await import('../src/cli.ts');
|
|
const cfg = {
|
|
engine: 'pglite' as const,
|
|
zeroentropy_api_key: 'test-ze-key',
|
|
openai_api_key: 'test-oai',
|
|
anthropic_api_key: 'test-anth',
|
|
};
|
|
const gwCfg = buildGatewayConfig(cfg as any);
|
|
expect(gwCfg.env?.ZEROENTROPY_API_KEY).toBe('test-ze-key');
|
|
// Regression on the existing two keys.
|
|
expect(gwCfg.env?.OPENAI_API_KEY).toBe('test-oai');
|
|
expect(gwCfg.env?.ANTHROPIC_API_KEY).toBe('test-anth');
|
|
} finally {
|
|
if (savedZe !== undefined) process.env.ZEROENTROPY_API_KEY = savedZe;
|
|
if (savedOai !== undefined) process.env.OPENAI_API_KEY = savedOai;
|
|
if (savedAnth !== undefined) process.env.ANTHROPIC_API_KEY = savedAnth;
|
|
}
|
|
});
|
|
|
|
test('CDX2-5+6: process.env wins over config (operator escape hatch contract)', async () => {
|
|
const saved = process.env.ZEROENTROPY_API_KEY;
|
|
process.env.ZEROENTROPY_API_KEY = 'env-wins-key';
|
|
try {
|
|
const { buildGatewayConfig } = await import('../src/cli.ts');
|
|
const cfg = { engine: 'pglite' as const, zeroentropy_api_key: 'file-key' };
|
|
const gwCfg = buildGatewayConfig(cfg as any);
|
|
expect(gwCfg.env?.ZEROENTROPY_API_KEY).toBe('env-wins-key');
|
|
} finally {
|
|
if (saved === undefined) delete process.env.ZEROENTROPY_API_KEY;
|
|
else process.env.ZEROENTROPY_API_KEY = saved;
|
|
}
|
|
});
|
|
|
|
test('GBrainConfig type includes zeroentropy_api_key field (TS compile guard)', async () => {
|
|
const { type } = await import('../src/core/config.ts').then(m => ({ type: undefined }));
|
|
// The type-level assertion happens at compile time. If this file
|
|
// compiles, the field exists. Body of the test is a runtime no-op.
|
|
expect(true).toBe(true);
|
|
});
|
|
});
|
|
|
|
// Lane D.1 — engine-kind branching already covered in test/embedding-dim-check.test.ts
|
|
// (extended in same wave). The PGLite branch + Postgres branch + databasePath
|
|
// fallback + no-op-recipe-removal tests live there.
|
|
|
|
// Lane D.2 — embed pre-flight dim mismatch
|
|
describe('v0.37 Lane D.2 — embed pre-flight dim mismatch', () => {
|
|
test('CDX2-9: EmbeddingDimMismatchError is exported + tagged', async () => {
|
|
const { EmbeddingDimMismatchError } = await import('../src/commands/embed.ts');
|
|
expect(typeof EmbeddingDimMismatchError).toBe('function');
|
|
const err = new EmbeddingDimMismatchError('test recipe');
|
|
expect(err).toBeInstanceOf(Error);
|
|
expect(err.kind).toBe('embedding_dim_mismatch');
|
|
expect(err.recipeMessage).toBe('test recipe');
|
|
expect(err.name).toBe('EmbeddingDimMismatchError');
|
|
});
|
|
});
|
|
|
|
// Lane D.4 — sync help dispatch
|
|
describe('v0.37 Lane D.4 — sync --help dispatch', () => {
|
|
test('CDX2-12: sync is in CLI_ONLY_SELF_HELP', async () => {
|
|
// This is a structural test — read the cli.ts source and assert
|
|
// sync appears in the set. Avoids requiring engine wiring.
|
|
const src = readFileSync(join(__dirname, '..', 'src', 'cli.ts'), 'utf-8');
|
|
// Match the CLI_ONLY_SELF_HELP set definition.
|
|
const setMatch = src.match(/const CLI_ONLY_SELF_HELP = new Set\(\[([\s\S]*?)\]\)/);
|
|
expect(setMatch).not.toBeNull();
|
|
const body = setMatch![1];
|
|
expect(body).toContain(`'sync'`);
|
|
});
|
|
});
|
|
|
|
// Deferred-TODO ship: gbrain reinit-pglite
|
|
describe('v0.37 deferred TODO shipped — gbrain reinit-pglite', () => {
|
|
test('reinit-pglite is registered in CLI_ONLY + CLI_ONLY_SELF_HELP', () => {
|
|
const src = readFileSync(join(__dirname, '..', 'src', 'cli.ts'), 'utf-8');
|
|
const onlyMatch = src.match(/const CLI_ONLY = new Set\(\[([\s\S]*?)\]\)/);
|
|
expect(onlyMatch).not.toBeNull();
|
|
expect(onlyMatch![1]).toContain(`'reinit-pglite'`);
|
|
|
|
const selfHelpMatch = src.match(/const CLI_ONLY_SELF_HELP = new Set\(\[([\s\S]*?)\]\)/);
|
|
expect(selfHelpMatch).not.toBeNull();
|
|
expect(selfHelpMatch![1]).toContain(`'reinit-pglite'`);
|
|
});
|
|
|
|
test('reinit-pglite module exports runReinitPglite', async () => {
|
|
const mod = await import('../src/commands/reinit-pglite.ts');
|
|
expect(typeof mod.runReinitPglite).toBe('function');
|
|
});
|
|
|
|
test('embeddingMismatchMessage PGLite branch recommends `gbrain reinit-pglite`', async () => {
|
|
const { embeddingMismatchMessage } = await import('../src/core/embedding-dim-check.ts');
|
|
const msg = embeddingMismatchMessage({
|
|
currentDims: 1536,
|
|
requestedDims: 1280,
|
|
requestedModel: 'zeroentropyai:zembed-1',
|
|
source: 'doctor',
|
|
engineKind: 'pglite',
|
|
databasePath: '/tmp/test.pglite',
|
|
});
|
|
// The one-command path appears before the by-hand recipe.
|
|
expect(msg).toContain('gbrain reinit-pglite --embedding-model zeroentropyai:zembed-1 --embedding-dimensions 1280');
|
|
// The by-hand path is still present as fallback.
|
|
expect(msg).toContain('mv /tmp/test.pglite /tmp/test.pglite.bak');
|
|
// The recommended-section header precedes the by-hand section.
|
|
const recIdx = msg.indexOf('Recommended');
|
|
const handIdx = msg.indexOf('Or by hand');
|
|
expect(recIdx).toBeGreaterThan(0);
|
|
expect(handIdx).toBeGreaterThan(recIdx);
|
|
});
|
|
});
|