Files
dat-thang/tests/interop/cabal/cabal_cli_features.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

163 lines
6.1 KiB
JavaScript

'use strict'
// Exercise the APIs that cabal-cli 15.x uses, with two independent local
// Cabal clients connected by the same live Hypercore replication streams.
const assert = require('assert')
const fs = require('fs')
const os = require('os')
const path = require('path')
const EventEmitter = require('events')
const Module = require('module')
const Client = require(process.env.CABAL_CLIENT_MODULE || 'cabal-client')
function once (emitter, event, predicate = () => true, timeoutMs = 12000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
emitter.removeListener(event, listener)
reject(new Error(`timed out waiting for Cabal ${event}`))
}, timeoutMs)
function listener (value) {
if (!predicate(value)) return
clearTimeout(timer)
emitter.removeListener(event, listener)
resolve(value)
}
emitter.on(event, listener)
})
}
function close (client, details) {
return new Promise((resolve, reject) => client.removeCabal(details.key, err => err ? reject(err) : resolve()))
}
function waitForUserName (details, name, timeoutMs = 12000) {
return new Promise((resolve, reject) => {
const deadline = Date.now() + timeoutMs
const poll = () => {
const user = Object.values(details.getUsers()).find(candidate => candidate.name === name)
if (user) return resolve(user)
if (Date.now() >= deadline) return reject(new Error(`${name} nickname did not replicate`))
setTimeout(poll, 25)
}
poll()
})
}
function createCliScreen (client) {
const clientPath = require.resolve(process.env.CABAL_CLIENT_MODULE || 'cabal-client')
const cliDirectory = path.resolve(path.dirname(clientPath), '..', '..')
const screenPath = path.join(cliDirectory, 'neat-screen.js')
const originalLoad = Module._load
function fakeNeatLog () {
const input = new EventEmitter()
input.cursor = 0
input.rawLine = () => ''
input.set = () => {}
return {
input,
render: () => {},
clear: () => {},
use: plugin => {
const state = {}
plugin(state, new EventEmitter())
}
}
}
Module._load = function (request, parent, isMain) {
if (request === 'neat-log') return fakeNeatLog
return originalLoad.call(this, request, parent, isMain)
}
try {
const NeatScreen = require(screenPath)
return new NeatScreen({
client,
frontendConfig: { messageTimeformat: '%H:%M:%S', messageIndent: 'nick' }
})
} finally {
Module._load = originalLoad
}
}
async function main () {
const root = fs.mkdtempSync(path.join(process.env.CABAL_TEST_DIR || os.tmpdir(), 'cabal-cli-features-'))
const sourceClient = new Client({ config: { dbdir: path.join(root, 'alice'), preferredPort: 0 } })
const peerClient = new Client({ config: { dbdir: path.join(root, 'bob'), preferredPort: 0 } })
let alice, bob, aliceStream, bobStream, screen
try {
alice = await sourceClient.createCabal()
const roomKey = alice.key
assert.match(roomKey, /^[0-9a-f]{64}$/i, 'create room returns a Cabal room key')
bob = await peerClient.addCabal(`cabal://${roomKey}`)
aliceStream = alice.core.replicate(true)
bobStream = bob.core.replicate(false)
aliceStream.on('error', () => {})
bobStream.on('error', () => {})
aliceStream.pipe(bobStream).pipe(aliceStream)
const aliceNick = once(alice, 'publish-nick', update => update.name === 'alice')
alice.processLine('/nick alice')
await aliceNick
const aliceChannel = once(alice, 'channel-join', update => update.channel === 'lobby')
alice.processLine('/join lobby')
await aliceChannel
const secondChannel = once(alice, 'channel-join', update => update.channel === 'garden')
alice.processLine('/join garden')
await secondChannel
assert(alice.getJoinedChannels().includes('lobby'))
assert(alice.getJoinedChannels().includes('garden'))
screen = createCliScreen(sourceClient)
const cliChannels = alice.getChannels({ includePM: true, onlyJoined: true })
alice.focusChannel(cliChannels[0])
screen.neat.input.emit('ctrl-n')
assert.strictEqual(alice.getCurrentChannel(), cliChannels[1], 'the cabal-cli Ctrl+N handler advances channels')
screen.neat.input.emit('ctrl-n')
assert.strictEqual(alice.getCurrentChannel(), cliChannels[2], 'the cabal-cli Ctrl+N handler continues the cycle')
alice.focusChannel('lobby')
const bobJoined = once(alice, 'channel-join', update => update.channel === 'lobby' && !update.isLocal)
const bobChannel = once(bob, 'channel-join', update => update.channel === 'lobby')
bob.processLine('/join lobby')
await bobChannel
await bobJoined
const bobNick = once(bob, 'publish-nick', update => update.name === 'bob')
bob.processLine('/nick bob')
await bobNick
await waitForUserName(alice, 'bob')
const fromAlice = 'hello from Alice in the lobby'
const gotAlice = once(bob, 'new-message', update =>
update.channel === 'lobby' && update.message.value.content.text === fromAlice)
alice.processLine(fromAlice)
await gotAlice
// Verify the actual room stream carries the published nickname record to Bob.
await waitForUserName(bob, 'alice')
const fromBob = 'hello back from Bob'
const gotBob = once(alice, 'new-message', update =>
update.channel === 'lobby' && update.message.value.content.text === fromBob)
bob.processLine(fromBob)
await gotBob
aliceStream.destroy()
bobStream.destroy()
aliceStream = bobStream = null
process.stdout.write(`cabal-cli 15.x room, /join, /nick, Ctrl+N channel cycle, and two-peer chat: PASS room=cabal://${roomKey}\n`)
} finally {
if (aliceStream) aliceStream.destroy()
if (bobStream) bobStream.destroy()
if (screen) {
if (screen.updateTimer) clearTimeout(screen.updateTimer)
if (alice && screen._updateHandler && screen._updateHandler[alice.key])
alice.removeListener('update', screen._updateHandler[alice.key])
}
if (alice) await close(sourceClient, alice)
if (bob) await close(peerClient, bob)
fs.rmSync(root, { recursive: true, force: true })
}
}
main().catch(err => { console.error(err); process.exitCode = 1 })