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>
151 lines
7.0 KiB
JavaScript
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 })
|