Testing
With one core, several adapters and several stores, the thing that actually
prevents ecosystem drift is an executable contract. @luno-oss/testing is that
contract.
What is in the kit
| Piece | Purpose |
|---|---|
| Fake node driver | A scriptable client that performs the real handshake, sends events and asserts acks |
| Store contract suite | Runs against any store, including the atomicity assertions |
| Protocol fixtures | Golden frames shared with the node’s Kotlin tests |
| Adapter suite | Boots an adapter and runs the fake node against it end to end |
The main entry point is deliberately vitest-free, so demos and real servers
can import the fake node without pulling a test framework into production
dependencies. The store conformance suite lives on the /store subpath.
The fake node
It performs the genuine connection handshake — version_negotiate → resync →
READY — over any transport, with no mocks in the core. It is the closest thing
to a phone that runs in CI.
Enrol the way a phone does, then attach the node to a channel and run the handshake:
import { FakeNode, enrollNode, fetchTransport, webSocketChannel } from '@luno-oss/testing'
import WebSocket from 'ws'
const { deviceId, credential } = await enrollNode(
fetchTransport('http://localhost:3000'),
'ABCD-1234'
)
const ws = new WebSocket('ws://localhost:3000/ws', {
headers: { Authorization: `Bearer ${credential}` }
})
await new Promise(resolve => ws.on('open', resolve))
const node = new FakeNode({ deviceId })
node.attach(webSocketChannel(ws))
await node.handshake() // version_negotiate → resync → READYBy default the node auto-answers commands with the same event flow the real
agent produces, so a luno.sms.send drives right through to delivery. Pass
autoAnswerCommands: false to drive each step yourself:
const frame = await node.waitFor(f => f.kind === 'command')
await node.sendEventAwaitingAck(mySmsSentEvent)
await node.sendInboundSms({ from: '+9779800000000', body: 'hello' })
node.received // every frame the server sent, for assertions
node.isReady()
node.close()For a unit test with no socket at all, channelPair() gives you an in-memory
pipe with the same NodeChannel interface. Use the fake node to test your own
event handlers, your dashboard, and any adapter you write.
Store conformance
import { describe } from 'vitest'
import { describeStoreConformance } from '@luno-oss/testing/store'
describe('MyStore', () => {
describeStoreConformance(() => new MyStore(connection))
})The suite covers the ordinary CRUD contract of every port, plus the concurrency assertions described in Stores.
Protocol fixtures
The golden frames stayed in @luno-oss/protocol, next to the codec they check.
The node’s Kotlin ProtocolFixturesTest reads the same fixtures/frames.json
and asserts the same byte-identical round-trip.
This is the mechanism that stops the Kotlin and TypeScript codecs drifting. When you change the protocol, change the fixtures — a change that updates only one implementation fails the other’s tests in the same commit, which is exactly what you want. Silent codec drift between a phone in the field and a server is extremely expensive to diagnose.
Testing your own integration
A reasonable ladder, cheapest first:
- Unit-test your handlers against plain payload objects. No Luno machinery needed.
- Run the fake node against your server over a real loopback socket. This catches adapter wiring, auth, and anything that depends on real frame ordering.
- Run your store through the conformance suite if you wrote one.
- Pair a real phone. The physical device is the final confirmation — the wire path it exercises is covered by the automated equivalent, but the radio, the carrier and the OEM firmware are not.
Steps 1–3 run in CI in seconds. Step 4 is the one that finds OEM battery management, carrier delivery-report behaviour and multi-SIM quirks — none of which any emulator reproduces.
What is proven today
@luno-oss/protocol— 91 tests, cross-checked against the Kotlin codec over the shared fixture corpus@luno-oss/core— 74 tests, including the exported store conformance suite assertingclaim()linearizability under real concurrency, and an end-to-end smoke run (pair → connect → handshake → send → delivered) against built bundles on real Web Crypto- Every adapter — gated on a fake-node run over its own transport: a real socket
for the Node frameworks, an in-memory
WebSocketPairfor the edge one @luno-oss/store-postgres— passes conformance, including a 60-way concurrent claim test, against real Postgres semantics via PGlite