/** * Arcrun#88:「零件目錄會說『這顆零件不存在』,但同一台實例上那顆零件跑得動」 * * 病史(leo21c 實例實測,2026-08-11): * `/cypher/search` 對 `if_control`/`http_request` 回 `not_found`, * 但兩者其實由 component-loader.ts 直接解析(LOGIC_BINDING_MAP/WASM_HTTP_RUNNER_IDS), * 從不查 registry;registry catalog 端點在該實例回 404(`GET /components/catalog` → * `{"success":false,"error":"零件 catalog 不存在"}`),search 因此誤判成「兩庫都查過沒有」。 * * 本測試複現病史的環境條件(wrangler.test.toml 未設 WORKER_SUBDOMAIN → registryBase * undefined → catalog.status='unreachable',等價於「registry 完全連不到」), * 驗證修法:RUNTIME_NATIVE_COMPONENT_IDS 的成員必須在 registry 查詢**之前**就短路成 found, * 不受 registry 健康狀態影響——因為它們的存在性從不依賴 registry。 */ import { describe, it, expect, vi, afterEach } from 'vitest'; import { parseTriplets } from '../src/actions/triplet-parser'; import { searchNodes } from '../src/actions/search-nodes'; afterEach(() => { vi.unstubAllGlobals(); }); const IF_CONTROL_TRIPLETS = [ 'input >> ON_SUCCESS >> if_control', ]; const HTTP_REQUEST_TRIPLETS = [ 'input >> ON_SUCCESS >> http_request', ]; const MULTI_BUILTIN_TRIPLETS = [ 'input >> ON_SUCCESS >> switch', 'input >> ON_SUCCESS >> filter', 'input >> ON_SUCCESS >> code', ]; const FAKE_COMPONENT_TRIPLETS = [ 'input >> ON_SUCCESS >> totally_made_up_component_xyz', ]; describe('Arcrun#88:執行期原生零件不受 registry 健康狀態影響', () => { it('if_control(LOGIC_BINDING_MAP 成員)在 registry 不可達時仍回 found', async () => { const parsed = parseTriplets(IF_CONTROL_TRIPLETS); expect(parsed).not.toBeNull(); const { nodeResults, missingNodes } = await searchNodes(parsed!, undefined, { // 無 WORKER_SUBDOMAIN/REGISTRY_BASE_URL → registryBase undefined → catalog unreachable }); expect(nodeResults.if_control.status).toBe('found'); expect(nodeResults.if_control.source).toBe('builtin'); // if_control 會分岔,branch_hint 應隨 found 一併附上(不必逐顆再查一次) expect(nodeResults.if_control.branch_hint?.edge_types).toEqual(['ON_TRUE', 'ON_FALSE']); expect(missingNodes).not.toContain('if_control'); }); it('http_request(WASM_HTTP_RUNNER_IDS 成員)在 registry 不可達時仍回 found', async () => { const parsed = parseTriplets(HTTP_REQUEST_TRIPLETS); const { nodeResults, missingNodes } = await searchNodes(parsed!, undefined, {}); expect(nodeResults.http_request.status).toBe('found'); expect(nodeResults.http_request.source).toBe('builtin'); expect(missingNodes).not.toContain('http_request'); }); it('switch/filter/code(同一批白名單的其他成員)也回 found,不逐一漏網', async () => { const parsed = parseTriplets(MULTI_BUILTIN_TRIPLETS); const { nodeResults } = await searchNodes(parsed!, undefined, {}); expect(nodeResults.switch.status).toBe('found'); expect(nodeResults.filter.status).toBe('found'); expect(nodeResults.code.status).toBe('found'); }); it('registry 完全連不到時,真正不存在的名字誠實回 unknown(不敢空口說沒有——既有行為,修法沒有動它)', async () => { const parsed = parseTriplets(FAKE_COMPONENT_TRIPLETS); const { nodeResults } = await searchNodes(parsed!, undefined, {}); expect(nodeResults.totally_made_up_component_xyz.status).toBe('unknown'); }); it('registry 查得到但目錄是空的(複現 leo21c 實例 catalog 404 的真實症狀):真正不存在的名字回 not_found', async () => { // 複現生產實測:GET /components/catalog → HTTP 200 空陣列(本測試模擬「registry 活著但沒東西」, // 與 leo21c 實例的「404 零件 catalog 不存在」殊途同歸——都會落到「查得到、目錄無此零件」)。 vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ success: true, data: { components: [], count: 0 } }), { status: 200, headers: { 'Content-Type': 'application/json' }, }), )); const parsed = parseTriplets(FAKE_COMPONENT_TRIPLETS); const { nodeResults, missingNodes } = await searchNodes(parsed!, undefined, { WORKER_SUBDOMAIN: 'test-sub', }); expect(nodeResults.totally_made_up_component_xyz.status).toBe('not_found'); expect(missingNodes).toContain('totally_made_up_component_xyz'); }); it('registry 目錄是空的(catalog 通但無資料)時,執行期原生零件依然 found——這才是 Arcrun#88 的核心場景', async () => { // 這就是 leo21c 實例的真實狀態:registry 活著、目錄卻沒有任何一顆執行期原生零件的記錄 // (SUBMISSIONS_KV 從未收到 if_control/http_request 的 submit)。若沒有本次修法, // 這裡會落到「兩庫都查過沒有」→ not_found,正是 Arcrun#88 回報的病徵。 vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ success: true, data: { components: [], count: 0 } }), { status: 200, headers: { 'Content-Type': 'application/json' }, }), )); const parsed = parseTriplets(IF_CONTROL_TRIPLETS); const { nodeResults } = await searchNodes(parsed!, undefined, { WORKER_SUBDOMAIN: 'test-sub' }); expect(nodeResults.if_control.status).toBe('found'); expect(nodeResults.if_control.source).toBe('builtin'); }); it('target=recipe 明確只查 recipe 庫時,執行期原生零件不搶答 found(尊重使用者明確限庫)', async () => { const parsed = parseTriplets(IF_CONTROL_TRIPLETS); const { nodeResults } = await searchNodes(parsed!, undefined, {}, 'discover', 'recipe'); // if_control 從來不是 recipe,target=recipe 下不該被 builtin 短路成 found expect(nodeResults.if_control.status).not.toBe('found'); }); });