/** * POST /kbdb/records/backfill-library + GET .../status proxy 測試(Arcrun#87 二次收尾,2026-08-13) * * 背景:基本盤 kbdb/src/routes/records.ts 新增了三元組版的批次標庫補存量端點 * (backfillTripletLibraryTags——藏書地圖讀的是三元組 record 自己的 'library' slot,跟 * entries 版 backfill-library 補的 metadata_json.library 是不同存放處)。這條 cypher proxy * 之前完全沒轉發這兩支——同 kbdb-records-patch-proxy.test.ts 那次的破口(能力在 base, * 插件/工作流打不到)。純轉發,owner_id 強制用租戶身份(不信任 caller 自帶 owner_id, * 同本檔既有 POST /kbdb/records 的慣例)。 * * 驗證 IO 接線(聚合真身在 KBDB 基本盤,這裡只測轉發,比照 kbdb-records-patch-proxy.test.ts 慣例): * 1. 租戶閘:無 X-Arcrun-API-Key → 401 不碰 KBDB * 2. body 沒有 library → 400,不轉發 * 3. 轉發:owner_id 一律用租戶身份覆蓋(即使 caller 自帶了別的 owner_id 也被忽略) * 4. GET status:owner_id 同樣強制用租戶身份,query 參數透傳 * * KBDB 打 fetchMock 假 host+disableNetConnect——測試絕不外連。 */ import { SELF, fetchMock } from 'cloudflare:test'; import { beforeAll, afterEach, describe, it, expect } from 'vitest'; const KEY = { 'X-Arcrun-API-Key': 'leo', 'Content-Type': 'application/json' }; beforeAll(() => { fetchMock.activate(); fetchMock.disableNetConnect(); }); afterEach(() => fetchMock.assertNoPendingInterceptors()); describe('POST /kbdb/records/backfill-library — 租戶閘', () => { it('無 X-Arcrun-API-Key → 401,不碰 KBDB', async () => { const res = await SELF.fetch('http://localhost/kbdb/records/backfill-library', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ library: 'arcrun' }), }); expect(res.status).toBe(401); }); }); describe('POST /kbdb/records/backfill-library — 參數驗證', () => { it('body 沒有 library → 400,不轉發', async () => { const res = await SELF.fetch('http://localhost/kbdb/records/backfill-library', { method: 'POST', headers: KEY, body: JSON.stringify({ source_prefix: 'gitea:Leo/Arcrun@' }), }); expect(res.status).toBe(400); }); }); describe('POST /kbdb/records/backfill-library — 轉發', () => { it('owner_id 一律用租戶身份覆蓋,即使 caller 自帶了別的 owner_id', async () => { fetchMock .get('https://kbdb.test') .intercept({ path: '/records/backfill-library', method: 'POST', body: JSON.stringify({ library: 'arcrun', owner_id: 'leo', // 來自 X-Arcrun-API-Key,不是 body 裡的 'someone-else' triplet_template: undefined, source_prefix: 'gitea:Leo/Arcrun@', limit: 200, }), }) .reply(200, { success: true, library: 'arcrun', scanned: 208, tagged: 200, remaining: 8, quota_limit: 2000, quota_used_today: 200, quota_exceeded: true }); const res = await SELF.fetch('http://localhost/kbdb/records/backfill-library', { method: 'POST', headers: KEY, body: JSON.stringify({ library: 'arcrun', owner_id: 'someone-else', source_prefix: 'gitea:Leo/Arcrun@', limit: 200 }), }); expect(res.status).toBe(200); const data = (await res.json()) as { success: boolean; tagged: number; remaining: number }; expect(data.success).toBe(true); expect(data.tagged).toBe(200); expect(data.remaining).toBe(8); }); }); describe('GET /kbdb/records/backfill-library/status — 租戶閘 + 轉發', () => { it('無 X-Arcrun-API-Key → 401', async () => { const res = await SELF.fetch('http://localhost/kbdb/records/backfill-library/status?source_prefix=gitea:Leo/Arcrun@'); expect(res.status).toBe(401); }); it('owner_id 強制用租戶身份,其餘 query 參數透傳', async () => { fetchMock .get('https://kbdb.test') .intercept({ path: '/records/backfill-library/status?owner_id=leo&source_prefix=gitea%3ALeo%2FArcrun%40', method: 'GET' }) .reply(200, { success: true, pending: 8 }); const res = await SELF.fetch('http://localhost/kbdb/records/backfill-library/status?source_prefix=gitea:Leo/Arcrun@', { headers: KEY, }); expect(res.status).toBe(200); const data = (await res.json()) as { success: boolean; pending: number }; expect(data.success).toBe(true); expect(data.pending).toBe(8); }); });