Files
dat-thang/tests/interop/cabal/cabal_cable_side_by_side.js
T
divorce b735900f2e 0.0.002
ddat interop with dat js
cable interop with cabal js
ddat's frontend = dat-thang
cable fe = cable-thang
dillo support based on cel's dillo-dat
bootstrap node
xplatform support

Signed-off-by: divo <418@cryspace.space>
2026-10-19 00:17:02 -04:00

151 lines
7.0 KiB
JavaScript

'use strict'
// Cabal's JS implementation is the reference side. Cable is exercised via
// its CLI against the exact room key and admin URI generated by that side.
// This deliberately does not call a Cable feed API: Cabal feed wire support
// has not been connected to Cable yet.
const assert = require('assert')
const fs = require('fs')
const os = require('os')
const path = require('path')
const { spawnSync } = require('child_process')
const Cabal = require(process.env.CABAL_JS_MODULE || 'cabal-core')
function callback (fn, ...args) {
return new Promise((resolve, reject) => fn(...args, (err, value) => err ? reject(err) : resolve(value)))
}
function close (cabal) {
return new Promise((resolve, reject) => cabal.close(err => err ? reject(err) : resolve()))
}
function cable (binary, home, ...args) {
let command = binary
let commandArgs = args
let env = { ...process.env, HOME: home }
if (process.env.CABLE_RUNNER === 'wsl') {
// Node and cabal-core may run on Windows while Cable was built in WSL.
// Convert both the executable and per-test HOME to paths visible in WSL.
const wslPath = value => {
const match = /^([A-Za-z]):[\\/](.*)$/.exec(value)
if (!match) return value.replace(/\\/g, '/')
return `/mnt/${match[1].toLowerCase()}/${match[2].replace(/\\/g, '/')}`
}
command = process.env.WSL_BIN || 'wsl.exe'
commandArgs = ['--exec', 'env', `HOME=${wslPath(home)}`, wslPath(binary), ...args]
env = { ...process.env }
}
const result = spawnSync(command, commandArgs, { encoding: 'utf8', env })
if (result.error) throw result.error
if (result.status !== 0) throw new Error(`cable ${args.join(' ')} failed: ${result.stderr}`)
return result.stdout.trim()
}
function waitForMessage (cabal, expected, timeoutMs) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
cabal.messages.events.removeListener('message', onMessage)
reject(new Error('Cabal JS replica did not receive chat/text record'))
}, timeoutMs)
function onMessage (message) {
if (!message || !message.value || !message.value.content ||
message.value.content.text !== expected) return
clearTimeout(timeout)
cabal.messages.events.removeListener('message', onMessage)
resolve(message)
}
cabal.messages.events.on('message', onMessage)
})
}
async function main () {
const root = fs.mkdtempSync(path.join(process.env.CABAL_TEST_DIR || os.tmpdir(), 'cabal-cable-'))
const home = path.join(root, 'cable-home')
const binary = path.resolve(process.env.CABLE_BIN || 'build/linux/cable')
fs.mkdirSync(home)
let source, replica, otherRoom, sourceStream, replicaStream
try {
const sourceStorage = path.join(root, 'js-source')
source = Cabal(sourceStorage)
await callback(source.ready.bind(source))
const roomKey = String(source.key).replace(/^cabal:\/\//, '').toLowerCase()
assert.match(roomKey, /^[0-9a-f]{64}$/, 'Cabal JS must create a 32-byte room key')
replica = Cabal(path.join(root, 'js-replica'), roomKey)
await callback(replica.ready.bind(replica))
const jsUserKey = await callback(source.getLocalKey.bind(source))
assert.match(jsUserKey, /^[0-9a-f]{64}$/, 'Cabal JS local writer key must be 32 bytes')
const expectedText = 'hello from Cabal JS to its replicated room'
const received = waitForMessage(replica, expectedText, 15000)
sourceStream = source.replicate(true)
replicaStream = replica.replicate(false)
sourceStream.on('error', () => {})
replicaStream.on('error', () => {})
sourceStream.pipe(replicaStream).pipe(sourceStream)
await callback(source.publishNick.bind(source), 'cable-side-by-side-user')
await callback(source.publish.bind(source), {
type: 'chat/text',
content: { channel: 'cable-interop', text: expectedText }
})
const replicated = await received
assert.strictEqual(replicated.value.type, 'chat/text')
assert.strictEqual(replicated.value.content.channel, 'cable-interop')
const nickRecord = await callback(source.getMessage.bind(source), { key: jsUserKey, seq: 0 })
// cabal-core releases have returned either the Hypercore node wrapper or
// its decoded value from getMessage; accept both documented representations.
const nickValue = nickRecord.value || nickRecord
assert.strictEqual(nickValue.type, 'about')
assert.strictEqual(nickValue.content.name, 'cable-side-by-side-user')
const replicatedNick = await callback(replica.getMessage.bind(replica), { key: jsUserKey, seq: 0 })
const replicatedNickValue = replicatedNick.value || replicatedNick
assert.strictEqual(replicatedNickValue.type, 'about')
assert.strictEqual(replicatedNickValue.content.name, 'cable-side-by-side-user')
sourceStream.destroy()
replicaStream.destroy()
sourceStream = null
replicaStream = null
await close(source)
source = Cabal(sourceStorage, roomKey)
await callback(source.ready.bind(source))
assert.strictEqual(await callback(source.getLocalKey.bind(source)), jsUserKey,
'Cabal JS local user/feed address must persist for the same room')
const adminKey = 'a'.repeat(64)
const roomAddress = `cabal://${roomKey}?admin=${adminKey}`
assert.strictEqual(cable(binary, home, 'address', roomAddress),
`cable://${roomKey}?admin=${adminKey}`)
const cableUser = cable(binary, home, 'whoami', roomAddress)
assert.match(cableUser, /^cable:\/\/[0-9a-f]{64}$/)
assert.notStrictEqual(cableUser.slice(8), roomKey, 'room key is not a user address')
assert.notStrictEqual(cableUser.slice(8), adminKey, 'room admin key is not a user address')
assert.notStrictEqual(cableUser.slice(8), jsUserKey,
'Cabal JS writer and Cable CLI represent separate local users')
assert.strictEqual(cable(binary, home, 'whoami', `cable://${roomKey}`), cableUser,
'Cable user address must persist for the same room, regardless of URI scheme/admin option')
otherRoom = Cabal(path.join(root, 'js-other-room'))
await callback(otherRoom.ready.bind(otherRoom))
const otherRoomKey = String(otherRoom.key).replace(/^cabal:\/\//, '').toLowerCase()
const otherJsUserKey = await callback(otherRoom.getLocalKey.bind(otherRoom))
assert.notStrictEqual(otherJsUserKey, jsUserKey,
'Cabal JS local writer keys must be independent across rooms')
const otherCableUser = cable(binary, home, 'whoami', `cabal://${otherRoomKey}`)
assert.notStrictEqual(otherCableUser, cableUser,
'Cable user address must be room-scoped like Cabal local writer feeds')
process.stdout.write(`Cabal JS nick/feed replication + Cable room/user identity: PASS room=cabal://${roomKey} js-user=${jsUserKey} cable-user=${cableUser}\n`)
} finally {
for (const stream of [sourceStream, replicaStream]) if (stream) stream.destroy()
for (const cabal of [source, replica, otherRoom]) if (cabal) await close(cabal)
if (process.env.CABAL_TEST_KEEP === '1') process.stdout.write(`kept Cabal test state at ${root}\n`)
else fs.rmSync(root, { recursive: true, force: true })
}
}
main().catch(err => { console.error(err); process.exitCode = 1 })