65075b4615
合併 arcrun-7-gloss-bridge(A1 gloss 橋)與 owner-threading(owner_id 寫入鏈必經):
- node-persist / triplet-ingest 衝突解為「兩者都要」:persistNodes 既寫 entity
record(帶真 owner)又 upsert gloss entry(帶真 owner、帶 source);ingestEnvelope
既帶 source.uri 又 thread 必填 owner 到底。
- gloss 路徑補 owner 必填(配合 owner-mandatory D28):
* upsertGlossEntry(..., owner_id: string) owner 改必填
* backfillGlossEntries(client, owner_id: string) owner 改必填
* POST /backfill-gloss-entries 缺 owner_id → 400(存量 record owner=None,
owner 由 caller 明確指定,不從 record 帶)
- 合併兩支測試:gloss 測試補 owner 參數以配合 base owner-required mock。
tsc:除 index.ts:57 swaggerUI 既存 baseline 外 0 error。
vitest:34 passed(gloss 7 + owner/graph 27)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ck8bjuBTLFkiFxqLGHMpqG
121 lines
5.7 KiB
TypeScript
121 lines
5.7 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 }], '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<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: '', // 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);
|
||
});
|
||
});
|