Files
bare-operating-system/packages/bare-os-protocol/test.js
T

406 lines
13 KiB
JavaScript

import test from 'brittle'
import b4a from 'b4a'
import crypto from 'hypercore-crypto'
import {
topicKey,
buildMbr,
parseMbr,
BLOCK_SIZE,
TOPIC_STRING,
PROTOCOL_APP_CHANNEL_NAME,
PROTOCOL_CAP_CHANNEL_NAME,
PROTOCOL_CHAT_CHANNEL_NAME,
PROTOCOL_MESHDROP_CHANNEL_NAME
} from './constants.js'
import {
BARE_OS_SEED_RPC_METHOD_SHORT_NAMES,
BARE_OS_SEED_RPC_METHODS,
bareOsIsAllowedSeedRpcMethodShort
} from './lib/seed-rpc-methods.js'
import {
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PRIMARY,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_EXTENDED_SEEDING_PLATFORM,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_RLIMITS_DELEGATES_SHELL,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_OFFLINE_NET_EXTENSIONS,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_HOST_TRANSPORT_DELEGATES,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_REPLICATION_OPERATOR_SURFACE,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PEAR_CORESTORE_HRPC,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_BARE_RUNTIME_PROTO_MUX,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_BARE_MODULE_CRYPTO_STAGING,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PEAR_INSPECT_LOGGER_TLS,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_HYPERCORE_PACK_HRPC_LIFECYCLE
} from './lib/kernel-feature-bits.js'
import {
BARE_OS_KERNEL_CAPABILITY_WORDS_JSON_KEY,
BARE_OS_KERNEL_CAPABILITY_WORD_KEYS,
buildKernelCapabilityWordsObject,
getKernelCapabilityWords,
readKernelCapabilityWord
} from './lib/kernel-capability-wire.js'
import { msgDataEncoding, msgSearchReqEncoding } from './lib/messages.js'
import {
msgChatHelloEncoding,
msgChatEventEncoding,
msgChatJoinEncoding,
BARE_OS_CHAT_WIRE_SCHEMA_VERSION
} from './lib/chat-messages.js'
import { pearMultisigShapeOk } from './lib/bare-os-pear-multisig-shape.js'
import { BARE_OS_PROTOMUX_CHANNEL_SCHEMA_VERSION } from './lib/protomux-channel-schema.js'
import { BARE_OS_PROTOMUX_CHANNEL_SCHEMA_VERSION as protomuxSchemaFromChannel } from './lib/channel.js'
import { setupSeedChannel } from './lib/channel.js'
import { bareOsProtMuxChatChannelEnabled } from './lib/chat-env.js'
import { setupBareOsChatChannel } from './lib/chat-channel.js'
test('topicKey is deterministic 32-byte hash', (t) => {
const a = topicKey()
const b = topicKey()
t.is(a.byteLength, 32)
t.alike(a, b)
})
test('topicKey differs from other string', (t) => {
const k1 = topicKey()
const k2 = crypto.hash(b4a.from('other-topic'))
t.unlike(k1, k2)
})
test('topicKey golden hash fixture for bare-os-v1', (t) => {
const k = topicKey()
t.is(
b4a.toString(k, 'hex'),
'9863e44075709205829ba35d40dccf5d8d07e3924bb71a1c941bb5149a0edc4b'
)
})
test('buildMbr + parseMbr roundtrip', (t) => {
const key = crypto.hash(b4a.from('primary-drive'))
const mbr = buildMbr(key)
t.is(mbr.byteLength, BLOCK_SIZE)
const { keys } = parseMbr(mbr)
t.is(keys.length, 1)
t.alike(keys[0], key)
})
test('buildMbr + parseMbr preserves deterministic failover order', (t) => {
const primary = crypto.hash(b4a.from('primary-drive-failover'))
const failoverA = crypto.hash(b4a.from('failover-a'))
const failoverB = crypto.hash(b4a.from('failover-b'))
const mbr = buildMbr(primary, [failoverA, failoverB])
const parsed = parseMbr(mbr)
t.is(parsed.keys.length, 3)
t.alike(parsed.keys[0], primary)
t.alike(parsed.keys[1], failoverA)
t.alike(parsed.keys[2], failoverB)
const parsed2 = parseMbr(buildMbr(primary, [failoverA, failoverB]))
t.alike(parsed2.keys, parsed.keys)
})
test('msgDataEncoding roundtrip', (t) => {
const m = { index: 0, data: b4a.from('hello') }
const pre = { buffer: null, start: 0, end: 0 }
msgDataEncoding.preencode(pre, m)
const buf = b4a.alloc(pre.end)
const state = { buffer: buf, start: 0, end: 0 }
msgDataEncoding.encode(state, m)
const decState = { buffer: buf, start: 0, end: buf.byteLength }
const dec = msgDataEncoding.decode(decState)
t.is(dec.index, 0)
t.alike(dec.data, m.data)
})
test('msgSearchReqEncoding roundtrip', (t) => {
const m = { id: 7, query: 'foo' }
const pre = { buffer: null, start: 0, end: 0 }
msgSearchReqEncoding.preencode(pre, m)
const buf = b4a.alloc(pre.end)
const state = { buffer: buf, start: 0, end: 0 }
msgSearchReqEncoding.encode(state, m)
const decState = { buffer: buf, start: 0, end: buf.byteLength }
const dec = msgSearchReqEncoding.decode(decState)
t.is(dec.id, 7)
t.is(dec.query, 'foo')
})
test('PROTOCOL_APP_CHANNEL_NAME is bare-os-app-v1', (t) => {
t.is(PROTOCOL_APP_CHANNEL_NAME, 'bare-os-app-v1')
})
test('PROTOCOL_CAP_CHANNEL_NAME is bare-os-cap-v1', (t) => {
t.is(PROTOCOL_CAP_CHANNEL_NAME, 'bare-os-cap-v1')
})
test('PROTOCOL_CHAT_CHANNEL_NAME is bare-os-chat-v1', (t) => {
t.is(PROTOCOL_CHAT_CHANNEL_NAME, 'bare-os-chat-v1')
})
test('protocol channel compatibility fixture (app/cap/chat/meshdrop)', (t) => {
const fixture = {
app: PROTOCOL_APP_CHANNEL_NAME,
cap: PROTOCOL_CAP_CHANNEL_NAME,
chat: PROTOCOL_CHAT_CHANNEL_NAME,
meshdrop: PROTOCOL_MESHDROP_CHANNEL_NAME
}
t.alike(fixture, {
app: 'bare-os-app-v1',
cap: 'bare-os-cap-v1',
chat: 'bare-os-chat-v1',
meshdrop: 'bare-os-meshdrop-v1'
})
t.ok(Object.values(fixture).every((name) => name.startsWith('bare-os-')))
t.ok(Object.values(fixture).every((name) => name.endsWith('-v1')))
})
test('setupSeedChannel batches initial sends with stream cork/uncork when available', (t) => {
let corkCalls = 0
let uncorkCalls = 0
let replicateCalls = 0
const primarySends = []
const pairedOpens = []
function makeChannelRecorder() {
const msgs = []
return {
messages: msgs,
addMessage() {
const slot = { send: (msg) => primarySends.push(msg) }
msgs.push(slot)
},
open() {
pairedOpens.push('open')
}
}
}
const mux = {
stream: {
cork() {
corkCalls++
},
uncork() {
uncorkCalls++
}
},
createChannel() {
return makeChannelRecorder()
},
pair(_opts, onpair) {
const achan = {
messages: [],
addMessage() {
this.messages.push({ send() {} })
},
open() {
pairedOpens.push('app-open')
}
}
onpair(achan)
return achan
}
}
const localRAM = new Map([[0, b4a.from('mbr')]])
setupSeedChannel(
/** @type {any} */ (mux),
localRAM,
() => {
replicateCalls++
},
{ enableBareOsAppChannel: true, enableBareOsChatChannel: false }
)
t.is(corkCalls, 1)
t.is(uncorkCalls, 1)
t.ok(pairedOpens.includes('open'))
t.ok(pairedOpens.includes('app-open'))
t.is(replicateCalls, 1)
t.ok(primarySends.some((v) => v instanceof Uint8Array), 'bitfield sent')
})
function roundtrip(enc, m, t) {
const pre = { buffer: null, start: 0, end: 0 }
enc.preencode(pre, m)
const buf = b4a.alloc(pre.end)
const state = { buffer: buf, start: 0, end: 0 }
enc.encode(state, m)
const decState = { buffer: buf, start: 0, end: buf.byteLength }
return enc.decode(decState)
}
test('msgChatHelloEncoding roundtrip', (t) => {
const m = { chatSchemaVersion: 1, swarmChatCapability: true }
const dec = roundtrip(msgChatHelloEncoding, m, t)
t.is(dec.chatSchemaVersion, 1)
t.is(dec.swarmChatCapability, true)
})
test('msgChatJoinEncoding roundtrip', (t) => {
const m = {
roomId: 'general',
displayName: 'alice',
senderPk: b4a.alloc(32, 3),
clientInstanceId: b4a.alloc(16, 7)
}
const dec = roundtrip(msgChatJoinEncoding, m, t)
t.is(dec.roomId, 'general')
t.is(dec.displayName, 'alice')
t.alike(dec.senderPk, m.senderPk)
t.alike(dec.clientInstanceId, m.clientInstanceId)
})
test('msgChatEventEncoding roundtrip', (t) => {
const m = {
schemaVersion: BARE_OS_CHAT_WIRE_SCHEMA_VERSION,
evtKind: 1,
roomId: 'general',
evtId: b4a.alloc(16, 9),
tsMs: Date.now(),
senderPk: b4a.alloc(32, 2),
displayName: 'bob',
body: 'hello swarm',
ttl: 4,
sigDetached: null
}
const dec = roundtrip(msgChatEventEncoding, m, t)
t.is(dec.schemaVersion, m.schemaVersion)
t.is(dec.body, 'hello swarm')
t.is(dec.ttl, 4)
t.is(dec.sigDetached, null)
})
test('TOPIC_STRING is bare-os-v1', (t) => {
t.is(TOPIC_STRING, 'bare-os-v1')
})
test('seed RPC method list matches kernel_info surface', (t) => {
t.ok(BARE_OS_SEED_RPC_METHOD_SHORT_NAMES.includes('capabilities'))
t.ok(BARE_OS_SEED_RPC_METHOD_SHORT_NAMES.includes('replication_queue'))
t.is(BARE_OS_SEED_RPC_METHODS.length, BARE_OS_SEED_RPC_METHOD_SHORT_NAMES.length)
t.ok(BARE_OS_SEED_RPC_METHODS.every((m) => m.startsWith('bare_os.')))
})
test('stock primary capability word is non-zero', (t) => {
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_PRIMARY >>> 0) > 0)
})
test('stock extended seeding platform word is non-zero', (t) => {
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_EXTENDED_SEEDING_PLATFORM >>> 0) > 0)
})
test('stock host-transport-delegates word is non-zero', (t) => {
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_HOST_TRANSPORT_DELEGATES >>> 0) > 0)
})
test('stock replication-operator-surface word is non-zero', (t) => {
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_REPLICATION_OPERATOR_SURFACE >>> 0) > 0)
})
test('stock pear-corestore-hrpc word is non-zero', (t) => {
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_PEAR_CORESTORE_HRPC >>> 0) > 0)
})
test('stock bare-runtime-proto-mux word is non-zero', (t) => {
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_BARE_RUNTIME_PROTO_MUX >>> 0) > 0)
})
test('stock bare-module-crypto-staging word is non-zero', (t) => {
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_BARE_MODULE_CRYPTO_STAGING >>> 0) > 0)
})
test('stock pear-inspect-logger-tls word is non-zero', (t) => {
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_PEAR_INSPECT_LOGGER_TLS >>> 0) > 0)
})
test('stock hypercore-pack-hrpc-lifecycle word is non-zero', (t) => {
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_HYPERCORE_PACK_HRPC_LIFECYCLE >>> 0) > 0)
})
test('kernelCapabilityWords round-trip fixture covers all 11 words', (t) => {
const wordsObj = buildKernelCapabilityWordsObject(
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PRIMARY,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_EXTENDED_SEEDING_PLATFORM,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_RLIMITS_DELEGATES_SHELL,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_OFFLINE_NET_EXTENSIONS,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_HOST_TRANSPORT_DELEGATES,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_REPLICATION_OPERATOR_SURFACE,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PEAR_CORESTORE_HRPC,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_BARE_RUNTIME_PROTO_MUX,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_BARE_MODULE_CRYPTO_STAGING,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PEAR_INSPECT_LOGGER_TLS,
BARE_OS_KERNEL_FEATURES_STOCK_WORD_HYPERCORE_PACK_HRPC_LIFECYCLE
)
const cap = { [BARE_OS_KERNEL_CAPABILITY_WORDS_JSON_KEY]: wordsObj }
const roundTrip = getKernelCapabilityWords(cap)
t.ok(roundTrip)
t.is(BARE_OS_KERNEL_CAPABILITY_WORD_KEYS.length, 11)
t.alike(Object.keys(roundTrip).sort(), [...BARE_OS_KERNEL_CAPABILITY_WORD_KEYS].sort())
for (const key of BARE_OS_KERNEL_CAPABILITY_WORD_KEYS) {
t.is(readKernelCapabilityWord(roundTrip, key), readKernelCapabilityWord(wordsObj, key))
}
})
test('seed RPC registry includes HyperDHT / blind-peer operator hints', (t) => {
t.ok(BARE_OS_SEED_RPC_METHOD_SHORT_NAMES.includes('dht_bootstrap_hint'))
t.ok(BARE_OS_SEED_RPC_METHOD_SHORT_NAMES.includes('blind_peer_topology_v2'))
})
test('bareOsIsAllowedSeedRpcMethodShort matches registry gate', (t) => {
t.ok(bareOsIsAllowedSeedRpcMethodShort('version'))
t.ok(bareOsIsAllowedSeedRpcMethodShort('capabilities'))
t.absent(bareOsIsAllowedSeedRpcMethodShort('not_a_registered_bare_os_method'))
t.absent(bareOsIsAllowedSeedRpcMethodShort(''))
})
test('pearMultisigShapeOk accepts valid multisig metadata', (t) => {
t.ok(pearMultisigShapeOk({ signers: ['a', 'b'], quorum: 1 }))
t.ok(pearMultisigShapeOk({ signers: ['x'], quorum: 1 }))
})
test('pearMultisigShapeOk rejects tampered shapes', (t) => {
t.absent(pearMultisigShapeOk(null))
t.absent(pearMultisigShapeOk({ signers: 'x', quorum: 1 }))
t.absent(pearMultisigShapeOk({ signers: ['a'], quorum: 2 }))
t.absent(pearMultisigShapeOk({ signers: [], quorum: 0 }))
})
test('BARE_OS_PROTOMUX_CHANNEL_SCHEMA_VERSION matches channel re-export', (t) => {
t.is(BARE_OS_PROTOMUX_CHANNEL_SCHEMA_VERSION, protomuxSchemaFromChannel)
t.ok(BARE_OS_PROTOMUX_CHANNEL_SCHEMA_VERSION >= 1)
})
test('bareOsProtMuxChatChannelEnabled defaults on; explicit opt-out tokens', (t) => {
t.ok(bareOsProtMuxChatChannelEnabled(undefined))
t.ok(bareOsProtMuxChatChannelEnabled({}))
t.ok(bareOsProtMuxChatChannelEnabled({ BARE_OS_PROTOMUX_CHAT_CHANNEL: '' }))
t.ok(bareOsProtMuxChatChannelEnabled({ BARE_OS_PROTOMUX_CHAT_CHANNEL: 'TRUE' }))
t.absent(bareOsProtMuxChatChannelEnabled({ BARE_OS_PROTOMUX_CHAT_CHANNEL: '0' }))
t.absent(bareOsProtMuxChatChannelEnabled({ BARE_OS_PROTOMUX_CHAT_CHANNEL: 'false' }))
t.absent(bareOsProtMuxChatChannelEnabled({ BARE_OS_PROTOMUX_CHAT_CHANNEL: 'OFF' }))
t.absent(bareOsProtMuxChatChannelEnabled({ BARE_OS_PROTOMUX_CHAT_CHANNEL: 'no' }))
})
test('setupBareOsChatChannel defers onChannelOpened until fullyOpened resolves', async (t) => {
/** @type {((v: boolean) => void) | null} */
let resolveOpened = null
const chan = {
addMessage() {},
open() {},
fullyOpened() {
return new Promise((resolve) => {
resolveOpened = resolve
})
}
}
const mux = {
createChannel() {
return chan
}
}
let called = false
setupBareOsChatChannel(/** @type {any} */ (mux), {
onChannelOpened() {
called = true
}
})
t.absent(called)
resolveOpened(true)
await Promise.resolve()
await Promise.resolve()
t.ok(called)
})