Files
kbdb-graph-plugin/tests/mock-client.ts
T
Leo 27f7448914 feat(ingest): POST /triplets/ingest 寫入端 + deprecate-then-append (T3.2-3.5)
對應 issue #1 T3 B 段。

- templates: TRIPLET_SLOTS 加 status/superseded_by/source_uri/content_hash;
  ENTITY_SLOTS 加 gloss;recordToTriplet 映射新欄位(缺省 status=active 相容舊資料)
- kbdb-client: ensureTemplate 改 slot-diff 補丁(既有 template 走 PATCH /templates/:id
  補缺 slot,取代 early-return → 免遷移腳本);新增 updateRecord(PATCH /records/:id)
- triplet-ingest action(88 行純函式):Zod strict 鏡射 ingest-candidate 契約 →
  idempotency(uri+hash 同→no-op)→ 先 append 後 deprecate(無「全無 active」空窗)
- POST /triplets/ingest route:strict 驗證失敗 → 422(禁送 graph 領域欄位)
- queryTriplets 預設 active-only(traverse/search/neighbors 皆經此),
  includeDeprecated opt-out 供 rollback/考古
- 6 測試案全綠(vitest 16 passed);mock-client 同步 slot-diff + updateRecord

gates: zero SQL / zero migration / 無 D1·Vectorize·AI 綁定 / dry-run bundle 乾淨

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:13:49 +08:00

91 lines
3.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> {
const id = this.id('entry');
const entry: BaseEntry = { id, content: input.content ?? null, entry_type: input.entry_type, owner_id: input.owner_id ?? 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[]> {
return [...this.entries.values()].filter(
(e) => (!filters.entry_type || e.entry_type === filters.entry_type) && (!filters.owner_id || e.owner_id === filters.owner_id),
);
}
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> {
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;
}