// 單元測試: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('G1:contract 含外部 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'); }); });