Files
Arcrun/cli/tests/deploy-code-component.test.ts
Leo 621cb8d948 feat(cli): code 零件接進 acr init/update 部署流程(自足 Worker 進部署清單 + vendored wasm 進 repo)
動機(Arcrun#4 後續,leo 批准):code 零件只有原始碼(registry/components/code),
downloadAndDeploy 完全沒涵蓋它——tier1 只掃 .component-builds/*(TinyGo 家族,要求
component.wasm),tier2 寫死四個引擎。merge 後用戶跑 acr update 應真的裝上 code。

接法(實查後裁定):
- code 是自足 Worker(quickjs-emscripten wasmfile variant,非 TinyGo;見其 index.ts 頭註),
  「缺 wasm」的真相是「這一類根本不在部署清單」+「vendored quickjs.wasm 是 gitignored
  build 產物、不進 archive」。
- deploy.ts 新增 SELF_CONTAINED_COMPONENT_WORKERS(目錄 + 必要產物 gate,比照 tier1
  component.wasm gate 的誠實跳過精神),discoverWorkerDirs 將其排進 tier1(零件先於引擎)。
- vendored quickjs.wasm(491KB)commit 進 repo:.gitignore 放行(完全比照
  !.component-builds/**/component.wasm 的「部署物 wasm 例外」先例)→ acr update 從
  Gitea archive 直接拿到,更新不需 npm build 工具鏈。
- 共享依賴抽成 SHARED_DEPLOY_DEPS 並補 quickjs-emscripten-core + wasmfile variant
  (版本對齊零件 package.json,測試看守 drift)→ root 裝一次、esbuild 往上 resolve。
- 注入零改動:既有 stripOfficialOnlyBindings 剝掉 code.arcrun.dev 官方 route、
  workers_dev=true 保留 → self-hosted 自動落 arcrun-code.<sub>.workers.dev。
- parts.ts BUILTIN_COMPONENTS 加 code 條目(issue #13 W3:零件=靜態清單)→ acr parts 可見。
  實查:init 本來就不對 registry 註冊任何零件(registry index 是官方 backfill 腳本的事),
  故「比照其他零件」=進 BUILTIN_COMPONENTS 即對齊。

測試:cli/tests/deploy-code-component.test.ts 9 顆全綠(node --test,零新依賴):
部署清單含 code / 缺產物誠實跳過 / wasm 已 git 追蹤(會進 archive)/ SHARED_DEPLOY_DEPS
涵蓋零件全部 runtime deps + 版本一致 / route 剝除與 [vars] 保留 / parts 清單含 code。
tsc --noEmit 綠;零件自身 12 顆 vitest 綠(沙箱行為未動)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d5jDbuqT5Htwv3Q88XXKk
2026-07-07 09:01:11 +00:00

138 lines
8.0 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.
/**
* deploy-code-component.test.ts — 離線驗「code 零件接進部署流程」(Arcrun#4 後續)。
*
* 驗三件事(不打網路、不部署):
* 1. 部署清單含 codediscoverWorkerDirs 在產物齊全時把 registry/components/code 收進 tier1
* 缺 vendored quickjs.wasm 時誠實跳過(gate 精神比照 tier1 component.wasm)。
* 2. 產物完整:repo 內 code 零件的部署必要檔存在且 quickjs.wasm 已被 git 追蹤
* = 會進 Gitea archive tarballacr update 才拿得到);共享依賴 SHARED_DEPLOY_DEPS
* 涵蓋 code 零件 package.json 的全部 runtime depsdrift 守門)。
* 3. 注入正確:stripOfficialOnlyBindings 對 code 的 wrangler.toml 剝掉官方 route
* code.arcrun.dev),保留 workers_dev / COMPONENT_ID → self-hosted 落自己帳號的 workers.dev。
*
* 用 Node 內建 test runnernode --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('discoverWorkerDirscode 產物齊全 → 進 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)。升套件版本卻沒重跑 vendornpm install → postinstall)再 commit
// → 這裡紅燈,防「package.json 升了、部署物 wasm 還是舊版」的 drift(審查小記 2026-07-07)。
// 升版 SOPcd 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 depsdrift 守門)', () => {
const pkg = JSON.parse(readFileSync(join(CODE_DIR, 'package.json'), 'utf8')) as
{ dependencies?: Record<string, string> };
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}」版本 driftSHARED_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('injectMultiTenantcode 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_COMPONENTSacr 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_examplecomponent: code');
});