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
This commit is contained in:
Claude
2026-07-19 09:34:28 +00:00
parent 289cd495b4
commit 10dda9e6f5
5 changed files with 369 additions and 3 deletions
@@ -0,0 +1,57 @@
/**
* GET /console 頁面測試 — 藏書地圖首頁區塊(library-map SDD M5/R4
*
* console 是單檔 HTML 薄殼(server 端零資料請求,client JS 打 /kbdb/map),所以頁面測試
* 驗的是「殼有沒有長對」:route 200、地圖區塊在搜尋框上方、進庫搜尋接的是既有 library
* 參數、空狀態/錯誤誠實文案在(不擋其他功能)。轉發行為真身在 kbdb-map-proxy.test.ts。
*/
import { SELF } from 'cloudflare:test';
import { describe, it, expect } from 'vitest';
async function page(): Promise<{ status: number; html: string }> {
const res = await SELF.fetch('http://localhost/console');
return { status: res.status, html: await res.text() };
}
describe('GET /console — 藏書地圖區塊', () => {
it('route 200、HTML 含地圖區塊(lm-section/lm-cards/lm-bridges', async () => {
const { status, html } = await page();
expect(status).toBe(200);
expect(html).toContain('藏書地圖');
expect(html).toContain('id="lm-section"');
expect(html).toContain('id="lm-cards"');
expect(html).toContain('id="lm-bridges"');
});
it('地圖區塊在搜尋框上方(DOM 順序:lm-section 先於 se-q', async () => {
const { html } = await page();
expect(html.indexOf('id="lm-section"')).toBeGreaterThan(-1);
expect(html.indexOf('id="lm-section"')).toBeLessThan(html.indexOf('id="se-q"'));
});
it('資料源接 /kbdb/mapcypher 代轉慣例)+逐庫詳圖 /kbdb/map/', async () => {
const { html } = await page();
expect(html).toContain("fetch('/kbdb/map'");
expect(html).toContain("'/kbdb/map/' + encodeURIComponent");
});
it('點庫卡片=既有搜尋 library 參數進庫(不新造參數)', async () => {
const { html } = await page();
expect(html).toContain("'&library=' + encodeURIComponent(S.library)");
expect(html).toContain('data-lib');
});
it('空狀態/錯誤分支=誠實文案(recompute backfill 指引)且不擋搜尋', async () => {
const { html } = await page();
expect(html).toContain('還沒有藏書地圖');
expect(html).toContain('/map/recompute');
expect(html).toContain('不影響下方搜尋');
expect(html).toContain('地圖服務不可達');
});
it('品牌字樣走 CONSOLE_BRAND 變數(未設=預設 Arcrun),地圖區塊沒硬編品牌', async () => {
const { html } = await page();
// 測試環境未設 CONSOLE_BRAND → 預設 Arcrun#21 rebrand:字樣由變數注入,非硬編 Mira)
expect(html).toContain('<title>Arcrun Console</title>');
});
});