4cc7057cea
修「打標≠讀標」: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 冪等
121 lines
5.5 KiB
TypeScript
121 lines
5.5 KiB
TypeScript
// 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<typeof mockClient>) =>
|
||
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 }]);
|
||
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 → 落 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);
|
||
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);
|
||
});
|
||
});
|