10dda9e6f5
library-map SDD M5(Arcrun#39):console 首頁從空白搜尋框變全館地圖。 落點裁定:console 是 hash-routing 單頁殼,home 隨 profile(full=駕駛艙、 rag=總庫搜尋)——地圖放總庫搜尋頁搜尋框上方:rag profile 該頁即首頁; full profile 它是全館入口,「點庫卡片→進該庫搜尋」動線同頁最短,一份實作 兩 profile 全吃。 - console.ts:藏書地圖區塊(lm-section)——每庫一張卡(庫名+narrative+ top entities 前 5 帶 degree+triplet_count+commit_hash/updated 更新時間感)、 跨庫 bridges 有資料才列一小區;兩段式渲染(全館列表先出卡,逐庫詳圖補 degree/bridges,詳圖失敗維持名字版不擋);點庫卡片=設 library filter (/kbdb/search 既有 library 參數,不新造)進庫搜尋,附清除鈕搜回全館; /map 空陣列或錯誤=誠實空狀態(指引 POST /map/recompute backfill), 不擋搜尋等其他功能。品牌沿 CONSOLE_BRAND 變數(#21),零外部庫。 - kbdb-proxy.ts:GET /kbdb/map+/kbdb/map/:library 純轉發(rule 07,聚合真身 在 kbdb base M2)。owner_id 不強制注入只選擇性透傳(同 M4 MCP kbdb_get_map 語意;現行 backfill map block owner=NULL,強制注入租戶=首頁永遠假空狀態); 仍要求 X-Arcrun-API-Key 同閘;KBDB 不可達誠實 502。 - 測試:kbdb-map-proxy.test.ts 9 測(租戶閘/轉發/owner 透傳/空陣列/404/502)+ console-library-map-page.test.ts 6 測(route 200/區塊在搜尋框上方/資料源接線/ library 參數/誠實空狀態文案/品牌變數)——15/15 過。 - 誠實回報:tsc 16 行 ExecutionContext.tracing 錯誤與 executor.test 1 失敗= main 基線既有(git stash 對照實證),非本分支引入。 merge 後需 gated redeploy cypher-executor(leo 閘)。關聯 #39。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUmjwkHLVBHM3ydhT1WSW3
135 lines
5.7 KiB
TypeScript
135 lines
5.7 KiB
TypeScript
/**
|
||
* 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 不可達');
|
||
});
|
||
});
|