Files
Arcrun/cypher-executor/tests/wasi-shim.test.ts
Claude 2707fca32b feat(arcrun): implement arcrun MVP — open-source AI workflow engine
Phase 1-5 complete per .agents/specs/u6u-core-mvp/:

**Phase 1 — Cherry-pick & cleanup**
- Create arcrun/ from cypher-executor, credentials, builtins, registry
- Remove 9 InkStone Service Bindings (KBDB, REGISTRY, CLINIC_*, AICEO, MINI_ME)
- Rewrite component-loader: 3-layer (builtin → WASM_BUCKET R2 → error)
- Remove autoPublishMissing.ts, proxy.ts (AICEO), execution-logger.ts (KBDB)
- Clean all KV namespace IDs and InkStone internal URLs from config files

**Phase 2 — contract.yaml completeness**
- Add credentials_required to gmail, google_sheets, telegram, line_notify
- Add config_example to all 21 components with annotated field descriptions

**Phase 3 — Credential injection**
- Add credential-injector.ts: AES-GCM decrypt from CREDENTIALS_KV
- Integrate into GraphExecutor before WASM execution
- Structured errors with repair instructions when credential missing

**Phase 4 — CLI (acr)**
- cli/package.json: arcrun package, bin: acr, deps: commander/js-yaml/chalk/ora
- 8 commands: init, creds push, push, run, validate, parts, list, logs
- Standard mode: writes directly to user's CF KV via CF REST API
- acr init: interactive setup with arcrun.dev API Key registration

**Phase 5 — Open source release prep**
- README.md: 5-minute quickstart, component table, workflow YAML syntax
- CONTRIBUTING.md: TinyGo dev env, component scaffolding, submission flow
- Security audit: no InkStone internal URLs/IDs in committed files
- .gitignore: exclude credentials.yaml, .wrangler, *.wasm

https://claude.ai/code/session_01BnCdSLVH8tUed9VrrPavgT
2026-04-16 04:06:25 +00:00

