/** * server instructions(`initialize` 時送出的那份,AI **沒辦法自己再要一次**)。 * * 這份測試守的是 2026-08-13 leo 那句話: * 「它應該是**你的知識來源**⋯⋯**沒有它你是瞎的**。」 * * 病灶(實測):instructions 裡唯一提到「這裡有知識可查」的,是**選配的**【藏書地圖】段 * (1500ms 逾時、失敗回 null、無聲消失)。一條 portal 連線就沒收到它 ⇒ 那個 session * 為了回答「Arcrun 是什麼」去讀了 682 行原始碼,而 `kbdb_search` 當下回得出 33 筆真答案。 * * 所以本檔的驗收判準只有一句: * **不管地圖抓不抓得到、身分是哪一種,讀完這份 instructions 的下一個動作都必須是「去查知識庫」。** */ import { describe, it, expect, beforeEach } from "vitest"; import { buildServerInstructions } from "../../src/mcp-handler.js"; import { __resetLibraryMapInstructionsCacheForTests } from "../../src/lib/library-map.js"; import type { Env } from "../../src/types.js"; import type { KnowledgeIdentity } from "../../src/lib/portal-client.js"; const SERVICE: KnowledgeIdentity = { kind: "service" }; const STALE: KnowledgeIdentity = { kind: "stale" }; /** 假 KBDB service binding(服務級憑據路徑)。 */ function makeEnv(respond: () => Response | Promise): Env { return { KBDB: { fetch: async () => respond() } } as unknown as Env; } const MAP_OK = () => new Response( JSON.stringify({ success: true, libraries: [ { library: "kb", narrative: "leo 的知識庫主庫", top_entities: ["Arcrun"], triplet_count: 1854 }, ], count: 1, }), ); /** * 「一個什麼都不知道的 AI 讀完,下一個動作會是什麼」的機械化判準。 * 三份 instructions 都必須通過同一組斷言——通不過就是還沒修好。 */ function expectPointsAtKnowledge(text: string) { // ① 明講這條連線後面有主人的知識庫 expect(text).toContain("知識庫"); // ② 指名工具與呼叫法(AI 不必猜工具名) expect(text).toContain("kbdb_search"); expect(text).toContain("Arcrun 是什麼"); // ③ 明確排除三條錯路 expect(text).toContain("不是 grep 原始碼"); expect(text).toContain("不是上網搜"); expect(text).toContain("不是回答「我不知道」"); // ④ 「沒查到」與「沒有」不可以混為一談 expect(text).toContain("那是**讀不到**,不是**不存在**"); } describe("buildServerInstructions — 三種情境都必須指向知識庫", () => { beforeEach(() => __resetLibraryMapInstructionsCacheForTests()); it("① 地圖抓得到:地圖照舊注入,且知識段仍在", async () => { const text = await buildServerInstructions(makeEnv(MAP_OK), SERVICE); expectPointsAtKnowledge(text); expect(text).toContain("【藏書地圖】"); expect(text).toContain("kb:leo 的知識庫主庫"); // 地圖成功時不該同時出現「沒取到」的話 expect(text).not.toContain("【藏書地圖:這次沒取到】"); }); it("② 地圖抓不到(HTTP 500):明講「沒取到」,不靜默、也不等於沒有知識", async () => { const text = await buildServerInstructions(makeEnv(() => new Response("boom", { status: 500 })), SERVICE); expectPointsAtKnowledge(text); expect(text).toContain("【藏書地圖:這次沒取到】"); expect(text).toContain("這是「地圖沒拿到」,不是「這裡沒有知識」"); }); it("② 地圖逾時/binding 爆炸:同樣明講,不擋連線(不 throw)", async () => { const env = { KBDB: { fetch: async () => { throw new Error("kbdb down"); }, }, } as unknown as Env; const text = await buildServerInstructions(env, SERVICE); expectPointsAtKnowledge(text); expect(text).toContain("【藏書地圖:這次沒取到】"); }); it("② 地圖回空清單:也算沒取到,不可以長得像「這裡沒有知識」", async () => { const text = await buildServerInstructions( makeEnv(() => new Response(JSON.stringify({ success: true, libraries: [], count: 0 }))), SERVICE, ); expect(text).toContain("【藏書地圖:這次沒取到】"); expect(text).toContain("不是「這裡沒有知識」"); }); it("③ 舊 token(stale):講成身分問題+給可執行的修法,仍指向知識庫", async () => { const text = await buildServerInstructions(makeEnv(MAP_OK), STALE); expectPointsAtKnowledge(text); expect(text).toContain("舊版簽發的 token"); expect(text).toContain("identity_missing"); expect(text).toContain("重新連線"); // 🔴 不可以講成「知識庫是空的」 expect(text).toContain("這不代表知識庫是空的"); }); it("stale 不去打 KBDB(地圖本身就是情報,舊 token 不給)", async () => { let called = 0; const env = { KBDB: { fetch: async () => { called += 1; return MAP_OK(); }, }, } as unknown as Env; await buildServerInstructions(env, STALE); expect(called).toBe(0); }); it("Arcrun 指路段照舊存在(知識段是新增的,不是取代)", async () => { const text = await buildServerInstructions(makeEnv(MAP_OK), SERVICE); expect(text).toContain("# Arcrun — 你已經配備了這套工具,別上網找"); expect(text).toContain("【先讀這裡】"); // 步驟 4 不再指名一個可能沒被 seed 的 slug(leo21c 實測連 INDEX 都沒有) expect(text).toContain("arcrun_list_skills()"); expect(text).not.toContain("arcrun_get_skill('INDEX')"); }); });