Files
Claude 10dda9e6f5 feat(console): library-map M5 — 總庫搜尋頁藏書地圖首頁區塊(R4)
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
2026-07-19 09:34:36 +00:00

135 lines
5.7 KiB
TypeScript
Raw Permalink 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.
/**
* GET /kbdb/map /kbdb/map/:library proxy 測試(library-map SDD M5/R4console 首頁藏書地圖資料源)
*
* 驗證 IO 接線(聚合真身在 KBDB 基本盤 kbdb/src/routes/map.tsM2 已測;這裡只測轉發):
* 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 假 hostwrangler.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 () => {
// 不掛 interceptordisableNetConnect 下 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 不可達');
});
});