From 22027b5f9f194beb4891a75f4e4a528ccc0cfd01 Mon Sep 17 00:00:00 2001 From: sharziki Date: Sat, 18 Apr 2026 15:57:35 -0400 Subject: [PATCH] fix(put): lowercase caller-supplied slug in importFromContent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit putPage runs validateSlug internally and stores the page at the lowercased slug, but the same-transaction tx.upsertChunks/getTags/addTag lookups use whatever slug the caller passed in. When a user ran `gbrain put claude-memory/TestUpper/test ...` the put_page handler forwarded the uppercase slug straight through and upsertChunks failed with "Page not found: claude-memory/TestUpper/test" — the same message `get_page` emits for a genuinely missing page, so users debugging a failing bulk put chased phantom "did the page get deleted?" paths instead of the real cause (issue #200). Normalize once at the top of importFromContent so putPage, getTags, addTag, removeTag, and upsertChunks all operate on the same lowercased slug. Added a regression test covering the mock-engine call arguments. Fixes #200 --- src/core/import-file.ts | 6 ++++++ test/import-file.test.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/core/import-file.ts b/src/core/import-file.ts index 69168907..770fc6f0 100644 --- a/src/core/import-file.ts +++ b/src/core/import-file.ts @@ -55,6 +55,12 @@ export async function importFromContent( content: string, opts: { noEmbed?: boolean } = {}, ): Promise { + // Normalize slug to lowercase to match engine-level validateSlug behavior. + // Without this, putPage stores the page at the lowercased slug while the + // followup tx.upsertChunks/getTags/addTag lookups use the caller's raw + // (possibly uppercase) slug and fail with "Page not found: ". + slug = slug.toLowerCase(); + // Reject oversized payloads before any parsing, chunking, or embedding happens. // Uses Buffer.byteLength to count UTF-8 bytes the same way disk size would, // so the network path behaves identically to the file path. diff --git a/test/import-file.test.ts b/test/import-file.test.ts index 60be770a..cdd289bf 100644 --- a/test/import-file.test.ts +++ b/test/import-file.test.ts @@ -380,6 +380,35 @@ Content to chunk but not embed. expect(result.status).toBe('imported'); }); + test('lowercases caller-supplied slug so putPage and upsertChunks agree', async () => { + // Engine-level validateSlug lowercases, so putPage stored the page at the + // lowercased slug. If importFromContent leaves the caller's casing intact, + // the same-transaction tx.upsertChunks/getTags calls look up the page by + // the raw uppercase slug and throw "Page not found". See issue #200. + const content = `--- +type: concept +title: TestUpper +--- + +Content that should succeed despite uppercase slug input. +`; + + const engine = mockEngine(); + const result = await importFromContent(engine, 'claude-memory/TestUpper/test', content, { noEmbed: true }); + + expect(result.status).toBe('imported'); + expect(result.slug).toBe('claude-memory/testupper/test'); + + const calls = (engine as any)._calls; + const putCall = calls.find((c: any) => c.method === 'putPage'); + expect(putCall).toBeTruthy(); + expect(putCall.args[0]).toBe('claude-memory/testupper/test'); + + const chunkCall = calls.find((c: any) => c.method === 'upsertChunks'); + expect(chunkCall).toBeTruthy(); + expect(chunkCall.args[0]).toBe('claude-memory/testupper/test'); + }); + test('assigns sequential chunk_index values', async () => { const filePath = join(TMP, 'indexed.md'); const longText = Array(50).fill('This is a sentence that adds length to the content.').join(' ');