Testing
The SDK provides a test framework for writing and running AssemblyScript tests against your JAM service, with configurable ecalli host call mocks.
Architecture
Testing involves two layers that work together:
┌─────────────────────────────────────┐
│ AssemblyScript test code (WASM) │
│ │
│ ┌───────────┐ ┌────────────────┐ │
│ │ Your test │ │ SDK test utils │ │
│ │ assertions│ │ TestGas, etc. │ │
│ └───────────┘ └───────┬────────┘ │
│ │ │
│ @external("ecalli", ...) │
└─────────────────────────┼───────────┘
│ WASM imports
┌─────────────────────────┼───────────┐
│ sdk-ecalli-mocks (Node.js) │
│ │
│ Stub implementations of ecalli │
│ host calls + configuration state │
└─────────────────────────────────────┘
-
sdk-ecalli-mocks/— A TypeScript (Node.js) package that provides stub implementations of all 27 ecalli host calls (general 0-5 + 100, refine 6-13, accumulate 14-26). These stubs satisfy the WASM imports at test time and hold configurable state (gas value, storage map, preimage data, etc.). -
sdk/test/test-ecalli/— AssemblyScript wrapper classes (TestGas,TestFetch,TestLookup,TestStorage,TestEcalli) that bridge to the JS-side stubs via@external("ecalli", ...)WASM imports. These give your AS test code a high-level API for configuring stub behavior.
Writing Tests
Test structure
Tests use the test() helper and Assert class from the SDK:
import { Assert, Test, test } from "@fluffylabs/as-lan/test";
export const TESTS: Test[] = [
test("my feature works", () => {
const assert = Assert.create();
assert.isEqual(1 + 1, 2, "basic math");
return assert;
}),
];
Each test function returns an Assert instance. Use assert.isEqual(actual, expected, msg)
to add assertions — any failure is recorded and reported after the test completes.
Test runner
Each service needs a test-run.ts entry point that registers test suites:
import { TestSuite, runTestSuites } from "@fluffylabs/as-lan/test";
import * as myTests from "./index.test";
export function runAllTests(): void {
runTestSuites([TestSuite.create(myTests.TESTS, "my-service.ts")]);
}
And a bin/test.js that boots the WASM and runs:
import { setMemory } from "ecalli";
import { memory, runAllTests } from "../build/test.js";
setMemory(memory);
runAllTests();
Build and run
npm test # compiles test target and runs bin/test.js
This compiles your test-run entry point to WASM (with the test target from
asconfig.json), then executes it in Node.js with the ecalli stubs providing
host call implementations.
Configuring Ecalli Mocks
By default the stubs provide sensible test values (e.g. gas() returns
1_000_000, lookup() returns "test-preimage", read()/write() use an
in-memory Map). You can override these from within your AS test code.
TestGas
Set the value returned by the gas() ecalli:
import { TestGas } from "@fluffylabs/as-lan/test";
TestGas.set(500); // gas() will now return 500
TestFetch
Set fixed data returned by the fetch() ecalli (overrides the default
kind-dependent pattern):
import { TestFetch } from "@fluffylabs/as-lan/test";
const data = new Uint8Array(4);
data[0] = 0xde; data[1] = 0xad; data[2] = 0xbe; data[3] = 0xef;
TestFetch.setData(data);
TestLookup
Set the preimage returned by the lookup() ecalli:
import { TestLookup } from "@fluffylabs/as-lan/test";
const preimage = new Uint8Array(3);
preimage[0] = 1; preimage[1] = 2; preimage[2] = 3;
TestLookup.setPreimage(preimage);
// Make lookup return NONE (preimage not found)
TestLookup.setNone();
Simulating extrinsic-driven preimage delivery
In production, preimages arrive out-of-band via the xtpreimages block
extrinsic and CE 142 gossip — a service that only calls solicit() (never
provide()) still sees the preimage become available once the network
delivers it. To exercise that path in tests without modeling block
inclusion, attach the preimage directly to the lookup() mock:
import { Bytes32, BytesBlob } from "@fluffylabs/as-lan";
import { TestLookup } from "@fluffylabs/as-lan/test";
// After this call, any lookup(hash) ecalli returns `preimage`.
TestLookup.setAttachedPreimage(
Bytes32.wrapUnchecked(hashBytes),
BytesBlob.wrap(preimageBytes),
);
// Clear all attached preimages (keeps the single-preimage fallback).
TestLookup.clearAttachedPreimages();
Attached entries take precedence over setPreimage / setNone. Both
TestEcalli.reset() and any resetPreimages/resetLookup path clear
the attached map, so tests starting with TestEcalli.reset() never see
leaked attachments from a prior test.
Good reference: the pastebin example’s "paste → solicit → attach → lookup retrieves blob" test exercises the full flow end-to-end.
TestHistoricalLookup
Set the preimage returned by the historical_lookup() ecalli (refine context):
import { TestHistoricalLookup } from "@fluffylabs/as-lan/test";
TestHistoricalLookup.setPreimage(data);
// Make historical_lookup return NONE
TestHistoricalLookup.setNone();
TestPreimages
Configure accumulate-context preimage ecalli stubs (query, solicit, forget, provide):
import { TestPreimages } from "@fluffylabs/as-lan/test";
import { EcalliResult } from "@fluffylabs/as-lan";
// Configure query to return "Available" with slot0=42:
// r7 = (slot0 << 32) | kind, r8 = (slot2 << 32) | slot1
TestPreimages.setQueryResult(i64((u64(42) << 32) | 1), 0);
// Configure query to return NONE (not solicited)
TestPreimages.setQueryResult(-1);
// Configure solicit to return an error
TestPreimages.setSolicitResult(EcalliResult.HUH);
// Configure forget to return OK
TestPreimages.setForgetResult(0);
// Configure provide to return WHO error
TestPreimages.setProvideResult(EcalliResult.WHO);
TestExportSegment
Override the export_segment() ecalli return value (refine context):
import { TestExportSegment } from "@fluffylabs/as-lan/test";
import { EcalliResult } from "@fluffylabs/as-lan";
// Make export_segment return FULL (segment limit reached)
TestExportSegment.setResult(EcalliResult.FULL);
By default, export_segment() returns an auto-incrementing segment index (0, 1, 2, …).
Use TestEcalli.reset() to restore the default behavior.
TestMachine
Configure machine ecalli stub return values (refine context, ecalli 8-13):
import { TestMachine } from "@fluffylabs/as-lan/test";
import { EcalliResult } from "@fluffylabs/as-lan";
// Make machine() return HUH (invalid entrypoint)
TestMachine.setMachineResult(EcalliResult.HUH);
// Make peek() return OOB
TestMachine.setPeekResult(EcalliResult.OOB);
// Make poke() return OOB
TestMachine.setPokeResult(EcalliResult.OOB);
// Make invoke() return Host (3) with host call index 12 in r8
TestMachine.setInvokeResult(3, 12);
// Make expunge() return a specific hash
TestMachine.setExpungeResult(0x42);
By default, machine() returns incrementing IDs, invoke() returns HALT, and
all other operations return OK. Use TestEcalli.reset() to restore defaults.
TestStorage
Pre-populate or delete entries in the read()/write() stub storage:
import { BytesBlob } from "@fluffylabs/as-lan";
import { TestStorage } from "@fluffylabs/as-lan/test";
// Pre-populate a key
const key = BytesBlob.encodeAscii("counter");
const value = BytesBlob.wrap(new Uint8Array(8));
TestStorage.set(key, value);
// Delete a key
TestStorage.set(key, null);
TestPrivileged
Configure the return values of privileged governance ecallis (bless, assign, designate):
import { TestPrivileged } from "@fluffylabs/as-lan/test";
import { EcalliResult } from "@fluffylabs/as-lan";
TestPrivileged.setBlessResult(EcalliResult.WHO);
TestPrivileged.setAssignResult(EcalliResult.CORE);
TestPrivileged.setDesignateResult(EcalliResult.HUH);
TestServices
Configure the return values of service lifecycle ecallis (new_service, eject):
import { TestServices } from "@fluffylabs/as-lan/test";
import { EcalliResult } from "@fluffylabs/as-lan";
TestServices.setNewServiceResult(EcalliResult.CASH);
TestServices.setEjectResult(EcalliResult.WHO);
By default, new_service() returns auto-incrementing service IDs (256, 257, …).
Setting a result overrides this behavior until reset.
TestTransfer
Configure the return value of the transfer() ecalli:
import { TestTransfer } from "@fluffylabs/as-lan/test";
import { EcalliResult } from "@fluffylabs/as-lan";
TestTransfer.setTransferResult(EcalliResult.CASH);
TestEcalli
Reset all configuration to defaults and clear storage:
import { TestEcalli } from "@fluffylabs/as-lan/test";
TestEcalli.reset();
Default Stub Behavior
General (0-5, 100):
| Ecalli | Default |
|---|---|
gas() | Returns 1_000_000 |
fetch() | Writes a 16-byte kind-dependent pattern, returns 16 |
lookup() | Writes "test-preimage" (13 bytes), returns 13 |
read() | Reads from in-memory Map; returns NONE (-1) if key missing |
write() | Writes to in-memory Map; returns previous value length or NONE |
info() | Returns a 96-byte structure (code_hash=0xAA..., balance=1000) |
log() | Prints [LEVEL] target: message to console |
Refine (6-13):
| Ecalli | Default |
|---|---|
historical_lookup() | Writes "test-historical" (15 bytes), returns 15 |
export_segment() | Returns incrementing segment index (0, 1, 2, …) |
machine() | Returns incrementing machine ID (0, 1, 2, …) |
peek() | Returns OK (0) |
poke() | Returns OK (0) |
pages() | Returns OK (0) |
invoke() | Returns HALT (0), writes r8 = 0 |
expunge() | Returns OK (0) |
Accumulate (14-26):
| Ecalli | Default |
|---|---|
bless() | Returns OK (0) |
assign() | Returns OK (0) |
designate() | Returns OK (0) |
checkpoint() | Returns remaining gas (delegates to gas() mock) |
new_service() | Returns incrementing service ID (256, 257, …) |
upgrade() | Returns OK (0) |
transfer() | Returns OK (0) |
eject() | Returns OK (0) |
query() | Returns NONE (-1), writes r8 = 0 |
solicit() | Returns OK (0) |
forget() | Returns OK (0) |
yield_result() | Returns OK (0) |
provide() | Returns OK (0) |
See the fibonacci and ecalli-test examples for usage examples.
Authoring new test helpers
Sometimes a test needs to reach past the stub ecalli surface — for example,
to simulate a block extrinsic (like TestLookup.setAttachedPreimage), seed
state that’s not reachable via any host call, or configure mock behavior
that spans multiple ecallis. The pattern used across this repo has four
layers.
Layer 1 — JS-side mock state
Add the state and the mutator function to the relevant file under
sdk-ecalli-mocks/src/ (grouped by ecalli module: general/, refine/,
accumulate/). The function is called from WASM via @external, so it
must take integer pointers, not Uint8Arrays:
// sdk-ecalli-mocks/src/general/lookup.ts
import { readBytes, writeToMem } from "../memory.js";
const attached: Map<string, Uint8Array> = new Map();
/**
* Simulate a preimage arriving via the `xtpreimages` extrinsic.
*/
export function setPreimageAttached(
hash_ptr: number,
preimage_ptr: number,
preimage_len: number,
): void {
const hashBytes = readBytes(hash_ptr, 32);
if (hashBytes.length !== 32) throw new Error("setPreimageAttached: hash must be 32 bytes");
const preimage = readBytes(preimage_ptr, preimage_len);
attached.set(toHex(hashBytes), preimage);
}
// Hook into the existing reset function so TestEcalli.reset() clears it:
export function resetLookup(): void {
// ... existing resets ...
attached.clear();
}
Key rules:
- Pointers, not Uint8Arrays. WASM imports pass integer offsets into
WASM memory. Use
readBytes(ptr, len)to materialize aUint8Array. - Validate lengths. Throw on wrong-sized inputs — this is a test helper, loud failures are a feature.
- Hook into reset. Extend the module’s
resetXxx()function soTestEcalli.reset()clears the new state automatically.
Layer 2 — JS barrel re-exports
Expose the function at the package root so WASM imports can find it by
name. Add it to BOTH the sub-barrel (general/index.ts, accumulate/index.ts,
or refine/index.ts) AND the top-level src/index.ts:
// sdk-ecalli-mocks/src/general/index.ts
export {
lookup, setLookupPreimage, setLookupNone, resetLookup,
setPreimageAttached, clearPreimageAttachments,
} from "./lookup.js";
// sdk-ecalli-mocks/src/index.ts
export {
lookup, setLookupPreimage, setLookupNone,
setPreimageAttached, clearPreimageAttachments,
} from "./general/index.js";
The top-level re-export is what satisfies the WASM imports — the name must
match exactly what you declare as @external("ecalli", "<name>") on the
AS side.
Layer 3 — AS-side wrapper
Add an @external declaration and a static wrapper class in
sdk/test/test-ecalli/ (usually alongside the stub of the ecalli it
augments — lookup.ts for lookup-related helpers, preimages.ts for
accumulate preimage stubs, etc.):
// sdk/test/test-ecalli/lookup.ts
import { Bytes32, BytesBlob } from "../../core/bytes";
// @ts-expect-error: decorator
@external("ecalli", "setPreimageAttached")
declare function _setPreimageAttached(
hash_ptr: u32,
preimage_ptr: u32,
preimage_len: u32,
): void;
export class TestLookup {
/**
* Simulate a preimage arriving via the `xtpreimages` block extrinsic.
*/
static setAttachedPreimage(hash: Bytes32, preimage: BytesBlob): void {
_setPreimageAttached(hash.ptr(), preimage.ptr(), preimage.length);
}
}
Key rules:
- Static class + static methods. Matches the style of every other
Test*helper (TestGas,TestFetch,TestStorage, …). - Ergonomic AS types in, pointers to WASM out. Accept
Bytes32/BytesBloband pass.ptr()+.lengthto the@externalbinding. - Place where it conceptually belongs. If the helper configures the
lookup()mock, put it onTestLookup, notTestPreimages— even if the underlying JS state lives elsewhere.
Layer 4 — documentation
Document the new helper in the relevant Test* subsection of this file
above. If it captures a production-only mechanism (extrinsic delivery,
gossip, etc.), explain the mechanism in a short paragraph so future
readers understand what path the mock is emulating.
End-to-end example
The TestLookup.setAttachedPreimage helper exercises this whole pattern;
search git log --all -- sdk-ecalli-mocks/src/general/lookup.ts for the
commit that introduced it as a minimal reference.