/** * GET /kbdb/map + /kbdb/map/:library proxy 測試(library-map SDD M5/R4,console 首頁藏書地圖資料源) * * 驗證 IO 接線(聚合真身在 KBDB 基本盤 kbdb/src/routes/map.ts,M2 已測;這裡只測轉發): * 1. 租戶閘:無 X-Arcrun-API-Key → 401 不碰 KBDB(與本 proxy 其他端點同閘) * 2. 轉發:/kbdb/map → base /map;/kbdb/map/:library → base /map/:library(庫名 URL-encode) * 3. owner_id **不強制注入、只選擇性透傳**(與 MCP kbdb_get_map 同義)——現行 backfill 的 * map block owner_id=NULL,強制注入租戶會讓首頁永遠假空狀態 * 4. 空陣列原樣透傳(前端誠實空狀態的資料依據);404/KBDB 不可達 → 誠實回報不假裝 * * KBDB 打 fetchMock 假 host(wrangler.test.toml KBDB_BASE_URL=https://kbdb.test)+ * disableNetConnect——測試絕不外連。 */ import { SELF, fetchMock } from 'cloudflare:test'; import { beforeAll, afterEach, describe, it, expect } from 'vitest'; const KEY = { 'X-Arcrun-API-Key': 'leo' }; beforeAll(() => { fetchMock.activate(); fetchMock.disableNetConnect(); }); afterEach(() => fetchMock.assertNoPendingInterceptors()); describe('GET /kbdb/map — 租戶閘', () => { it('無 X-Arcrun-API-Key → 401,不碰 KBDB', async () => { const res = await SELF.fetch('http://localhost/kbdb/map'); expect(res.status).toBe(401); }); it('/kbdb/map/:library 同閘 → 401', async () => { const res = await SELF.fetch('http://localhost/kbdb/map/kb'); expect(res.status).toBe(401); }); }); describe('GET /kbdb/map — 全館地圖轉發', () => { it('轉發 base /map、回應原樣透傳(不注入 owner_id)', async () => { const payload = { success: true, libraries: [ { library: 'kb', narrative: 'leo 的知識庫主庫', top_entities: ['00-INDEX'], triplet_count: 111, updated_at: 1784451880 }, ], count: 1, }; fetchMock .get('https://kbdb.test') .intercept({ path: '/map', method: 'GET' }) .reply(200, payload); const res = await SELF.fetch('http://localhost/kbdb/map', { headers: KEY }); expect(res.status).toBe(200); const data = (await res.json()) as typeof payload; expect(data.count).toBe(1); expect(data.libraries[0].library).toBe('kb'); expect(data.libraries[0].triplet_count).toBe(111); }); it('空陣列原樣透傳(前端據此顯示誠實空狀態,指引 recompute backfill)', async () => { fetchMock .get('https://kbdb.test') .intercept({ path: '/map', method: 'GET' }) .reply(200, { success: true, libraries: [], count: 0 }); const res = await SELF.fetch('http://localhost/kbdb/map', { headers: KEY }); expect(res.status).toBe(200); const data = (await res.json()) as { libraries: unknown[]; count: number }; expect(data.libraries).toEqual([]); expect(data.count).toBe(0); }); it('caller 帶 owner_id → 透傳給 base(選擇性,非強制注入)', async () => { fetchMock .get('https://kbdb.test') .intercept({ path: '/map', query: { owner_id: 'someone' }, method: 'GET' }) .reply(200, { success: true, libraries: [], count: 0 }); const res = await SELF.fetch('http://localhost/kbdb/map?owner_id=someone', { headers: KEY }); expect(res.status).toBe(200); }); }); describe('GET /kbdb/map/:library — 單庫詳圖轉發', () => { it('轉發 base /map/:library、詳圖(degree/bridges/commit_hash)原樣透傳', async () => { const map = { record_id: 'e_1', library: 'kb', narrative: 'leo 的知識庫主庫', top_entities: [{ name: '00-INDEX', degree: 29 }], relation_profile: [{ predicate: '連結至', count: 48 }], bridges: [{ entity: 'Gitea', libraries: ['kb', 'notes'] }], triplet_count: 111, commit_hash: 'abc1234def', status: 'active', updated_at: 1784451880, }; fetchMock .get('https://kbdb.test') .intercept({ path: '/map/kb', method: 'GET' }) .reply(200, { success: true, map }); const res = await SELF.fetch('http://localhost/kbdb/map/kb', { headers: KEY }); expect(res.status).toBe(200); const data = (await res.json()) as { map: typeof map }; expect(data.map.top_entities[0].degree).toBe(29); expect(data.map.bridges[0].libraries).toEqual(['kb', 'notes']); }); it('庫名 URL-encode 轉發(中文庫名不炸)', async () => { fetchMock .get('https://kbdb.test') .intercept({ path: `/map/${encodeURIComponent('筆記庫')}`, method: 'GET' }) .reply(200, { success: true, map: { library: '筆記庫' } }); const res = await SELF.fetch(`http://localhost/kbdb/map/${encodeURIComponent('筆記庫')}`, { headers: KEY }); expect(res.status).toBe(200); }); it('base 404(庫沒地圖)→ 原樣透傳 404,不假裝有資料', async () => { fetchMock .get('https://kbdb.test') .intercept({ path: '/map/nope', method: 'GET' }) .reply(404, { success: false, error: 'not found' }); const res = await SELF.fetch('http://localhost/kbdb/map/nope', { headers: KEY }); expect(res.status).toBe(404); const data = (await res.json()) as { success: boolean }; expect(data.success).toBe(false); }); }); describe('KBDB 不可達 → 誠實 502(前端顯示地圖不可達,不擋搜尋)', () => { it('/kbdb/map fetch 炸 → 502 + success:false', async () => { // 不掛 interceptor:disableNetConnect 下 fetch 直接 throw → route catch → 502 const res = await SELF.fetch('http://localhost/kbdb/map', { headers: KEY }); expect(res.status).toBe(502); const data = (await res.json()) as { success: boolean; error: string }; expect(data.success).toBe(false); expect(data.error).toContain('KBDB 不可達'); }); });