/** * deploy-code-component.test.ts — 離線驗「code 零件接進部署流程」(Arcrun#4 後續)。 * * 驗三件事(不打網路、不部署): * 1. 部署清單含 code:discoverWorkerDirs 在產物齊全時把 registry/components/code 收進 tier1; * 缺 vendored quickjs.wasm 時誠實跳過(gate 精神比照 tier1 component.wasm)。 * 2. 產物完整:repo 內 code 零件的部署必要檔存在且 quickjs.wasm 已被 git 追蹤 * (= 會進 Gitea archive tarball,acr update 才拿得到);共享依賴 SHARED_DEPLOY_DEPS * 涵蓋 code 零件 package.json 的全部 runtime deps(drift 守門)。 * 3. 注入正確:stripOfficialOnlyBindings 對 code 的 wrangler.toml 剝掉官方 route * (code.arcrun.dev),保留 workers_dev / COMPONENT_ID → self-hosted 落自己帳號的 workers.dev。 * * 用 Node 內建 test runner(node --test,零額外依賴;Node ≥22.18 自帶 TS type-stripping)。 */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, statSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { discoverWorkerDirs, SELF_CONTAINED_COMPONENT_WORKERS, SHARED_DEPLOY_DEPS, stripOfficialOnlyBindings, injectMultiTenant, } from '../src/lib/deploy.ts'; const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const CODE_DIR = join(REPO_ROOT, 'registry', 'components', 'code'); // ── 1. 部署清單 ─────────────────────────────────────────────────────────────── function makeFixtureRoot(opts: { withWasm: boolean; withToml?: boolean }): string { const root = mkdtempSync(join(tmpdir(), 'arcrun-test-root-')); const dir = join(root, 'registry', 'components', 'code'); mkdirSync(join(dir, 'vendor'), { recursive: true }); if (opts.withToml !== false) writeFileSync(join(dir, 'wrangler.toml'), 'name = "arcrun-code"\n'); if (opts.withWasm) writeFileSync(join(dir, 'vendor', 'quickjs.wasm'), 'fake-wasm'); return root; } test('discoverWorkerDirs:code 產物齊全 → 進 tier1(部署清單含 code)', () => { const root = makeFixtureRoot({ withWasm: true }); const { tier1 } = discoverWorkerDirs(root); assert.ok(tier1.some(d => d.endsWith(join('registry', 'components', 'code'))), `tier1 應含 code 目錄,實得:${JSON.stringify(tier1)}`); }); test('discoverWorkerDirs:缺 vendor/quickjs.wasm → 誠實跳過(不讓 wrangler 因缺檔失敗)', () => { const root = makeFixtureRoot({ withWasm: false }); const { tier1, tier2 } = discoverWorkerDirs(root); assert.ok(!tier1.concat(tier2).some(d => d.includes('code')), 'wasm 缺席時不應把 code 排進部署清單'); }); test('discoverWorkerDirs:缺 wrangler.toml → 跳過', () => { const root = makeFixtureRoot({ withWasm: true, withToml: false }); const { tier1 } = discoverWorkerDirs(root); assert.equal(tier1.length, 0); }); test('SELF_CONTAINED_COMPONENT_WORKERS 宣告 code 目錄與必要產物', () => { const code = SELF_CONTAINED_COMPONENT_WORKERS.find(w => w.dir.join('/') === 'registry/components/code'); assert.ok(code, '清單應含 registry/components/code'); assert.ok(code!.requires.some(r => r.join('/') === 'vendor/quickjs.wasm')); }); // ── 2. 產物完整(對 repo 實體檢查) ────────────────────────────────────────── test('repo 內 code 零件部署必要檔齊全,quickjs.wasm 已 commit(會進 Gitea archive)', () => { for (const f of ['wrangler.toml', 'index.ts', 'sandbox.mjs', join('vendor', 'quickjs.wasm')]) { assert.ok(statSync(join(CODE_DIR, f)).isFile(), `缺 ${f}`); } assert.ok(statSync(join(CODE_DIR, 'vendor', 'quickjs.wasm')).size > 100_000, 'quickjs.wasm 尺寸異常(應約 491KB)'); // git 追蹤 = 會被 git archive 打包進 Gitea 下載物(.gitignore 放行是否生效的最終證據)。 const tracked = execFileSync('git', ['ls-files', '--', 'registry/components/code/vendor/quickjs.wasm'], { cwd: REPO_ROOT, encoding: 'utf8' }).trim(); assert.equal(tracked, 'registry/components/code/vendor/quickjs.wasm', 'vendor/quickjs.wasm 未被 git 追蹤(.gitignore 放行失效或忘了 git add)'); }); // vendored quickjs.wasm 的內容指紋(@jitl/quickjs-wasmfile-release-sync ^0.32.0 的 // emscripten-module.wasm)。升套件版本卻沒重跑 vendor(npm install → postinstall)再 commit // → 這裡紅燈,防「package.json 升了、部署物 wasm 還是舊版」的 drift(審查小記 2026-07-07)。 // 升版 SOP:cd registry/components/code && npm install → commit vendor/quickjs.wasm → 更新此常數。 const QUICKJS_WASM_SHA256 = '105c3bed22d457e43e3d1c3c1c6959fda62a8fe06f0fc8a985303c3a2be72232'; test('vendored quickjs.wasm 內容指紋一致(升版沒重 vendor 就紅燈)', () => { const buf = readFileSync(join(CODE_DIR, 'vendor', 'quickjs.wasm')); const sha = createHash('sha256').update(buf).digest('hex'); assert.equal(sha, QUICKJS_WASM_SHA256, 'vendor/quickjs.wasm 指紋與紀錄不符:若剛升 quickjs 套件版本,重跑 npm install 讓 postinstall ' + '重 vendor、commit 新 wasm,並同步更新 QUICKJS_WASM_SHA256'); }); test('SHARED_DEPLOY_DEPS 涵蓋 code 零件全部 runtime deps(drift 守門)', () => { const pkg = JSON.parse(readFileSync(join(CODE_DIR, 'package.json'), 'utf8')) as { dependencies?: Record }; for (const [dep, ver] of Object.entries(pkg.dependencies ?? {})) { assert.ok(dep in SHARED_DEPLOY_DEPS, `code 零件 runtime dep「${dep}」不在 SHARED_DEPLOY_DEPS → 共享安裝路徑下 esbuild 會解析失敗`); assert.equal(SHARED_DEPLOY_DEPS[dep], ver, `「${dep}」版本 drift:SHARED_DEPLOY_DEPS=${SHARED_DEPLOY_DEPS[dep]} vs 零件 package.json=${ver}`); } }); // ── 3. 注入正確(用 repo 內真 wrangler.toml) ──────────────────────────────── test('stripOfficialOnlyBindings:剝掉 code.arcrun.dev route,保留 workers_dev + COMPONENT_ID', () => { const toml = readFileSync(join(CODE_DIR, 'wrangler.toml'), 'utf8'); assert.match(toml, /code\.arcrun\.dev/, '前提:repo toml 應含官方 route(官方 CI 部署用)'); const out = stripOfficialOnlyBindings(toml); assert.ok(!/\[\[routes\]\]|zone_name|code\.arcrun\.dev/.test(out), `官方 route 應被剝除:\n${out}`); assert.match(out, /workers_dev\s*=\s*true/, 'workers_dev 須保留(self-hosted 靠它對外)'); assert.match(out, /COMPONENT_ID\s*=\s*"code"/, '[vars] COMPONENT_ID 須保留'); assert.match(out, /name\s*=\s*"arcrun-code"/, 'worker 名須保留(cypher wasmWorkerUrl 慣例 arcrun-{kebab})'); }); test('injectMultiTenant:code toml 有 [vars] → 插入 MULTI_TENANT="false"(無害,與其他 worker 一致)', () => { const toml = readFileSync(join(CODE_DIR, 'wrangler.toml'), 'utf8'); const out = injectMultiTenant(toml); assert.match(out, /MULTI_TENANT\s*=\s*"false"/); }); // ── 4. acr parts 清單含 code ───────────────────────────────────────────────── // (parts.ts 內部以 .js 副檔名 import 相鄰模組,node --test 的 type-stripping 不重寫 // 副檔名 → 無法直接 import;以原始碼文字驗證清單含 code 條目。tsc 另保證型別正確。) test('BUILTIN_COMPONENTS(acr parts 靜態清單)含 code 條目', () => { const src = readFileSync(join(REPO_ROOT, 'cli', 'src', 'commands', 'parts.ts'), 'utf8'); assert.match(src, /canonical_id: 'code'/, 'parts.ts BUILTIN_COMPONENTS 應含 code'); assert.match(src, /component: code/, 'code 條目應含 config_example(component: code)'); });