Files
Arcrun/kbdb/tests/embed-selftest.test.ts
T
uncle6me-web 6846d6ddae fix(semantic): 故障照實說是故障——不再把壞掉說成「沒開通」(leo 2026-08-09 直令)
一、文案(portal/console/kbdb hint):語意搜尋是一安裝就提供的功能,
   降級=故障。橫幅改「語意搜尋目前故障/我們的問題/你不用做任何事」,
   拿掉「還沒開通、想開通請匯出診斷檔」這種要使用者申請開通的假框架。
   kbdb 降級回應加 degraded_reason(module_off / embed_query_failed)。

二、查詢向量化失敗不再偽裝成空結果(leo 點名的謊):
   semanticSearch 舊行為「AI 額度用完 → 回 []」會讓使用者以為
   自己的知識庫裡沒有這筆資料。改丟 EmbedQueryFailedError,
   route 誠實降級 keyword+照實告知是暫時故障。

三、源頭機制(裝好的實例為什麼會失去語意搜尋):
   - acr update:kbdb_embed 判斷 ===true → !==false。config 缺欄位時
     redeploy 會把 [[vectorize]]+[ai] binding 靜默剝掉(wrangler deploy
     整份覆蓋),一台正常實例就此壞掉。init 預設同步翻成 [Y/n]。
   -(另 repo)deploy-all.mjs ensureVectorizeIndex 失敗改致命中止。

四、順手自癒:孤兒向量/下架殘影搜尋時背景清除;空結果且 pending>0
   背景 backfill;no_index 拆「故障」vs「還沒有資料」兩態。

測試:kbdb 146/146(新增 degraded 6 案+selftest 1 案);cli 10/10;
瀏覽器端到端兩種故障畫面實測(local wrangler dev+portal 真登入)。
無 SDD 對應:leo 直令修故障(同 08-07 檢修孔前例的人閘直接授權路徑)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 02:05:12 +08:00

125 lines
5.1 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.
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<T>() {
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<T>() { 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:falseArcrun#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:falsenote 說明故障,不 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();
});
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');
});
});