mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 19:49:14 +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>
119 lines
4.8 KiB
TypeScript
119 lines
4.8 KiB
TypeScript
/**
|
|
* #1284 — sync auto-embed must not be handed slugs deleted in the same run.
|
|
*
|
|
* The delete loop pushes confirmed-deleted slugs into pagesAffected (the
|
|
* full manifest for extract/report paths), but the end-of-run auto-embed
|
|
* used to pass that same list to runEmbedCore. embedPage throws
|
|
* 'Page not found: <slug>' for each deleted slug and the per-slug loop
|
|
* serr-logs 'Error embedding <slug>: Page not found' — pure noise on every
|
|
* rename/delete sync. The embed call now filters against the run's
|
|
* deleted-slug set (a slug re-imported later in the run stays embeddable).
|
|
*/
|
|
|
|
import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test';
|
|
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { execSync } from 'child_process';
|
|
import { tmpdir } from 'os';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
import { resetPgliteState } from './helpers/reset-pglite.ts';
|
|
import { configureGateway, resetGateway } from '../src/core/ai/gateway.ts';
|
|
|
|
let engine: PGLiteEngine;
|
|
let repoPath: string;
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({});
|
|
await engine.initSchema();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
resetGateway(); // preload beforeEach restores legacy defaults for later files
|
|
await engine.disconnect();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await resetPgliteState(engine);
|
|
repoPath = mkdtempSync(join(tmpdir(), 'gbrain-sync-del-embed-'));
|
|
execSync('git init', { cwd: repoPath, stdio: 'pipe' });
|
|
execSync('git config user.email "test@test.com"', { cwd: repoPath, stdio: 'pipe' });
|
|
execSync('git config user.name "Test"', { cwd: repoPath, stdio: 'pipe' });
|
|
mkdirSync(join(repoPath, 'people'), { recursive: true });
|
|
writeFileSync(join(repoPath, 'people/alice-example.md'), [
|
|
'---',
|
|
'type: person',
|
|
'title: Alice Example',
|
|
'---',
|
|
'',
|
|
'Alice is a person page that will be deleted.',
|
|
].join('\n'));
|
|
execSync('git add -A && git commit -m "initial"', { cwd: repoPath, stdio: 'pipe' });
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (repoPath) rmSync(repoPath, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('sync auto-embed vs deleted slugs (#1284)', () => {
|
|
test('delete-only incremental sync does not log Page not found from embed', async () => {
|
|
const { performSync } = await import('../src/commands/sync.ts');
|
|
|
|
// Seed: first sync with --no-embed imports alice and sets the bookmark.
|
|
const first = await performSync(engine, { repoPath, noPull: true, noEmbed: true });
|
|
expect(first.status).toBe('first_sync');
|
|
expect(await engine.getPage('people/alice-example')).not.toBeNull();
|
|
|
|
// Delete the file and commit.
|
|
execSync('git rm -q people/alice-example.md && git commit -qm "delete alice"', {
|
|
cwd: repoPath, stdio: 'pipe', shell: '/bin/bash',
|
|
});
|
|
|
|
// Configure the gateway to MATCH the schema width with creds present,
|
|
// so runEmbedCore's preflights (assertEmbeddingEnabled, creds check,
|
|
// dim-mismatch check) all pass and the per-slug embed loop actually
|
|
// runs. Pre-fix, that loop received the deleted slug and serr-logged
|
|
// 'Error embedding people/alice-example: Page not found'.
|
|
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 dims = parseInt(rows[0].format_type.match(/vector\((\d+)\)/i)![1], 10);
|
|
configureGateway({
|
|
embedding_model: 'openai:text-embedding-3-large',
|
|
embedding_dimensions: dims,
|
|
env: { ...process.env, OPENAI_API_KEY: 'sk-test-not-real' },
|
|
});
|
|
|
|
// Capture both stderr channels serr() can write to.
|
|
const captured: string[] = [];
|
|
const origErr = console.error;
|
|
const origWrite = process.stderr.write.bind(process.stderr);
|
|
console.error = (...args: unknown[]) => { captured.push(args.map(String).join(' ')); };
|
|
(process.stderr as { write: unknown }).write = ((s: unknown) => {
|
|
captured.push(String(s));
|
|
return true;
|
|
}) as typeof process.stderr.write;
|
|
|
|
let result: Awaited<ReturnType<typeof performSync>>;
|
|
try {
|
|
// NOTE: no noEmbed — the auto-embed path must run to pin the bug.
|
|
result = await performSync(engine, { repoPath, noPull: true });
|
|
} finally {
|
|
console.error = origErr;
|
|
(process.stderr as { write: unknown }).write = origWrite;
|
|
}
|
|
|
|
expect(result.status).toBe('synced');
|
|
expect(result.deleted).toBe(1);
|
|
// pagesAffected stays the full manifest for extract/report consumers.
|
|
expect(result.pagesAffected).toContain('people/alice-example');
|
|
// The regression: deleted slug handed to the embedder.
|
|
const all = captured.join('\n');
|
|
expect(all).not.toContain('Page not found');
|
|
});
|
|
});
|