216 lines
7.7 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.
/**
* WASI shim 單元測試
* Task 2.2 — Requirements: 3.1, 3.3
*/
import { describe, it, expect } from 'vitest';
import { createWasiShim } from '../src/lib/wasi-shim';
// 建立一個最小的 fake WebAssembly.Memory(用 ArrayBuffer 模擬)
function makeFakeMemory(size = 65536): WebAssembly.Memory {
// 用真實的 WebAssembly.MemoryVitest 環境支援)
return new WebAssembly.Memory({ initial: 1 });
}
/** 在 memory 中寫入 iovec 陣列,回傳 iovs 指標 */
function writeIovecs(
view: DataView,
iovecs: Array<{ buf: number; buf_len: number }>,
startPtr: number,
): number {
for (let i = 0; i < iovecs.length; i++) {
view.setUint32(startPtr + i * 8, iovecs[i].buf, true);
view.setUint32(startPtr + i * 8 + 4, iovecs[i].buf_len, true);
}
return startPtr;
}
describe('createWasiShim', () => {
describe('fd_readstdin', () => {
it('一次讀取完整 stdin', () => {
const input = '{"key":"value"}';
const shim = createWasiShim(input);
const mem = makeFakeMemory();
shim.setMemory(mem);
const view = new DataView(mem.buffer);
const inputBytes = new TextEncoder().encode(input);
// 配置 buffer 區域(offset 100)和 iovecoffset 0
const bufPtr = 100;
const iovsPtr = 0;
const nreadPtr = 50;
writeIovecs(view, [{ buf: bufPtr, buf_len: inputBytes.length }], iovsPtr);
const fd_read = (shim.imports.wasi_snapshot_preview1 as Record<string, Function>).fd_read;
const result = fd_read(0, iovsPtr, 1, nreadPtr);
expect(result).toBe(0); // ESUCCESS
const nread = view.getUint32(nreadPtr, true);
expect(nread).toBe(inputBytes.length);
// 驗證讀取的內容
const readBytes = new Uint8Array(mem.buffer, bufPtr, nread);
expect(new TextDecoder().decode(readBytes)).toBe(input);
});
it('分多次讀取 stdin', () => {
const input = 'hello';
const shim = createWasiShim(input);
const mem = makeFakeMemory();
shim.setMemory(mem);
const view = new DataView(mem.buffer);
const fd_read = (shim.imports.wasi_snapshot_preview1 as Record<string, Function>).fd_read;
// 第一次讀 3 bytes
writeIovecs(view, [{ buf: 200, buf_len: 3 }], 0);
fd_read(0, 0, 1, 50);
expect(view.getUint32(50, true)).toBe(3);
expect(new TextDecoder().decode(new Uint8Array(mem.buffer, 200, 3))).toBe('hel');
// 第二次讀剩餘 2 bytes
writeIovecs(view, [{ buf: 300, buf_len: 10 }], 0);
fd_read(0, 0, 1, 50);
expect(view.getUint32(50, true)).toBe(2);
expect(new TextDecoder().decode(new Uint8Array(mem.buffer, 300, 2))).toBe('lo');
// 第三次讀:stdin 已耗盡,nread = 0
writeIovecs(view, [{ buf: 400, buf_len: 10 }], 0);
fd_read(0, 0, 1, 50);
expect(view.getUint32(50, true)).toBe(0);
});
it('非 stdin fd 回傳 ENOSYS', () => {
const shim = createWasiShim('');
const mem = makeFakeMemory();
shim.setMemory(mem);
const view = new DataView(mem.buffer);
writeIovecs(view, [{ buf: 100, buf_len: 10 }], 0);
const fd_read = (shim.imports.wasi_snapshot_preview1 as Record<string, Function>).fd_read;
expect(fd_read(1, 0, 1, 50)).toBe(76); // ENOSYS
expect(fd_read(2, 0, 1, 50)).toBe(76);
});
});
describe('fd_writestdout/stderr', () => {
it('寫入 stdoutfd=1)並可透過 getStdout 讀取', () => {
const shim = createWasiShim('');
const mem = makeFakeMemory();
shim.setMemory(mem);
const view = new DataView(mem.buffer);
const data = new TextEncoder().encode('{"valid":true}');
const bufPtr = 100;
new Uint8Array(mem.buffer).set(data, bufPtr);
writeIovecs(view, [{ buf: bufPtr, buf_len: data.length }], 0);
const fd_write = (shim.imports.wasi_snapshot_preview1 as Record<string, Function>).fd_write;
const result = fd_write(1, 0, 1, 50);
expect(result).toBe(0);
expect(view.getUint32(50, true)).toBe(data.length);
expect(shim.getStdout()).toBe('{"valid":true}');
});
it('寫入 stderrfd=2)並可透過 getStderr 讀取', () => {
const shim = createWasiShim('');
const mem = makeFakeMemory();
shim.setMemory(mem);
const view = new DataView(mem.buffer);
const data = new TextEncoder().encode('error message');
const bufPtr = 100;
new Uint8Array(mem.buffer).set(data, bufPtr);
writeIovecs(view, [{ buf: bufPtr, buf_len: data.length }], 0);
const fd_write = (shim.imports.wasi_snapshot_preview1 as Record<string, Function>).fd_write;
fd_write(2, 0, 1, 50);
expect(shim.getStderr()).toBe('error message');
expect(shim.getStdout()).toBe(''); // stdout 不受影響
});
it('多次寫入 stdout 會合併', () => {
const shim = createWasiShim('');
const mem = makeFakeMemory();
shim.setMemory(mem);
const view = new DataView(mem.buffer);
const fd_write = (shim.imports.wasi_snapshot_preview1 as Record<string, Function>).fd_write;
const write = (text: string, bufPtr: number) => {
const data = new TextEncoder().encode(text);
new Uint8Array(mem.buffer).set(data, bufPtr);
writeIovecs(view, [{ buf: bufPtr, buf_len: data.length }], 0);
fd_write(1, 0, 1, 50);
};
write('{"valid":', 100);
write('true}', 200);
expect(shim.getStdout()).toBe('{"valid":true}');
});
it('非 stdout/stderr fd 回傳 ENOSYS', () => {
const shim = createWasiShim('');
const mem = makeFakeMemory();
shim.setMemory(mem);
const view = new DataView(mem.buffer);
writeIovecs(view, [{ buf: 100, buf_len: 5 }], 0);
const fd_write = (shim.imports.wasi_snapshot_preview1 as Record<string, Function>).fd_write;
expect(fd_write(0, 0, 1, 50)).toBe(76); // stdin 不能寫
expect(fd_write(3, 0, 1, 50)).toBe(76); // 其他 fd
});
});
describe('proc_exit', () => {
it('proc_exit(0) 拋出 Error(正常結束)', () => {
const shim = createWasiShim('');
const proc_exit = (shim.imports.wasi_snapshot_preview1 as Record<string, Function>).proc_exit;
expect(() => proc_exit(0)).toThrow('wasm exit: 0');
});
it('proc_exit(1) 拋出 Error(錯誤結束)', () => {
const shim = createWasiShim('');
const proc_exit = (shim.imports.wasi_snapshot_preview1 as Record<string, Function>).proc_exit;
expect(() => proc_exit(1)).toThrow('wasm exit: 1');
});
});
describe('其餘 syscall 回傳 ENOSYS', () => {
it('fd_seek 回傳 ENOSYS', () => {
const shim = createWasiShim('');
const fd_seek = (shim.imports.wasi_snapshot_preview1 as Record<string, Function>).fd_seek;
expect(fd_seek(1, 0, 0, 0)).toBe(76);
});
it('sock_connect 相關 syscall 回傳 ENOSYS', () => {
const shim = createWasiShim('');
const imports = shim.imports.wasi_snapshot_preview1 as Record<string, Function>;
expect(imports.sock_recv()).toBe(76);
expect(imports.sock_send()).toBe(76);
expect(imports.sock_shutdown()).toBe(76);
});
it('path_open 回傳 ENOSYS', () => {
const shim = createWasiShim('');
const imports = shim.imports.wasi_snapshot_preview1 as Record<string, Function>;
expect(imports.path_open()).toBe(76);
expect(imports.path_create_directory()).toBe(76);
});
});
describe('setMemory 未呼叫時', () => {
it('fd_write 在 memory 未設定時拋出錯誤', () => {
const shim = createWasiShim('');
// 不呼叫 setMemory
const fd_write = (shim.imports.wasi_snapshot_preview1 as Record<string, Function>).fd_write;
expect(() => fd_write(1, 0, 1, 50)).toThrow('WASI memory not set');
});
});
});