// node gloss → embeddable base entry:走 mock KbdbClient(API-as-Wall),零 SQL、不打網路。 // 覆蓋:前向 persistNodes 多落 embeddable entry / embed=false 不落 / 空 gloss 不落 / 冪等不重複 / // gloss 變則更新 / backfill 對既有 entity records 補 entry(含冪等、embed=false 略過)。 import { describe, it, expect } from 'vitest'; import { persistNodes } from '../src/actions/node-persist'; import { backfillGlossEntries } from '../src/actions/backfill-gloss-entries'; import { NODE_GLOSS_ENTRY_TYPE } from '../src/actions/gloss-entry'; import { TPL_ENTITY } from '../src/lib/templates'; import { mockClient } from './mock-client'; const glossEntries = (c: ReturnType) => c.listEntries({ entry_type: NODE_GLOSS_ENTRY_TYPE }); describe('persistNodes — 前向:每個 node 另落一筆 embeddable base entry', () => { it('有 gloss 的 node → 落 entry:content=名+gloss、metadata.embed=true、帶 source/node_id', async () => { const c = mockClient(); await persistNodes( c, [{ name: '系統動力學', id: 'sd.md', gloss: '用因果圖表解複雜系統的方法論' }], 'leo21c', 'github:uncle6me-web/wiki@sd.md', ); const entries = await glossEntries(c); expect(entries.length).toBe(1); const e = entries[0]; expect(e.content).toBe('系統動力學:用因果圖表解複雜系統的方法論'); expect(e.page_name).toBe('gloss:sd.md'); expect(e.owner_id).toBe('leo21c'); const meta = JSON.parse(e.metadata_json!); expect(meta.embed).toBe(true); // 關鍵:base embed 模組讀這個 expect(meta.source).toBe('github:uncle6me-web/wiki@sd.md'); expect(meta.node_id).toBe('sd.md'); expect(meta.canonical).toBe('系統動力學'); // entity record 仍照舊存在(兩者並存:record 給圖、entry 給 embed)。 const recs = await c.listRecordsByTemplate(TPL_ENTITY); expect(recs.find((r) => r.values.canonical === '系統動力學')).toBeTruthy(); }); it('embed=false 的 node → 不落 embeddable entry', async () => { const c = mockClient(); await persistNodes(c, [{ name: '黃仁勳', id: '黃仁勳', gloss: 'NVIDIA 創辦人', embed: false }], 'leo21c'); expect((await glossEntries(c)).length).toBe(0); }); it('空 gloss 的 node → 不落 embeddable entry(不造無意義空殼)', async () => { const c = mockClient(); await persistNodes(c, [{ name: '某卡', id: 'card.md' }], 'leo21c'); expect((await glossEntries(c)).length).toBe(0); }); it('冪等:同一 node 重複 ingest → 只一筆 embeddable entry', async () => { const c = mockClient(); const nodes = [{ name: '長壽', id: 'longevity', gloss: '活得更久本身具有價值' }]; await persistNodes(c, nodes, 'leo21c', 'u1'); await persistNodes(c, nodes, 'leo21c', 'u1'); // 再送一次 expect((await glossEntries(c)).length).toBe(1); }); it('gloss 變更 → 更新既有 entry 內容(仍只一筆)', async () => { const c = mockClient(); await persistNodes(c, [{ name: '長壽', id: 'longevity', gloss: '舊描述' }], 'leo21c'); await persistNodes(c, [{ name: '長壽', id: 'longevity', gloss: '活得更久本身具有價值' }], 'leo21c'); const entries = await glossEntries(c); expect(entries.length).toBe(1); expect(entries[0].content).toBe('長壽:活得更久本身具有價值'); }); }); describe('backfillGlossEntries — 存量:對既有 entity records 補 embeddable entry', () => { // 直接以 entity record 造存量(模擬既有 16 筆),不經前向路徑。 async function seedEntity( c: ReturnType, v: { canonical: string; node_id?: string; gloss?: string; embed?: string }, ) { await c.createRecord(TPL_ENTITY, { canonical: v.canonical, node_id: v.node_id ?? '', aliases_json: '[]', entity_type: '', gloss: v.gloss ?? '', embed: v.embed ?? 'true', owner: '', // slot 存量 owner=None(模擬 D28 前的無主 record);backfill 的 owner 由 caller 明確指定 }, 'leo21c'); // 記錄層 owner_id:base D28 後 createRecord 一律要 owner;caller 明指 } it('有 gloss 的既有 record → 落 entry;embed=false / 空 gloss → 略過', async () => { const c = mockClient(); await seedEntity(c, { canonical: '長壽', node_id: 'longevity', gloss: '活得更久本身具有價值' }); await seedEntity(c, { canonical: '系統動力學', node_id: 'sd', gloss: '用因果圖表解複雜系統的方法論' }); await seedEntity(c, { canonical: '無標', node_id: 'x', gloss: '不該嵌', embed: 'false' }); await seedEntity(c, { canonical: '空描述', node_id: 'y' }); // 空 gloss const res = await backfillGlossEntries(c, 'leo21c'); expect(res.scanned).toBe(4); expect(res.created).toBe(2); expect(res.skipped).toBe(2); // embed=false + 空 gloss const entries = await glossEntries(c); expect(entries.length).toBe(2); expect(entries.every((e) => JSON.parse(e.metadata_json!).embed === true)).toBe(true); expect(entries.map((e) => e.content).sort()).toEqual( ['系統動力學:用因果圖表解複雜系統的方法論', '長壽:活得更久本身具有價值'].sort(), ); }); it('冪等:連跑兩次 backfill → 不造重複(第二次全 unchanged)', async () => { const c = mockClient(); await seedEntity(c, { canonical: '長壽', node_id: 'longevity', gloss: '活得更久本身具有價值' }); const first = await backfillGlossEntries(c, 'leo21c'); expect(first.created).toBe(1); const second = await backfillGlossEntries(c, 'leo21c'); expect(second.created).toBe(0); expect(second.unchanged).toBe(1); expect((await glossEntries(c)).length).toBe(1); }); });