fix(takes): resolve the page before writing markdown in takes add (#3781)

Wave-assembled from PR #3781 by @ghizi.

Co-Authored-By: Constantin Gozman <constantingozman@gmail.com>
This commit is contained in:
Garry Tan
2026-08-12 14:38:36 -07:00
committed by Sina Matian
co-authored by Constantin Gozman
parent f7d4f19124
commit 8db07e4a79
2 changed files with 84 additions and 3 deletions
+7 -2
View File
@@ -210,6 +210,13 @@ async function cmdAdd(engine: BrainEngine, args: string[], sourceId?: string): P
const brainDir = await resolveBrainDir(engine, dirArg ?? null);
await withPageLock(slug, async () => {
// Resolve the page BEFORE touching the markdown. getPageId exits 1 when the
// page isn't in the brain; doing this after writeBody left a .md file
// carrying a take with no DB row — invisible to scorecard/calibration but
// present on disk, so a later `takes add` would number the next row past a
// take the DB never saw. update/supersede/resolve already resolve first.
const pageId = await getPageId(engine, slug, sourceId);
const path = pageFilePath(brainDir, slug);
const body = readBodyOrEmpty(path);
const { body: nextBody, rowNum } = upsertTakeRow(body, {
@@ -217,8 +224,6 @@ async function cmdAdd(engine: BrainEngine, args: string[], sourceId?: string): P
});
writeBody(path, nextBody);
// Mirror to DB. Page may not be in DB yet if not synced — caller must run sync first.
const pageId = await getPageId(engine, slug, sourceId);
await engine.addTakesBatch([{
page_id: pageId, row_num: rowNum, claim, kind, holder, weight,
since_date: since, source, active: true, superseded_by: null,
+77 -1
View File
@@ -1,4 +1,4 @@
import { afterEach, describe, expect, test } from 'bun:test';
import { afterEach, describe, expect, spyOn, test } from 'bun:test';
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
@@ -195,3 +195,79 @@ describe('gbrain takes CLI source scoping', () => {
expect(listCalls).toEqual([{ page_id: 11, active: true, limit: 500 }]);
});
});
describe('gbrain takes add — page validated before markdown is written', () => {
test('missing page leaves no orphaned .md on disk', async () => {
const brainDir = mkdtempSync(join(tmpdir(), 'gbrain-takes-orphan-'));
const home = mkdtempSync(join(tmpdir(), 'gbrain-takes-orphan-home-'));
tmpRoots.push(brainDir, home);
const { engine, added } = makeEngine();
// makeEngine returns [] for any slug other than shared/page, so getPageId
// takes its not-found path and exits 1.
const errs: string[] = [];
const errSpy = spyOn(console, 'error').mockImplementation((...a: unknown[]) => { errs.push(a.join(' ')); });
const exitSpy = spyOn(process, 'exit').mockImplementation(((code?: number) => {
throw new Error(`EXIT:${code}`);
}) as never);
let exited: string | null = null;
try {
await withEnv({ GBRAIN_SOURCE: undefined, GBRAIN_HOME: home }, async () => {
await runTakes(engine, [
'add',
'missing/page',
'--claim',
'Claim against a page that was never synced',
'--kind',
'bet',
'--who',
'council',
'--dir',
brainDir,
]);
});
} catch (e) {
if (!(e as Error).message.startsWith('EXIT:')) throw e;
exited = (e as Error).message;
} finally {
errSpy.mockRestore();
exitSpy.mockRestore();
}
expect(exited).toBe('EXIT:1');
expect(errs.join('\n')).toContain('Page not found in brain: missing/page');
// Pre-fix the .md was written before getPageId ran, so the take survived
// on disk with no DB row — invisible to scorecard, but real enough to
// shift row numbering on the next add.
expect(existsSync(join(brainDir, 'missing/page.md'))).toBe(false);
expect(added).toEqual([]);
});
test('existing page still writes both markdown and DB', async () => {
const brainDir = mkdtempSync(join(tmpdir(), 'gbrain-takes-ok-'));
const home = mkdtempSync(join(tmpdir(), 'gbrain-takes-ok-home-'));
tmpRoots.push(brainDir, home);
const { engine, added } = makeEngine();
await withEnv({ GBRAIN_SOURCE: undefined, GBRAIN_HOME: home }, async () => {
await runTakes(engine, [
'add',
'shared/page',
'--claim',
'Claim against a real page',
'--kind',
'bet',
'--who',
'council',
'--dir',
brainDir,
]);
});
expect(existsSync(join(brainDir, 'shared/page.md'))).toBe(true);
expect(readFileSync(join(brainDir, 'shared/page.md'), 'utf8')).toContain('Claim against a real page');
expect(added.flat()).toHaveLength(1);
expect(added.flat()[0]!.page_id).toBe(11);
});
});