// 單元測試: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', () => { it('合法小型 WASM 通過所有步驟', () => { const wasm = makeMinimalWasm(10); const result = runSandboxAcceptance(wasm, BASE_CONTRACT); expect(result.success).toBe(true); expect(result.canonical_id).toBe('validate_json'); expect(result.version).toBe('v1'); }); it('步驟 (a):體積超過上限時失敗', () => { // max_size_kb = 1,但 wasm 超過 1KB const contract = { ...BASE_CONTRACT, constraints: { ...BASE_CONTRACT.constraints, max_size_kb: 1 } }; const wasm = makeMinimalWasm(2000); // > 1KB const result = 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('步驟 (c):含禁止 syscall 時失敗', () => { // 在 wasm bytes 中嵌入禁止的 syscall 字串 const syscallStr = 'sock_connect'; const encoder = new TextEncoder(); const syscallBytes = encoder.encode(syscallStr); const wasm = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ...syscallBytes]); const result = runSandboxAcceptance(wasm, BASE_CONTRACT); expect(result.success).toBe(false); expect(result.failed_step).toBe('syscall_scan'); expect(result.reason).toContain('sock_connect'); expect(result.guide_anchor).toBe('#syscall-constraints'); }); it('步驟 (c):含 path_open 時失敗', () => { const encoder = new TextEncoder(); const syscallBytes = encoder.encode('path_open'); const wasm = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ...syscallBytes]); const result = runSandboxAcceptance(wasm, BASE_CONTRACT); expect(result.success).toBe(false); expect(result.failed_step).toBe('syscall_scan'); }); it('size_check 失敗後不執行後續步驟(含禁止 syscall 的大型 wasm)', () => { // 同時違反 size_check 和 syscall_scan const encoder = new TextEncoder(); const syscallBytes = encoder.encode('sock_connect'); const padding = new Uint8Array(2000); // > 1KB 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 = runSandboxAcceptance(wasm, contract); // 應在 size_check 就停止,不到 syscall_scan expect(result.failed_step).toBe('size_check'); }); });