feat(portal): GET /portal/data/diagnostics —— 檢修孔聚合端點

leo 2026-08-07 直接指令:「一顆按鈕在設定裡,按鈕下載一個檔案,把檔案發給我,你看那個
檔」。本端點是那個檔的資料來源:聚合 embed 模組健康狀態(module_enabled/cards_embedded/
cards_pending/self_test)、知識庫規模(library_count/triplet_count)、bundle_version、
instance_url。

紅線落實:
- 只轉發數字/布林/字串狀態,KBDB /map 回應裡的 narrative/top_entities(卡片內容)讀出
  triplet_count 後即丟棄,測試 portal-data.test.ts 新增案專門斷言回應不含內容字樣。
- 認證沿用既有 requirePortalUser session 閘,不對外公開。

3 個新測試全綠(未登入 401/完整聚合含隱私斷言/embed 未開時誠實回 false 不假裝)。
既有 1 個失敗案(/portal HTML 殼 404)為 stash 驗證過的既有失敗,與本次改動無關。
This commit is contained in:
uncle6me-web
2026-08-07 18:38:03 +08:00
parent 9344562258
commit 83aa1f6bb2
2 changed files with 167 additions and 0 deletions
+78
View File
@@ -711,3 +711,81 @@ describe('dedupeSourcesByPaget129 出處去重)', () => {
expect(out.length).toBe(1); // 同 page_name → 合為一筆
});
});
// ═══════════════ 7. GET /portal/data/diagnostics(檢修孔,2026-08-07) ═══════════════
describe('GET /portal/data/diagnostics', () => {
it('未登入 → 401,不碰 KBDB', async () => {
const res = await get('/portal/data/diagnostics');
expect(res.status).toBe(401);
});
it('登入 → 200,聚合 embed 健康狀態+規模統計+版本;只含數字/布林/字串狀態', async () => {
await seedSession('tok-diag1', 'rec_diag1');
mockGetRecord('rec_diag1', userValues());
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/embed/backfill/status'), method: 'GET' })
.reply(200, { success: true, enabled: true, pending: 3, embedded: 80 });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/embed/selftest'), method: 'GET' })
.reply(200, { success: true, enabled: true, tested: true, passed: false, note: '搜不到自己' });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/map'), method: 'GET' })
.reply(200, {
success: true,
libraries: [
{ name: 'general', triplet_count: 67, narrative: '不該出現在診斷檔', top_entities: ['密卡', '內容'] },
],
count: 1,
});
const res = await get('/portal/data/diagnostics', { Authorization: 'Bearer tok-diag1' });
expect(res.status).toBe(200);
const body = (await res.json()) as {
library_count: number;
triplet_count: number;
embedding: { module_enabled: boolean; cards_embedded: number; cards_pending: number; self_test: { ran: boolean; found_itself: boolean | null } };
instance_url: string;
bundle_version: string | null;
};
expect(body.library_count).toBe(1);
expect(body.triplet_count).toBe(67);
expect(body.embedding.module_enabled).toBe(true);
expect(body.embedding.cards_embedded).toBe(80);
expect(body.embedding.cards_pending).toBe(3);
expect(body.embedding.self_test.ran).toBe(true);
expect(body.embedding.self_test.found_itself).toBe(false);
expect(body.instance_url).toBe('http://localhost');
// 紅線斷言:整份回應不含知識卡內容本體(/map 回應裡的 narrativetop_entities 沒被轉發)
const raw = JSON.stringify(body);
expect(raw).not.toContain('不該出現在診斷檔');
expect(raw).not.toContain('密卡');
});
it('embed 模組未開(自架未開語義搜尋)→ 誠實回 module_enabled:false,不是假裝有 index', async () => {
await seedSession('tok-diag2', 'rec_diag2');
mockGetRecord('rec_diag2', userValues());
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/embed/backfill/status'), method: 'GET' })
.reply(200, { success: true, enabled: false, pending: 0, embedded: 0 });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/embed/selftest'), method: 'GET' })
.reply(200, { success: true, enabled: false, tested: false, passed: null, note: 'embed 模組未開' });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/map'), method: 'GET' })
.reply(200, { success: true, libraries: [], count: 0 });
const res = await get('/portal/data/diagnostics', { Authorization: 'Bearer tok-diag2' });
expect(res.status).toBe(200);
const body = (await res.json()) as { embedding: { module_enabled: boolean; self_test: { ran: boolean; found_itself: boolean | null } } };
expect(body.embedding.module_enabled).toBe(false);
expect(body.embedding.self_test.ran).toBe(false);
expect(body.embedding.self_test.found_itself).toBeNull();
});
});