Files
gbrain/test/serve-http-embedding-width.test.ts
69e7e79a1f fix(ingest,sync,serve): three singleton P0s — type round-trip, deleted-slug embed noise, stateless width guard (#3140)
- #1035: importFromContent preserves an existing page's type when incoming
  frontmatter omits an explicit type: field. Explicit type stays an override;
  absence means preserve; new pages still path-infer. The existing-page fetch
  moved above the content-hash compute so a no-op re-put stays a hash-match
  skip. Root-cause fix covers put_page, sync, capture — every caller.
- #1284: sync's end-of-run auto-embed no longer receives slugs deleted in the
  same run (embedPage threw 'Page not found' per deleted slug and serr-logged
  noise on every rename/delete sync). pagesAffected stays the full manifest
  for extract/report paths; a slug deleted then re-imported in the same run
  stays embeddable.
- #1196: gbrain serve --http now runs doctor's embedding_width_consistency
  check at startup and prints a loud stderr banner (with the paste-ready
  recipe + GBRAIN_EMBEDDING_MODEL/DIMENSIONS hint) when the resolved width
  diverges from the brain's vector(N) column — the stateless-container
  fallthrough that broke every write. Fail-open; reads unaffected.

Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 12:57:37 -07:00

71 lines
2.5 KiB
TypeScript

/**
* #1196 — serve --http startup embedding-width guard.
*
* A stateless host (container without config.json) resolves the compiled-in
* default embedding width; against an existing brain with a different
* vector(N) column, every write fails. runServeHttp now runs doctor's
* embedding_width_consistency check at startup and prints a loud stderr
* banner. This pins the helper: mismatch → banner with the recipe;
* match → null (no banner noise on healthy brains).
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { configureGateway, resetGateway } from '../src/core/ai/gateway.ts';
import { embeddingWidthStartupWarning } from '../src/commands/serve-http.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
});
afterAll(async () => {
// Rule: files that configureGateway must resetGateway in afterAll.
// The legacy-embedding preload's beforeEach re-applies defaults for
// subsequent files in the same shard.
resetGateway();
await engine.disconnect();
});
async function schemaDims(): Promise<number> {
const rows = await engine.executeRaw<{ format_type: string }>(
`SELECT format_type(atttypid, atttypmod) AS format_type
FROM pg_attribute
WHERE attrelid = 'content_chunks'::regclass
AND attname = 'embedding'
AND NOT attisdropped`,
);
const m = rows[0].format_type.match(/vector\((\d+)\)/i);
return parseInt(m![1], 10);
}
describe('embeddingWidthStartupWarning (#1196)', () => {
test('resolved width matches the schema: no banner', async () => {
const dims = await schemaDims();
configureGateway({
embedding_model: 'openai:text-embedding-3-large',
embedding_dimensions: dims,
env: { ...process.env },
});
expect(await embeddingWidthStartupWarning(engine)).toBeNull();
});
test('resolved width diverges from the schema: loud banner with recipe', async () => {
// Simulate the stateless-container fallthrough: gateway resolves a
// width different from the brain's actual vector(N) column.
configureGateway({
embedding_model: 'openai:text-embedding-3-small',
embedding_dimensions: 768,
env: { ...process.env },
});
const warn = await embeddingWidthStartupWarning(engine);
expect(warn).not.toBeNull();
expect(warn!).toContain('[serve-http] WARNING');
expect(warn!).toContain('mismatch');
expect(warn!).toContain('GBRAIN_EMBEDDING_DIMENSIONS');
});
});