Files
Arcrun/cypher-executor/tests/kbdb-records-backfill-library-proxy.test.ts
T
uncle6me-web 179dd60571 fix(kbdb): 三元組版標庫批次補存量+對外通道(Arcrun#87 二次收尾)
藏書地圖(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>
2026-08-13 11:12:56 +08:00

101 lines
4.4 KiB
TypeScript
Raw 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.
/**
* 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 statusowner_id 同樣強制用租戶身份,query 參數透傳
*
* KBDB 打 fetchMock 假 hostdisableNetConnect——測試絕不外連。
*/
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);
});
});