Files
Arcrun/kbdb/tests/record-grow-owner.test.ts

97 lines
4.5 KiB
TypeScript
Raw Permalink 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.
// portal-auth P2 附帶修復回歸測試(design §2.2):
// updateRecord 的「grow 新 slot」路徑建 entry 時,必須帶 record 既有 owner_id。
// 修前:createEntry(db, { content, entry_type: 'value' }) → owner_id=NULL 孤兒 entry
// owner-scoped 查詢(searchByTemplate JOIN entries.owner_id / searchEntries owner filter)看不到該 slot 值。
// 測法沿 library-filter.test.ts 慣例:fake D1 按 SQL 形狀路由回應+捕捉 INSERT 參數
// (不解讀 SQL 語意——真語意由本機 miniflare 端到端驗證,PR 附證據)。
import { describe, it, expect } from 'vitest';
import { updateRecord } from '../src/actions/record-crud';
interface Captured { sql: string; params: unknown[] }
/**
* 可路由 fake D1
* - entry_values JOIN entries 查詢 → 回既有 slot rows(含 owner_id
* - templates 查詢 → 回 templateslots_json 含既有 + 可 grow 的 slot
* - INSERT INTO entries → 捕捉參數(本測試的斷言目標)
* - SELECT * FROM entries WHERE id → 回假 entrycreateEntry 的 insert 後回讀)
* - 其餘(UPDATE / INSERT entry_values / getRecord SELECT)→ 空殼
*/
function makeRoutedDB(recordOwnerId: string | null, captured: Captured[]) {
const prepare = (sql: string) => {
const rec: Captured = { sql, params: [] };
captured.push(rec);
const stmt = {
bind(...args: unknown[]) { rec.params = args; return stmt; },
async all<T>() {
if (sql.includes('FROM entry_values ev JOIN entries e')) {
return {
results: [
{ slot_name: 'email', entry_id: 'e_existing', template_id: 'tpl_pu', owner_id: recordOwnerId },
] as unknown as T[],
};
}
if (sql.includes('FROM entry_values ev JOIN entries e ON ev.entry_id = e.id')) {
return { results: [] as T[] };
}
return { results: [] as T[] };
},
async first<T>() {
if (sql.includes('FROM templates')) {
return {
id: 'tpl_pu', name: 'portal_user', description: null,
slots_json: JSON.stringify(['email', 'status']), created_by: 'system',
} as unknown as T;
}
if (sql.startsWith('SELECT * FROM entries WHERE id')) {
return {
id: 'e_new', content: 'active', entry_type: 'value', owner_id: recordOwnerId,
parent_id: null, page_name: null, refs_json: '[]', tags_json: '[]', task_status: null,
content_hash: null, is_embedded: 0, confidence: null, metadata_json: null,
created_at: 1, updated_at: 1,
} as unknown as T;
}
return null as unknown as T;
},
async run() { return { success: true }; },
};
return stmt;
};
return { prepare } as unknown as D1Database;
}
describe('updateRecord grow-slot 路徑帶 owner_iddesign §2.2 回歸)', () => {
it('record 有 owner_id → grow 出的新 entry INSERT 帶同一 owner_id(修前為 null', async () => {
const captured: Captured[] = [];
const db = makeRoutedDB('leo::portal', captured);
// 'status' slot 在 template 裡但 record 尚無 → 走 grow 路徑
await updateRecord(db, 'rec_1', { status: 'active' });
const insertEntry = captured.find((c) => c.sql.includes('INSERT INTO entries'));
expect(insertEntry, 'grow 路徑應 INSERT 新 entry').toBeTruthy();
// entry-crud.ts createEntry 的 INSERT 欄位序:id, content, entry_type, owner_id, ...
expect(insertEntry!.params[1]).toBe('active'); // content
expect(insertEntry!.params[2]).toBe('value'); // entry_type
expect(insertEntry!.params[3]).toBe('leo::portal'); // owner_id ← 本修復的斷言核心
});
it('record 本就無 owner_id(全域資料)→ grow 出的 entry owner_id 維持 null(行為不變)', async () => {
const captured: Captured[] = [];
const db = makeRoutedDB(null, captured);
await updateRecord(db, 'rec_1', { status: 'active' });
const insertEntry = captured.find((c) => c.sql.includes('INSERT INTO entries'));
expect(insertEntry).toBeTruthy();
expect(insertEntry!.params[3]).toBeNull();
});
it('既有 slot 更新(非 grow)→ 走 UPDATE entries,不 INSERT 新 entry', async () => {
const captured: Captured[] = [];
const db = makeRoutedDB('leo::portal', captured);
await updateRecord(db, 'rec_1', { email: 'new@example.com' });
expect(captured.some((c) => c.sql.includes('UPDATE entries SET content'))).toBe(true);
expect(captured.some((c) => c.sql.includes('INSERT INTO entries'))).toBe(false);
});
});