179dd60571
藏書地圖(library-map.ts)讀的是三元組 record 自己的 'library' slot(entry_values), 不是 entries.metadata_json.library——Arcrun#85 建的 POST /entries/backfill-library 補的是後者,補了地圖依然是 0。既有能補三元組 library 值的通道只有單筆 PATCH /kbdb/records/:id(b6ef0f0,已在 main),母體上千筆時逐筆呼叫不現實,也沒有 節流/冪等保護。 新增: - kbdb/src/actions/library-backfill.ts:backfillTripletLibraryTags + tripletLibraryBackfillStatus,安全原則與既有 entries 版逐條對齊——呼叫端決定 library 值+source_prefix 篩選(base 不猜語意)、冪等(NOT EXISTS 找缺 library 的 那半)、與 entries 版/embed reconcile 共用同一顆 D69 每日 D1 寫入額度、owner_id 必填、 寫入沿用既有 updateRecord(不手刻第二套 entry_values UPSERT SQL)。 - kbdb/src/routes/records.ts:POST /records/backfill-library + GET .../status。 - cypher-executor/src/routes/kbdb-proxy.ts:對外通道,owner_id 強制用租戶身份(同本檔 既有 POST /kbdb/records 慣例,不信任 caller 自帶 owner_id)。 測試:kbdb 新增 5 案(含跨租戶隔離/冪等/D69 共用額度截斷/status 統計),全套 218/218 綠燈;cypher-executor 新增 5 案(租戶閘/參數驗證/轉發/owner_id 覆蓋), 兩份既有失敗(auth.test.ts ExecutionContext 型別缺陷、portal-data/portal-admin/ console-library-map-page/executor.test.ts 訊息措辭)複驗與本次改動無關(stash 前後 一致,pre-existing)。 紅線:未併 main、未部署、未對 leo21c 執行任何實際 backfill(缺安全的寫入通道—— MCP 工具集無 update/patch record 能力,CLI 全域設定指著 leo21c 不可裸跑)。 批次執行是下一步,需總管/leo 決定執行方式與時機。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
4.4 KiB
TypeScript
101 lines
4.4 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|