Files
kbdb-graph-plugin/tests/mock-client.ts
T
Claude 65075b4615 feat(graph): 收斂 gloss-bridge + owner-threading 為單一可部署分支
合併 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
2026-07-05 11:20:47 +00:00

109 lines
4.2 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.
// 記憶體版 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_nameidempotency key)、metadata_jsonembed 打標)、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;
}