Testing
@korajs/test provides a testing harness for verifying sync, conflict resolution, and multi-device behavior. It creates virtual device networks with real SQLite stores and in-memory transports. No actual network required.
Installation
pnpm add -D @korajs/test@beta vitestThe package runs each device on a real SQLite store (better-sqlite3) and a real sync engine, and the server on a real KoraSyncServer, so tests exercise the same fold, constraints and validators as production. It works with any test runner; the examples use Vitest.
Creating a test network
createTestNetwork() sets up a server and devices connected by in-memory transports:
import { defineSchema, t } from '@korajs/core'
import { createTestNetwork, type TestNetwork } from '@korajs/test'
import { afterEach, describe, expect, test } from 'vitest'
const schema = defineSchema({
version: 1,
collections: {
todos: {
fields: {
title: t.string(),
completed: t.boolean().default(false),
},
},
},
})
describe('sync', () => {
let network: TestNetwork | null = null
afterEach(async () => {
await network?.close()
network = null
})
test('data syncs between devices', async () => {
network = await createTestNetwork(schema)
const [deviceA, deviceB] = network.devices
if (!deviceA || !deviceB) throw new Error('expected two devices')
await deviceA.collection('todos').insert({ title: 'Buy milk' })
await deviceA.sync()
await deviceB.sync()
const todos = await deviceB.getState('todos')
expect(todos).toHaveLength(1)
expect(todos[0]?.title).toBe('Buy milk')
})
})sync() connects on the first call and afterwards flushes pending uploads; a connected device receives other devices' operations as the server relays them. Call sync() on each device after writes, as above.
Devices and options
network = await createTestNetwork(schema, { devices: 3 })
network = await createTestNetwork(schema, { deviceNames: ['alice', 'bob', 'charlie'] })Other options configure the server and the links: validateOperation, serverStore (SQLite or Postgres instead of memory), chaos (drop, duplicate, reorder, latency), blobStorage, encryption. See the Test API.
import type { OperationValidator } from '@korajs/server'
const validateOperation: OperationValidator = (op) => {
if (op.collection === 'todos' && op.type === 'insert') {
return { action: 'reject', code: 'FORBIDDEN', message: 'Inserts are blocked' }
}
return { action: 'accept' }
}
test('the server refuses inserts', async () => {
network = await createTestNetwork(schema, { validateOperation })
const [device] = network.devices
if (!device) throw new Error('expected a device')
await device.sync()
await device.collection('todos').insert({ title: 'Refused' })
await device.sync()
const rejected = await device.getRejectedOperations()
expect(rejected[0]?.code).toBe('FORBIDDEN')
})Offline behavior
test('offline writes sync after reconnecting', async () => {
network = await createTestNetwork(schema)
const [deviceA, deviceB] = network.devices
if (!deviceA || !deviceB) throw new Error('expected two devices')
await deviceA.sync()
await deviceA.disconnect()
await deviceA.collection('todos').insert({ title: 'Offline todo' })
await deviceA.reconnect()
await deviceA.sync()
await deviceB.sync()
const todos = await deviceB.getState('todos')
expect(todos.map((todo) => todo.title)).toEqual(['Offline todo'])
})Asserting convergence
expectConverged() throws with the differences unless every device holds the same records; checkConvergence() returns them instead:
test('concurrent edits converge', async () => {
network = await createTestNetwork(schema, { devices: 3 })
const [a, b, c] = network.devices
if (!a || !b || !c) throw new Error('expected three devices')
const todo = await a.collection('todos').insert({ title: 'Shared' })
for (const device of [a, b, c]) await device.sync()
// Concurrent offline edits of the same field
await b.disconnect()
await c.disconnect()
await b.collection('todos').update(todo.id, { title: 'From B' })
await c.collection('todos').update(todo.id, { title: 'From C' })
await b.reconnect()
await c.reconnect()
for (const device of [b, c, a, b, c]) await device.sync()
await expectConverged(network.devices, schema)
const result = await checkConvergence(network.devices, schema)
expect(result.converged).toBe(true)
})With chaos, use expectConvergedEventually(devices, schema, { timeoutMs }), which retries until the network settles.
Inspecting merge decisions
Each device records merge decisions in its audit trail, as an app does:
test('the concurrent edit produced a merge trace', async () => {
network = await createTestNetwork(schema)
const [deviceA] = network.devices
if (!deviceA) throw new Error('expected a device')
const traces = await deviceA.store.getAuditTraces({ collections: ['todos'] })
expect(Array.isArray(traces)).toBe(true)
})Device events are on device.emitter (for example merge:conflict or sync:operation-rejected).
Testing schema migrations
createMixedTestNetwork(serverSchema, serverOptions, devices) runs devices on different schema versions against one server, to test supportedSchemaVersions and operation transforms. See the Test API.

