Build custom execution modules (WASM64 Rust or standalone Lua) and HyperBEAM integration tests for a task. Iterates until all tests pass 100%.
Resolve HyperBEAM path
Read .env.hyperbeam and extract the CWD value:
grep '^CWD=' .env.hyperbeam 2>/dev/null | cut -d= -f2-
If .env.hyperbeam doesn't exist or has no CWD, STOP immediately and tell the user:
No HyperBEAM configured. Custom module tests require a HyperBEAM installation.
Set it up by adding a
CWDentry to.env.hyperbeam:bashecho "CWD=/path/to/your/HyperBEAM" >> .env.hyperbeamOr re-run
npx wao createand choose option 1 (clone) or 2 (link).
Steps
- •
Read
tasks.jsonand find the task matching$ARGUMENTS(task id). Update its status to"in_progress". - •
Read
plan.mdfor the module specifications:- •Module type: WASM64 (Rust) or standalone Lua
- •Actions to handle
- •Response format
- •State management
- •
Read the relevant docs:
- •Standalone Lua →
docs/aos-lua.md(custom Lua section) and the tutorial atdocs/docs/pages/tutorials/custom-lua.mdx - •WASM64 Rust → the tutorial at
docs/docs/pages/tutorials/rust-wasm64.mdx - •SDK patterns →
docs/wao-sdk.md(HB class:cacheBinary,cacheScript,spawnAOS,scheduleAOS,computeAOS,scheduleLua,computeLua)
- •Standalone Lua →
- •
If the module type is
module-lua— write a standalone Lua module:Write the module in
custom-lua/{name}.lua:lua-- {name}.lua - Custom Lua module for lua@5.3a device -- Standalone: no AOS boot code, no Handlers framework. local state = {} local function intstr(n) if n == math.floor(n) then return string.format("%d", n) end return tostring(n) end function compute(base, req, opts) local action = nil if type(req) == "table" then if type(req.body) == "table" then action = req.body.Action or req.body.action end if not action then action = req.Action or req.action end end -- Handle actions if action == "MyAction" then -- Implementation here end base.results = { outbox = { ["1"] = { data = "result" } }, output = "" } return base endKey patterns:
- •Entry point is
compute(base, req, opts)— NOTHandlers.add - •Action is in
req.body.Actionorreq.body.action(HTTP lowercases headers) - •Response goes in
base.results.outbox— string-keyed table - •Module-level variables persist between compute calls (Lua VM state is saved)
- •No AOS framework available — no
Handlers,ao.send,msg.reply
- •Entry point is
- •
If the module type is
module-wasm— write a Rust WASM64 module:Write the module in
custom-wasm/src/lib.rs:rust#![no_std] #![no_main] use core::panic::PanicInfo; #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} } static mut BUMP: usize = 4096; #[no_mangle] pub extern "C" fn malloc(size: usize) -> usize { unsafe { let ptr = BUMP; BUMP += size; ptr } } #[no_mangle] pub extern "C" fn free(_ptr: usize) -> usize { 0 } // Manual byte copy (WAMR rejects memory.copy) unsafe fn copy_bytes(dst: *mut u8, src: *const u8, len: usize) { let mut i = 0; while i < len { *dst.add(i) = *src.add(i); i += 1; } } #[no_mangle] pub extern "C" fn handle(msg_ptr: usize, _proc_ptr: usize) -> usize { // Parse JSON message, handle actions, return JSON response // Response format: {"ok":true,"response":{"Output":{"data":""},"Messages":[{"Data":"..."}]}} unsafe { /* implementation */ 0 } }Key constraints:
- •
#![no_std]required — WAMR rejectsmemory.copyfrom std - •All pointers are
i64(memory64 WASM) - •Must export
malloc,free,handle - •Response is JSON with
Messagesarray for outbox - •Use manual byte loops, not memcpy
Build with:
bashcd custom-wasm && cargo +nightly build --target wasm64-unknown-unknown --release -Zbuild-std=core,panic_abort -Zbuild-std-features=panic_immediate_abort
- •
- •
If the task type is
module-test— write HyperBEAM integration tests:For Lua modules — write
test/{name}-module.test.js:jsimport { describe, it, before, after } from "node:test" import assert from "node:assert" import { readFileSync } from "node:fs" import { resolve } from "node:path" import { HyperBEAM } from "wao/test" import HB from "wao/hb" const luaPath = resolve(import.meta.dirname, "../custom-lua/{name}.lua") async function spawnCustomLua(hb, moduleId) { return hb.spawn({ "data-protocol": "ao", variant: "ao.TN.1", module: moduleId, "execution-device": "lua@5.3a", "push-device": "push@1.0", "patch-from": "/results/outbox", }) } describe("{name} custom Lua module", () => { let hbeam, hb, moduleId before(async () => { hbeam = await new HyperBEAM({ reset: true }).ready() hb = new HB({ url: hbeam.url }) await hb.init(hbeam.jwk) const luaSrc = readFileSync(luaPath, "utf-8") moduleId = await hb.cacheScript(luaSrc, "application/lua") }) after(async () => { if (hbeam) hbeam.kill() }) it("should spawn process with module", async () => { const { pid } = await spawnCustomLua(hb, moduleId) assert.ok(pid) }) // Test all actions, state persistence, error handling })For WASM modules — write
test/{name}-module.test.js:jsimport { describe, it, before, after } from "node:test" import assert from "node:assert" import { readFileSync } from "node:fs" import { resolve } from "node:path" import { HyperBEAM } from "wao/test" import HB from "wao/hb" const wasmPath = resolve(import.meta.dirname, "../custom-wasm/target/wasm64-unknown-unknown/release/custom_wasm.wasm") describe("{name} custom WASM64 module", () => { let hbeam, hb before(async () => { hbeam = await new HyperBEAM({ reset: true }).ready() hb = new HB({ url: hbeam.url }) await hb.init(hbeam.jwk) }) after(async () => { if (hbeam) hbeam.kill() }) it("should cache and spawn", async () => { const wasm = readFileSync(wasmPath) const imageId = await hb.cacheBinary(wasm, "application/wasm") const { pid } = await hb.spawnAOS({ image: imageId }) assert.ok(pid) }) // Test all actions via scheduleAOS/computeAOS }) - •
Run the tests:
bashyarn test test/{name}-module.test.js - •
If any tests fail:
- •Read the error output carefully
- •For WASM: check compilation output, verify no
memory.copyinstructions - •For Lua: check
computefunction returnsbasewithresultsset - •Fix the code and re-run
- •Repeat until 100% pass
- •
Update the task status to
"done"intasks.json.
Iteration Protocol
For each test-fix cycle:
- •Run tests and capture FULL output
- •Identify the FIRST failing test (fix failures in order)
- •Classify: is the bug in module code or test code?
- •Fix ONE issue per iteration (don't change multiple things at once)
- •Re-run only the failing test first, then full suite
- •If the same error persists after 3 different fix attempts, step back:
- •Re-read the relevant docs (custom module patterns, WASM64/Lua conventions, etc.)
- •Check if the plan itself has a wrong assumption
- •Try a completely different approach
- •Continue iterating until all tests pass — there is no retry limit
Only escalate to the user if:
- •The failure is environmental (cargo missing, WASM build tools, port conflict)
- •The test requires user input (wallet, external service URL)
- •You've tried 10+ iterations with no progress on the same error
Troubleshooting
Lua module: "compute not found"
- •Ensure the function is named
compute(global, not local) - •Must take exactly 3 arguments:
(base, req, opts)
Lua module: action is nil
- •HTTP signatures lowercase headers: check both
req.body.Actionandreq.body.action - •Also check
req.Actionas a fallback
WASM: "memory.copy not supported"
- •Ensure
#![no_std]is set in lib.rs - •Use manual byte loops, not any std function that might use memory.copy
- •Verify Cargo.toml has
crate-type = ["cdylib"]
WASM: compilation fails with wasm64
- •Install nightly:
rustup toolchain install nightly - •Must use
-Zbuild-std=core,panic_abort - •Target is
wasm64-unknown-unknown(not wasm32)
Tests hang on HyperBEAM spawn
- •Kill stale beam.smp processes:
pkill -f beam.smp - •Check
.env.hyperbeamCWD path is valid - •Requires Erlang/OTP 27+