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
109 lines
4.2 KiB
TypeScript
109 lines
4.2 KiB
TypeScript
// 記憶體版 KbdbClient — 模擬基本盤 API,供插件單元測試(不打真網路、不碰 D1)。
|
||
// 只實作插件實際用到的方法,行為對齊 arcrun/kbdb 契約。
|
||
|
||
import type { KbdbClient, BaseEntry, BaseRecord } from '../src/lib/kbdb-client';
|
||
|
||
export class MockKbdbClient {
|
||
private entries = new Map<string, BaseEntry>();
|
||
private templates = new Map<string, string[]>();
|
||
private records = new Map<string, { template: string; values: Record<string, string>; owner_id?: string }>();
|
||
private seq = 0;
|
||
|
||
private id(prefix: string): string {
|
||
this.seq += 1;
|
||
return `${prefix}-${this.seq}`;
|
||
}
|
||
|
||
async createEntry(input: any): Promise<BaseEntry> {
|
||
// 對齊基本盤 D27/D28 即將上線的 owner 必填:缺 owner → 400(模擬 base 擋死無主寫入)。
|
||
if (!(input?.owner_id ?? '').trim()) throw new Error('[kbdb-base] POST /entries: owner_id required (400)');
|
||
const id = this.id('entry');
|
||
// 對齊 base entry-crud:保留 page_name(idempotency key)、metadata_json(embed 打標)、parent_id。
|
||
const entry: BaseEntry = {
|
||
id,
|
||
content: input.content ?? null,
|
||
entry_type: input.entry_type,
|
||
owner_id: input.owner_id ?? null,
|
||
parent_id: input.parent_id ?? null,
|
||
page_name: input.page_name ?? null,
|
||
metadata_json: input.metadata_json ?? null,
|
||
};
|
||
this.entries.set(id, entry);
|
||
return entry;
|
||
}
|
||
|
||
async getEntry(id: string): Promise<BaseEntry | null> {
|
||
return this.entries.get(id) ?? null;
|
||
}
|
||
|
||
async listEntries(filters: any = {}): Promise<BaseEntry[]> {
|
||
// 對齊 base entry-crud listEntries 的 exact-match filters(含 page_name idempotency key、parent_id)。
|
||
return [...this.entries.values()].filter(
|
||
(e) =>
|
||
(!filters.entry_type || e.entry_type === filters.entry_type) &&
|
||
(!filters.owner_id || e.owner_id === filters.owner_id) &&
|
||
(!filters.parent_id || e.parent_id === filters.parent_id) &&
|
||
(!filters.page_name || e.page_name === filters.page_name),
|
||
);
|
||
}
|
||
|
||
async searchEntries(q: string, owner_id?: string): Promise<BaseEntry[]> {
|
||
const needle = q.toLowerCase();
|
||
return [...this.entries.values()].filter(
|
||
(e) => (e.content ?? '').toLowerCase().includes(needle) && (!owner_id || e.owner_id === owner_id),
|
||
);
|
||
}
|
||
|
||
async updateEntry(id: string, patch: any): Promise<BaseEntry | null> {
|
||
const e = this.entries.get(id);
|
||
if (!e) return null;
|
||
Object.assign(e, patch);
|
||
return e;
|
||
}
|
||
|
||
async deleteEntry(id: string): Promise<void> {
|
||
this.entries.delete(id);
|
||
}
|
||
|
||
async ensureTemplate(name: string, slots: string[]): Promise<void> {
|
||
// 對齊真 client 的 slot-diff 行為:既有 template 補缺 slot(不 early-return)。
|
||
const have = this.templates.get(name);
|
||
if (!have) {
|
||
this.templates.set(name, [...slots]);
|
||
return;
|
||
}
|
||
const set = new Set(have);
|
||
for (const s of slots) if (!set.has(s)) have.push(s);
|
||
}
|
||
|
||
async createRecord(template: string, values: Record<string, string>, owner_id?: string): Promise<string> {
|
||
// 對齊基本盤 D27/D28 即將上線的 owner 必填:缺 owner → 400(模擬 base 擋死無主寫入)。
|
||
if (!(owner_id ?? '').trim()) throw new Error('[kbdb-base] POST /records: owner_id required (400)');
|
||
const id = this.id('rec');
|
||
this.records.set(id, { template, values: { ...values }, owner_id });
|
||
return id;
|
||
}
|
||
|
||
async getRecord(recordId: string): Promise<BaseRecord | null> {
|
||
const r = this.records.get(recordId);
|
||
if (!r) return null;
|
||
return { record_id: recordId, template: r.template, values: r.values };
|
||
}
|
||
|
||
async updateRecord(recordId: string, values: Record<string, string>): Promise<void> {
|
||
const r = this.records.get(recordId);
|
||
if (r) Object.assign(r.values, values);
|
||
}
|
||
|
||
async listRecordsByTemplate(template: string, owner_id?: string): Promise<BaseRecord[]> {
|
||
return [...this.records.entries()]
|
||
.filter(([, r]) => r.template === template && (!owner_id || r.owner_id === owner_id))
|
||
.map(([record_id, r]) => ({ record_id, template: r.template, values: r.values }));
|
||
}
|
||
}
|
||
|
||
/** 轉成 KbdbClient 型別供 action 接受。 */
|
||
export function mockClient(): KbdbClient {
|
||
return new MockKbdbClient() as unknown as KbdbClient;
|
||
}
|