Files
kbdb-graph-plugin/tests/gloss-entry.test.ts
T
Claude 4cc7057cea feat(graph): node gloss→base embeddable entry 橋——Arcrun#7 A1
修「打標≠讀標」:node gloss+embed 標在 entity record、但 base embed 只掃 entries.metadata.embed
→ persistNodes 另落一筆 content=canonical:gloss、metadata.embed=true 的 base entry(走 API/零SQL)。
- gloss-entry.ts: upsertGlossEntry 冪等(確定性 page_name + list-then-write)
- backfill-gloss-entries.ts + route: 對既有 entity record 補 gloss entry
- triplet-ingest 帶 source.uri 進 metadata;kbdb-client 暴露 metadata_json(base 早支援)
- vitest 30/30(+7);[→arcrun] base 缺 upsert entry 端點,暫以 list-then-write 冪等
2026-07-05 09:16:26 +00:00

121 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// node gloss → embeddable base entry:走 mock KbdbClientAPI-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<typeof mockClient>) =>
c.listEntries({ entry_type: NODE_GLOSS_ENTRY_TYPE });
describe('persistNodes — 前向:每個 node 另落一筆 embeddable base entry', () => {
it('有 gloss 的 node → 落 entrycontent=名+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 }]);
expect((await glossEntries(c)).length).toBe(0);
});
it('空 gloss 的 node → 不落 embeddable entry(不造無意義空殼)', async () => {
const c = mockClient();
await persistNodes(c, [{ name: '某卡', id: 'card.md' }]);
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<typeof mockClient>,
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: '',
});
}
it('有 gloss 的既有 record → 落 entryembed=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);
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);
expect(first.created).toBe(1);
const second = await backfillGlossEntries(c);
expect(second.created).toBe(0);
expect(second.unchanged).toBe(1);
expect((await glossEntries(c)).length).toBe(1);
});
});