Files
Arcrun/kbdb/tests/record-grow-owner.test.ts
T
uncle6me-web dd65b06a11 portal-auth P2(#24 #25):portal_user 模型+認證 API(不 merge,待總管審)
- lib/portal-auth.ts:PBKDF2-SHA256 600k(WebCrypto deriveBits)、pbkdf2-sha256$… 格式、
  常數時間比對(UI session 登入,console-auth 同類先例,非 workflow credential 原語)
- lib/portal-seeds.ts + init-seed:portal_user / portal_library template 冪等種入(零新表)
- routes/portal.ts:login/logout/session/me-password/bootstrap(console owner 閘)/
  admin users CRUD + reset-password / 庫目錄 CRUD;帳號資料全寫 {tenant}::portal 子 namespace
  (D-2)+ email head entry O(1) 查找(§2.3);session KV 只存 record_id、每請求回讀 record
  → 停用即時生效;絕不回租戶字串;登入節流 5 次/15 分(KV TTL)
- [vars] PORTAL_SESSION_TTL(預設 604800)+ types.ts
- 順修 kbdb record-crud updateRecord grow-slot 漏 owner_id(design §2.2)+回歸測試

驗證:kbdb 20/20、cypher 133/134(1 失敗 stash 複驗=pre-existing)、雙包 tsc 0;
本機雙 worker 端到端 33/33(含搜尋隔離實測:租戶 ns 搜 email/雜湊 0 筆、對照組
leo::portal 搜得到;明碼落地抽查 0 筆)。console-auth 既有行為未動;demo 8787/8788 未碰。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 12:10:03 +08:00

97 lines
4.5 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.
// 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);
});
});