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
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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 { 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)');
|
||||
});
|
||||
|
||||
test('SHARED_DEPLOY_DEPS 涵蓋 code 零件全部 runtime deps(drift 守門)', () => {
|
||||
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}」版本 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)');
|
||||
});
|
||||
Reference in New Issue
Block a user