From 06ff0e21ced57c4a1f9d4d4a0822db2e051ff7a8 Mon Sep 17 00:00:00 2001 From: arcrun-subagent Date: Tue, 7 Jul 2026 08:42:51 +0000 Subject: [PATCH] =?UTF-8?q?fix(cypher-executor):=20component-loader=20?= =?UTF-8?q?=E7=99=BD=E5=90=8D=E5=96=AE=E8=A3=9C=20code=20=E9=9B=B6?= =?UTF-8?q?=E4=BB=B6=EF=BC=88#29=20=E7=99=BC=E7=8F=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WASM_HTTP_RUNNER_IDS 漏 'code',導致 workflow 寫 `component: code` 在解析鏈 step 1-6 全不命中後落到 step 8 直接「找不到零件」——本 PR 的 graph_neighbors 範例正用 code 節點,故歸此分支。 URL 推導:走既有 wasmWorkerUrl 通用推導 arcrun-code.{WORKER_SUBDOMAIN}.workers.dev (WORKER_SUBDOMAIN 來自 wrangler.toml [vars],self-hosted 由 deploy 注入自己的 subdomain)——無寫死官方 code.arcrun.dev,self-hosted 天然成立。 測試:tests/component-loader-code.test.ts(2 測)——`code` 解析成 runner 不 throw、 stub fetch 證 runner 打 arcrun-code.{sub}.workers.dev;wasmWorkerUrl 對任意 subdomain 推導正確。tsc 乾淨、全套 50 passed(1 失敗為 main 既有、無關)。 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_015d5jDbuqT5Htwv3Q88XXKk --- cypher-executor/src/lib/component-loader.ts | 5 ++ .../tests/component-loader-code.test.ts | 56 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 cypher-executor/tests/component-loader-code.test.ts diff --git a/cypher-executor/src/lib/component-loader.ts b/cypher-executor/src/lib/component-loader.ts index 5eec1a7..d438357 100644 --- a/cypher-executor/src/lib/component-loader.ts +++ b/cypher-executor/src/lib/component-loader.ts @@ -36,6 +36,11 @@ import type { Bindings, ComponentRunner, ServiceBinding } from '../types'; const WASM_HTTP_RUNNER_IDS: ReadonlySet = new Set([ // 通用 HTTP 零件 'http_request', + // 通用 code 零件(sandbox inline JS,Arcrun#10 / 07-thin-shell §3.5 code-node):獨立 Worker, + // URL 走 wasmWorkerUrl 通用推導(arcrun-code.{WORKER_SUBDOMAIN}.workers.dev, + // self-hosted 由 WORKER_SUBDOMAIN var 注入自己的 subdomain,無寫死官方域名)。 + // 漏這行 = workflow 寫 `component: code` 落到 step 8 直接「找不到零件」(#29 發現)。 + 'code', // gmail / telegram / line_notify / google_sheets 已降級為 recipe(2026-05-29 Phase 2): // recipe:gmail_send / telegram_send / line_notify_send / google_sheets_read|append // 走 step 6 KV recipe 解析,不再是零件。零件目錄已刪。 diff --git a/cypher-executor/tests/component-loader-code.test.ts b/cypher-executor/tests/component-loader-code.test.ts new file mode 100644 index 0000000..4c57c45 --- /dev/null +++ b/cypher-executor/tests/component-loader-code.test.ts @@ -0,0 +1,56 @@ +/** + * component-loader:`code` 零件解析測試(#29 發現的白名單缺口)。 + * + * 背景:workflow 寫 `component: code` 時,component-loader 解析鏈 step 1-6 都不命中 + * (非 builtin / 非 URL / 非 hash / 非邏輯零件 binding / 非 recipe),必須靠 step 7 的 + * WASM_HTTP_RUNNER_IDS 白名單 → wasmWorkerUrl 通用推導。白名單漏 `code` 就會落到 + * step 8 直接丟「找不到零件」——graph_neighbors 等用 code 節點的 workflow 全滅。 + * + * 驗證: + * 1. `code` 能被解析成 runner(不 throw)。 + * 2. runner 打的 URL = arcrun-code.{WORKER_SUBDOMAIN}.workers.dev(通用推導、無寫死 + * 官方 code.arcrun.dev → self-hosted 換 WORKER_SUBDOMAIN 即落地)。 + */ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { env } from 'cloudflare:test'; +import { createComponentLoader, wasmWorkerUrl } from '../src/lib/component-loader'; +import type { Bindings } from '../src/types'; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('component-loader:code 零件解析(WASM HTTP runner 白名單)', () => { + it('wasmWorkerUrl 通用推導:code → arcrun-code.{subdomain}.workers.dev(self-hosted 換 subdomain 即成立)', () => { + expect(wasmWorkerUrl('code', 'uncle6-me')).toBe('https://arcrun-code.uncle6-me.workers.dev'); + expect(wasmWorkerUrl('code', 'my-selfhosted-sub')).toBe('https://arcrun-code.my-selfhosted-sub.workers.dev'); + }); + + it('component: code 能被解析成 runner,且 fetch 打 subdomain 推導的 URL', async () => { + const fakeEnv = { + ...env, + WORKER_SUBDOMAIN: 'test-sub', + } as unknown as Bindings; + + const loader = createComponentLoader(fakeEnv); + // 白名單漏 `code` 時這裡就 throw「找不到零件 "code"」(#29 的故障型態) + const runner = await loader('code'); + expect(typeof runner).toBe('function'); + + // stub fetch 捕 URL:證明 runner 打的是通用推導 URL,非寫死官方域名 + const calledUrls: string[] = []; + const fetchSpy = vi.fn(async (url: unknown) => { + calledUrls.push(String(url)); + return new Response(JSON.stringify({ success: true, data: { ok: true } }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }); + vi.stubGlobal('fetch', fetchSpy); + + const result = await runner({ code: 'return {ok:true}', input: {} }) as Record; + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(calledUrls[0]).toBe('https://arcrun-code.test-sub.workers.dev'); + expect(result.success).toBe(true); + }); +});