525faaf5d0
病因:/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,與本次改動無關)。
117 lines
5.9 KiB
TypeScript
117 lines
5.9 KiB
TypeScript
/**
|
||
* 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');
|
||
});
|
||
});
|