Files
gbrain/test/write-through.test.ts
T
63977054af v0.41.30.0 fix(brainstorm/lsd): --save writes the advertised .md file via canonical ingestion path (#1655)
* refactor: extract shared atomic writePageThrough helper

Lift the v0.38 put_page disk write-through (operations.ts) into a shared
src/core/write-through.ts helper and upgrade it to write atomically (unique
temp file + rename) so a crash or a concurrent gbrain sync can never read a
half-written .md. put_page now calls the helper; behavior is preserved (repo
guards, source-awareness, provenance overrides) and its write is now atomic.
brainstorm/lsd --save will call the same helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(brainstorm/lsd): --save writes the advertised file via canonical ingestion

--save printed 'Saved to <slug>' unconditionally but only did a raw DB
putPage: the promised wiki/ideas/<slug>.md file was never written, the page
had no chunks (unsearchable, and churned by the next sync), and a failed DB
write under PgBouncer still claimed success.

Route save through importFromContent({noEmbed:true}) for a canonical row, then
the shared writePageThrough helper renders the file from that row. persistSavedIdea
+ formatSaveOutcome report honestly which sinks landed and exit nonzero when
nothing persisted. buildIdeaSlug gets a random nonce so same-day ideas don't
clobber. New buildBrainstormFrontmatterObject feeds serializeMarkdown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.41.30.0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: document write-through.ts + brainstorm --save canonical path (v0.41.30.0)

Add Key Files entry for the new shared atomic src/core/write-through.ts
helper, note the brainstorm/lsd --save canonical-ingestion rewrite on the
brainstorm entry, and note put_page now calls the shared atomic helper on
the operations.ts entry. Regenerate llms-full.txt to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 09:14:14 -07:00

140 lines
5.0 KiB
TypeScript

/**
* Shared write-through helper tests (src/core/write-through.ts).
*
* Covers the skip/error branches and the atomic-write guarantee. The helper is
* the canonical disk sink shared by `put_page` and `gbrain brainstorm/lsd
* --save`, extracted from the v0.38 put_page write-through and upgraded to write
* atomically (.tmp + rename).
*/
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { resetPgliteState } from './helpers/reset-pglite.ts';
import { resetGateway } from '../src/core/ai/gateway.ts';
import { writePageThrough } from '../src/core/write-through.ts';
import { importFromContent } from '../src/core/import-file.ts';
import { serializePageToMarkdown, resolvePageFilePath } from '../src/core/markdown.ts';
let engine: PGLiteEngine;
let tmpRoot: string;
let brainDir: string;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
});
afterAll(async () => {
await engine.disconnect();
resetGateway();
});
beforeEach(async () => {
await resetPgliteState(engine);
resetGateway();
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gbrain-wt-helper-'));
brainDir = path.join(tmpRoot, 'brain');
fs.mkdirSync(brainDir, { recursive: true });
});
afterEach(() => {
fs.rmSync(tmpRoot, { recursive: true, force: true });
});
async function seedPage(slug: string): Promise<void> {
await importFromContent(engine, slug, `---\ntitle: T\ntype: note\n---\n\n# Body ${slug}\n`, {
noEmbed: true,
sourceId: 'default',
sourcePath: `${slug}.md`,
});
}
function walkFiles(dir: string): string[] {
const out: string[] = [];
const walk = (d: string) => {
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
const p = path.join(d, e.name);
if (e.isDirectory()) walk(p);
else out.push(p);
}
};
walk(dir);
return out;
}
describe('writePageThrough', () => {
test('writes the file rendered from the saved row; no .tmp leftover', async () => {
await engine.setConfig('sync.repo_path', brainDir);
const slug = 'wiki/ideas/2026-01-01-lsd-foo-abc123';
await seedPage(slug);
const res = await writePageThrough(engine, slug, {
sourceId: 'default',
frontmatterOverrides: { source_kind: 'lsd' },
});
expect(res.written).toBe(true);
const expectedPath = resolvePageFilePath(brainDir, slug, 'default');
expect(res.path).toBe(expectedPath);
expect(fs.existsSync(expectedPath)).toBe(true);
// Content is the canonical serialization of the saved row (the file is
// rendered FROM the row, so the sinks can't diverge).
const page = await engine.getPage(slug, { sourceId: 'default' });
const tags = await engine.getTags(slug, { sourceId: 'default' });
const expected = serializePageToMarkdown(page!, tags, {
frontmatterOverrides: { source_kind: 'lsd' },
});
expect(fs.readFileSync(expectedPath, 'utf8')).toBe(expected);
// Atomic write left no temp sibling.
const dir = path.dirname(expectedPath);
expect(fs.readdirSync(dir).some((f) => f.includes('.tmp.'))).toBe(false);
});
test('no sync.repo_path → skipped no_repo_configured', async () => {
await engine.setConfig('sync.repo_path', '');
const slug = 'wiki/ideas/x-1';
await seedPage(slug);
const res = await writePageThrough(engine, slug);
expect(res).toEqual({ written: false, skipped: 'no_repo_configured' });
});
test('sync.repo_path is a file, not a directory → skipped repo_not_found', async () => {
const fileAsRepo = path.join(tmpRoot, 'not-a-dir');
fs.writeFileSync(fileAsRepo, 'x');
await engine.setConfig('sync.repo_path', fileAsRepo);
const slug = 'wiki/ideas/x-2';
await seedPage(slug);
const res = await writePageThrough(engine, slug);
expect(res).toEqual({ written: false, skipped: 'repo_not_found' });
});
test('row missing → skipped page_not_found_after_write', async () => {
await engine.setConfig('sync.repo_path', brainDir);
const res = await writePageThrough(engine, 'wiki/ideas/does-not-exist');
expect(res).toEqual({ written: false, skipped: 'page_not_found_after_write' });
});
test('[REGRESSION] mkdir ENOTDIR (parent is a file) → error, no partial .md, no .tmp', async () => {
await engine.setConfig('sync.repo_path', brainDir);
// Block the `wiki/` directory by putting a FILE named "wiki" under the repo,
// so `mkdir -p <repo>/wiki/ideas` throws ENOTDIR deterministically.
fs.writeFileSync(path.join(brainDir, 'wiki'), 'blocker');
const slug = 'wiki/ideas/blocked-1';
await seedPage(slug);
const res = await writePageThrough(engine, slug, { sourceId: 'default' });
expect(res.written).toBe(false);
expect(typeof res.error).toBe('string');
const files = walkFiles(brainDir);
expect(files.some((f) => f.endsWith('.md'))).toBe(false);
expect(files.some((f) => f.includes('.tmp.'))).toBe(false);
});
});