Files
Arcrun/registry/tests/sandboxAcceptance.test.ts
T
Leo 202a5ab8d6 feat(registry): Phase 3 零件投稿靜態把關 + component-gatekeeping SDD
新 SDD .agents/specs/component-gatekeeping/(richblack 確認,含 venue 修訂 + 信任模型)。

registry 端靜態把關(CF Worker 可跑,不執行 wasm):
- G1 detectFakeComponent: 外部 URL/domain + http_request 子集偵測,硬擋退稿指回 recipe
- G3 wasmImports: 解析 wasm import section,只准 wasi_snapshot_preview1 + u6u 白名單
- G5/G6: unimplemented_steps 明列 gherkin/cold_start/runtime_compat,不假綠(§3c/§7)
- gherkin_evidence 一致性驗證(投稿者本地跑,registry 不重跑——CF 禁 runtime 編譯 wasm)

把關範圍:公共庫 + self-hosted 私人庫同一套(design §0.0)。
信任模型(design §4.5):Gherkin 全綠≠安全;純 WASI 沙箱框死能力才是發佈底氣;
第一期 evidence 可造假(誠實標明),平台重跑列未來。

hook: pre-write-guard 白名單加 component-gatekeeping / component-registry-canon SDD 目錄。

測試: sandboxAcceptance.test.ts 4 綠(含 G1 假零件被擋)。

待續(同 SDD): G4 CLI 投稿指令本地跑 Gherkin、G0 人類閘門、R5 白名單+本機 hook。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 17:53:03 +08:00

84 lines
3.5 KiB
TypeScript
Raw 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.
// 單元測試:sandboxAcceptance
// Requirements: 2.1, 2.2
import { describe, it, expect } from 'vitest';
import { runSandboxAcceptance } from '../src/actions/sandboxAcceptance';
import type { ComponentContract } from '../src/types';
const BASE_CONTRACT: ComponentContract = {
canonical_id: 'validate_json',
display_name: 'JSON 格式驗證器',
category: 'logic',
version: 'v1',
wasi_target: 'preview1',
stability: 'floating',
runtime_compat: ['cf-workers', 'wazero'],
constraints: {
max_size_kb: 100,
max_cold_start_ms: 50,
no_network_syscall: true,
io_model: 'stdin_stdout_json',
},
input_schema: { type: 'object' },
output_schema: { type: 'object' },
gherkin_tests: [
{ scenario: 'happy', given: '{}', then_contains: '{}' },
{ scenario: 'error', given: '{}', then_contains: '{}' },
],
};
// 建立合法的小型 WASM(最小 WASM magic + version header
function makeMinimalWasm(extraBytes = 0): Uint8Array {
const magic = [0x00, 0x61, 0x73, 0x6d]; // \0asm
const version = [0x01, 0x00, 0x00, 0x00];
const padding = new Array(extraBytes).fill(0x00);
return new Uint8Array([...magic, ...version, ...padding]);
}
describe('runSandboxAcceptance', () => {
// 註:G4 Gherkin 真實作後,minimal wasm(只有 magic header)無法 instantiate
// 會在 gherkin_tests 步驟失敗。同步步驟(size/syscall/fake)的失敗測試仍有效,
// 因為它們在 Gherkin 之前就擋下。「全通過」需真實零件 wasm,移至整合測試。
it('步驟 (a):體積超過上限時失敗(在 Gherkin 前擋下)', async () => {
const contract = { ...BASE_CONTRACT, constraints: { ...BASE_CONTRACT.constraints, max_size_kb: 1 } };
const wasm = makeMinimalWasm(2000); // > 1KB
const result = await runSandboxAcceptance(wasm, contract);
expect(result.success).toBe(false);
expect(result.failed_step).toBe('size_check');
expect(result.reason).toContain('超過上限');
expect(result.guide_anchor).toBeDefined();
expect(result.canonical_id).toBe('validate_json');
expect(result.version).toBe('v1');
});
it('步驟:含禁止 syscall 時失敗', async () => {
const encoder = new TextEncoder();
const syscallBytes = encoder.encode('sock_connect');
const wasm = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ...syscallBytes]);
const result = await runSandboxAcceptance(wasm, BASE_CONTRACT);
expect(result.success).toBe(false);
expect(result.failed_step).toBe('syscall_scan');
expect(result.reason).toContain('sock_connect');
});
it('size_check 失敗後不執行後續步驟', async () => {
const encoder = new TextEncoder();
const syscallBytes = encoder.encode('sock_connect');
const padding = new Uint8Array(2000);
const wasm = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ...syscallBytes, ...padding]);
const contract = { ...BASE_CONTRACT, constraints: { ...BASE_CONTRACT.constraints, max_size_kb: 1 } };
const result = await runSandboxAcceptance(wasm, contract);
expect(result.failed_step).toBe('size_check');
});
it('G1contract 含外部 URL 的假零件被擋(最先擋)', async () => {
const contract = { ...BASE_CONTRACT, canonical_id: 'fake_gmail', description: '打 https://gmail.googleapis.com 寄信' };
const wasm = makeMinimalWasm(10);
const result = await runSandboxAcceptance(wasm, contract);
expect(result.success).toBe(false);
expect(result.failed_step).toBe('fake_component_scan');
expect(result.reason).toContain('recipe');
});
});