Files
Arcrun/cypher-executor/tests/search-nodes-runtime-native.test.ts
uncle6me-web 525faaf5d0 fix(cypher-executor): /cypher/search 不再誤報執行期原生零件 not_found(Arcrun#88)
病因:/cypher/search 只查 component registry(SUBMISSIONS_KV,經 submitComponent/
index-only 才有記錄);而 component-loader.ts 能直接解析、從不查 registry 的一整類
零件(trigger_workflow/BUILTIN_COMPONENTS/LOGIC_BINDING_MAP/WASM_HTTP_RUNNER_IDS,
如 if_control/http_request/switch)從未被 submit 過。leo21c 實例實測:
GET /components/catalog 回 404「零件 catalog 不存在」,search 因此對這些零件誠實地
回「兩庫都查過沒有」——但它們其實跑得動(leo 08-11 探測工作流已證)。

修法:從 component-loader.ts 匯出 RUNTIME_NATIVE_COMPONENT_IDS(既有三份執行期
解析白名單的聯集,非新清單),search-nodes.ts 在查 registry 之前先比對,
不受 registry 是否可達/是否已 backfill 影響。真正不存在的零件仍誠實回
not_found/unknown,not_found 的分型建議與相似候選機制不變。

刻意不做:不掃描 registry/components/* 目錄當清單來源——那含已標記待刪的死碼
(km_writer/kbdb_upsert_block),07-30 曾把這類死碼誤灌進 registry;也不投資
SUBMISSIONS_KV 的 backfill 腳本——decisions-summary.md D29 已定調 SUBMISSIONS_KV
併入「KV 退休戰」,不宜再加投資。

測試:cypher-executor/tests/search-nodes-runtime-native.test.ts 7 case 全綠,
複現 registry unreachable/registry 可達但目錄空(leo21c 實例的真實症狀)兩種情境。
全 suite 迴歸:357 pass(較修前 350 pass 多 7 個新測試),既有 14 個失敗與修前
數量、內容完全相同(pre-existing,與本次改動無關)。
2026-08-11 21:51:21 +08:00

117 lines
5.9 KiB
TypeScript
Raw Permalink 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.
/**
* Arcrun#88:「零件目錄會說『這顆零件不存在』,但同一台實例上那顆零件跑得動」
*
* 病史(leo21c 實例實測,2026-08-11):
* `/cypher/search` 對 `if_control``http_request` 回 `not_found`
* 但兩者其實由 component-loader.ts 直接解析(LOGIC_BINDING_MAPWASM_HTTP_RUNNER_IDS),
* 從不查 registryregistry 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_controlLOGIC_BINDING_MAP 成員)在 registry 不可達時仍回 found', async () => {
const parsed = parseTriplets(IF_CONTROL_TRIPLETS);
expect(parsed).not.toBeNull();
const { nodeResults, missingNodes } = await searchNodes(parsed!, undefined, {
// 無 WORKER_SUBDOMAINREGISTRY_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_requestWASM_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('switchfiltercode(同一批白名單的其他成員)也回 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 從來不是 recipetarget=recipe 下不該被 builtin 短路成 found
expect(nodeResults.if_control.status).not.toBe('found');
});
});