Skip to Content

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

PiecePurpose
Fake node driverA scriptable client that performs the real handshake, sends events and asserts acks
Store contract suiteRuns against any store, including the atomicity assertions
Protocol fixturesGolden frames shared with the node’s Kotlin tests
Adapter suiteBoots 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_negotiateresync → 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 → READY

By 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:

  1. Unit-test your handlers against plain payload objects. No Luno machinery needed.
  2. 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.
  3. Run your store through the conformance suite if you wrote one.
  4. 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 asserting claim() 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 WebSocketPair for the edge one
  • @luno-oss/store-postgres — passes conformance, including a 60-way concurrent claim test, against real Postgres semantics via PGlite