import { describe, it, expect } from 'vitest'; import { embedSelfTest } from '../src/embed'; import type { Bindings, Entry } from '../src/types'; // ── Minimal in-memory fakes ─────────────────────────────────────────────── // embedSelfTest issues exactly one DB statement: // SELECT * FROM entries WHERE is_embedded = 1 AND content <> '' [AND owner_id = ?] // ORDER BY updated_at DESC LIMIT 1 // The fake's `first()` filters the in-memory store accordingly and returns the // last match (proxy for "ORDER BY updated_at DESC LIMIT 1" given store insertion order). function mkEntry(id: string, content: string, ownerId = 'leo', is_embedded = 1): Entry { return { id, content, entry_type: 'block', owner_id: ownerId, parent_id: null, page_name: null, refs_json: '[]', tags_json: '[]', task_status: null, content_hash: null, is_embedded, confidence: null, metadata_json: JSON.stringify({ embed: true }), created_at: 1, updated_at: 1, }; } function makeFakeDB(store: Entry[]) { const prepare = (_sql: string) => { let bound: unknown[] = []; const stmt = { bind(...args: unknown[]) { bound = args; return stmt; }, async first() { const ownerId = bound.length > 0 ? String(bound[0]) : undefined; const rows = store.filter( (e) => e.is_embedded === 1 && (e.content ?? '').trim() !== '' && (!ownerId || e.owner_id === ownerId), ); return (rows.length > 0 ? rows[rows.length - 1] : null) as unknown as T; }, async all() { return { results: [] as T[] }; }, async run() { return { success: true }; }, }; return stmt; }; return { prepare } as unknown as D1Database; } function makeEnv( store: Entry[], opts: { withBindings?: boolean; matches?: { id: string; score: number }[] } = {}, ): Bindings { const withBindings = opts.withBindings ?? true; return { DB: makeFakeDB(store), ENVIRONMENT: 'test', ...(withBindings ? { AI: { async run() { return { data: [[0.1, 0.2, 0.3]] }; } }, VECTORIZE: { async query() { return { matches: opts.matches ?? [] }; } }, } : {}), } as unknown as Bindings; } describe('embedSelfTest(檢修孔:卡片自我查詢,驗證 index 真的可用)', () => { it('module off → enabled:false, tested:false, passed:null(誠實不假綠)', async () => { const env = makeEnv([mkEntry('e1', 'hello')], { withBindings: false }); const r = await embedSelfTest(env); expect(r.enabled).toBe(false); expect(r.tested).toBe(false); expect(r.passed).toBeNull(); expect(typeof r.note).toBe('string'); }); it('沒有任何已嵌入卡片 → tested:false, passed:null(非失敗,只是還沒東西可測)', async () => { const env = makeEnv([]); const r = await embedSelfTest(env); expect(r.enabled).toBe(true); expect(r.tested).toBe(false); expect(r.passed).toBeNull(); }); it('自我查詢能搜到自己 → passed:true', async () => { const store = [mkEntry('e1', 'doorbell workflow content')]; const env = makeEnv(store, { matches: [{ id: 'e1', score: 0.9 }] }); const r = await embedSelfTest(env); expect(r.enabled).toBe(true); expect(r.tested).toBe(true); expect(r.passed).toBe(true); }); it('自我查詢搜不到自己 → passed:false(Arcrun#11 那種「嵌了但查不到」故障模式)', async () => { const store = [mkEntry('e1', 'doorbell workflow content')]; const env = makeEnv(store, { matches: [{ id: 'some-other-id', score: 0.5 }] }); const r = await embedSelfTest(env); expect(r.enabled).toBe(true); expect(r.tested).toBe(true); expect(r.passed).toBe(false); }); it('向量化本身失敗(AI 額度用完)→ tested:false+note 說明故障,不 throw 也不假 passed', async () => { const store = [mkEntry('e1', '取樣內容', 'o1')]; const env = { DB: makeFakeDB(store), ENVIRONMENT: 'test', AI: { async run() { throw new Error('3040: daily limit'); } }, VECTORIZE: { async query() { return { matches: [] }; } }, } as unknown as Bindings; const r = await embedSelfTest(env, { owner_id: 'o1' }); expect(r.enabled).toBe(true); expect(r.tested).toBe(false); expect(r.passed).toBeNull(); expect(r.note).toContain('沒跑成'); }); it('依 owner_id 隔離:別的租戶的已嵌入卡片不會被拿來測', async () => { const store = [mkEntry('e1', 'content', 'other-tenant')]; const env = makeEnv(store, { matches: [] }); const r = await embedSelfTest(env, { owner_id: 'leo' }); expect(r.enabled).toBe(true); expect(r.tested).toBe(false); expect(r.passed).toBeNull(); }); // ── Arcrun#85 D70(2026-08-11 leo21c 全盲事故)────────────────────────────── // 兩種故障的**處方相反**,舊版都只回一句「需要重新 reindex」: // ① metadata filter 死掉(metadata index 沒建)→ 先建 index 再 reindex;只 reindex 無效 // ② 向量不在現役 index(換世代沒重嵌) → reindex 才是對的 // 判法=拿掉 filter 再查一次。下面的假 VECTORIZE 依「有沒有帶 filter」回不同結果, // 精確重現 leo21c 的現場(不帶 filter score 0.8957 命中、帶 owner_id 0 命中)。 function makeFilterAwareEnv( store: Entry[], opts: { unfilteredMatches: { id: string; score: number }[]; filteredMatches: { id: string; score: number }[] }, ): Bindings { return { DB: makeFakeDB(store), ENVIRONMENT: 'test', AI: { async run() { return { data: [[0.1, 0.2, 0.3]] }; } }, VECTORIZE: { async query(_v: number[], o?: { filter?: Record }) { const filtered = !!(o?.filter && Object.keys(o.filter).length > 0); return { matches: filtered ? opts.filteredMatches : opts.unfilteredMatches }; }, }, } as unknown as Bindings; } it('不帶 filter 搜得到、帶 owner_id 搜不到 → filter_blind:true,處方是「先建 metadata index 再 reindex」', async () => { const store = [mkEntry('e1', '淡水河口的黑面琵鷺在退潮時會集體覓食', 'bfezv28v')]; const env = makeFilterAwareEnv(store, { unfilteredMatches: [{ id: 'e1', score: 0.8957 }], // leo21c 實測分數 filteredMatches: [], }); const r = await embedSelfTest(env, { owner_id: 'bfezv28v' }); expect(r.tested).toBe(true); expect(r.passed).toBe(false); expect(r.filter_blind).toBe(true); expect(r.note).toContain('metadata'); // 🔴 處方順序必須寫出來——只叫人 reindex 正是 leo21c 修不好的原因 expect(r.note).toContain('reindex'); expect(r.note).toMatch(/先.*建.*再/s); }); it('帶不帶 filter 都搜不到 → filter_blind:false,處方才是 reindex', async () => { const store = [mkEntry('e1', 'content', 'o1')]; const env = makeFilterAwareEnv(store, { unfilteredMatches: [], filteredMatches: [] }); const r = await embedSelfTest(env, { owner_id: 'o1' }); expect(r.passed).toBe(false); expect(r.filter_blind).toBe(false); expect(r.note).toContain('reindex'); expect(r.note).not.toContain('metadata index 沒建'); }); it('帶 filter 就搜得到 → passed:true、filter_blind:null,且不多花第二次查詢', async () => { const store = [mkEntry('e1', 'content', 'o1')]; let queries = 0; const env = { DB: makeFakeDB(store), ENVIRONMENT: 'test', AI: { async run() { return { data: [[0.1, 0.2, 0.3]] }; } }, VECTORIZE: { async query() { queries++; return { matches: [{ id: 'e1', score: 0.9 }] }; } }, } as unknown as Bindings; const r = await embedSelfTest(env, { owner_id: 'o1' }); expect(r.passed).toBe(true); expect(r.filter_blind).toBeNull(); expect(queries).toBe(1); // 健康的情況不該多打一次(成本紀律) }); it('回應絕不含卡片內容或 entry id(隱私紅線)', async () => { const store = [mkEntry('e1', 'this is the secret card body, must never leak')]; const env = makeEnv(store, { matches: [{ id: 'e1', score: 0.9 }] }); const r = await embedSelfTest(env); const json = JSON.stringify(r); expect(json).not.toContain('e1'); expect(json).not.toContain('secret card body'); }); });