mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 14:59:47 +00:00
- #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>
71 lines
2.5 KiB
TypeScript
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');
|
|
});
|
|
});
|