2026-10-29 19:41:30 +01:00
// The parts of the Matrix mouth that sit on CultLib: the chat_* wire documents, the
// CultCache cursor and brake stores, and the CultNet RUDP endpoint. Needs the built
// CultLib TypeScript packages (CULTLIB_ROOT, else ../CultLib), like the other Bifrost tests.
import test from "node:test" ;
import assert from "node:assert/strict" ;
import dgram from "node:dgram" ;
2026-10-30 02:49:59 +01:00
import { createServer } from "node:http" ;
import { spawn } from "node:child_process" ;
2026-10-29 19:41:30 +01:00
import { mkdir , mkdtemp , readFile , writeFile } from "node:fs/promises" ;
import { tmpdir } from "node:os" ;
import { resolve } from "node:path" ;
import * as docs from "../tools/persona-chat-documents.mjs" ;
2026-10-30 02:49:59 +01:00
import { cultcacheState , connectionIdFor , createBrakeReader , loadRuntime , pageOf , startChatEndpoint } from "../tools/persona-matrix-mouth.mjs" ;
2026-10-29 19:41:30 +01:00
const root = resolve ( import . meta . dirname , ".." ) ;
const runtime = loadRuntime ( resolve ( process . env . CULTLIB _ROOT ? ? resolve ( root , ".." , "CultLib" ) ) ) ;
const { msgpack } = runtime ;
const fixtureDir = resolve ( import . meta . dirname , "fixtures" , "persona-chat-v1" ) ;
const delivery = {
seq : 7 , roomAlias : "#operator:gamecult.org" , roomId : "!abc:gamecult.org" , eventId : "$e1" , sender : "@meta:gamecult.org" , senderDisplay : "Meta" ,
text : "hello eureka" , atUnixMs : 1761700000000 ,
context : [ { sender : "@eureka:gamecult.org" , text : "earlier" , atUnixMs : 1761699999000 } ] ,
2026-10-30 02:49:59 +01:00
mentions : [ "@eureka:gamecult.org" , "@nova:gamecult.org" ] , isDirect : true ,
2026-10-29 19:41:30 +01:00
} ;
const samples = {
[ docs . kinds . pendingRequest ] : { tuple : docs . pendingRequestTuple ( { persona : "eureka" , afterSeq : 3 } ) , parse : docs . parsePendingRequest } ,
[ docs . kinds . pending ] : { tuple : docs . pendingTuple ( { persona : "eureka" , deliveries : [ delivery ] } ) , parse : docs . parsePending } ,
[ docs . kinds . ack ] : { tuple : docs . ackTuple ( { persona : "eureka" , throughSeq : 7 } ) , parse : docs . parseAck } ,
[ docs . kinds . sendRequest ] : { tuple : docs . sendRequestTuple ( { requestId : "req-1" , persona : "eureka" , roomAlias : "#operator:gamecult.org" , text : "hi" } ) , parse : docs . parseSendRequest } ,
[ docs . kinds . sendReceipt ] : { tuple : docs . sendReceiptTuple ( { requestId : "req-1" , eventId : "$sent" } ) , parse : docs . parseSendReceipt } ,
2026-10-30 02:49:59 +01:00
[ docs . kinds . refusal ] : { tuple : docs . refusalTuple ( { persona : "ghost" , refusal : "UnknownPersona" } ) , parse : docs . parseRefusal } ,
2026-10-29 19:41:30 +01:00
} ;
for ( const [ kind , { tuple , parse } ] of Object . entries ( samples ) ) {
test ( ` golden fixture: ${ kind } is byte-stable and parses back ` , async ( ) => {
const bytes = Buffer . from ( msgpack . encode ( tuple ) ) ;
const file = resolve ( fixtureDir , ` ${ kind } .msgpack ` ) ;
if ( process . env . UPDATE _FIXTURES === "1" ) { await mkdir ( fixtureDir , { recursive : true } ) ; await writeFile ( file , bytes ) ; }
assert . deepEqual ( bytes , await readFile ( file ) , "wire bytes changed: Huginn decodes these fixtures" ) ;
assert . deepEqual ( parse ( msgpack . decode ( bytes ) ) , parse ( tuple ) ) ;
} ) ;
}
test ( "the chat documents refuse malformed tuples" , ( ) => {
assert . throws ( ( ) => docs . parseAck ( [ "eureka" ] ) ) ;
assert . throws ( ( ) => docs . parseAck ( [ "eureka" , - 1 ] ) ) ;
assert . throws ( ( ) => docs . parsePendingRequest ( [ "" , 0 ] ) ) ;
assert . throws ( ( ) => docs . parseSendRequest ( [ "r" , "eureka" , "#a:b" , "x" . repeat ( 4001 ) ] ) ) ;
assert . throws ( ( ) => docs . parseSendRequest ( [ "r" , "eureka" , "#a:b" , 5 ] ) ) ;
2026-10-30 02:49:59 +01:00
const wire = docs . deliveryTuple ( delivery ) ;
assert . equal ( wire . length , 11 ) ;
assert . deepEqual ( docs . parseDelivery ( wire ) , delivery ) ;
assert . throws ( ( ) => docs . parseDelivery ( wire . slice ( 0 , 9 ) ) , /11-field/ , "the nine-field delivery is gone: one shape" ) ;
assert . throws ( ( ) => docs . parseDelivery ( [ ... wire . slice ( 0 , 10 ) , "yes" ] ) , /boolean/ ) ;
assert . throws ( ( ) => docs . parseDelivery ( [ ... wire . slice ( 0 , 9 ) , [ "" ] , true ] ) ) ;
assert . throws ( ( ) => docs . refusalTuple ( { persona : "p" , refusal : "Invented" } ) ) ;
2026-10-29 19:41:30 +01:00
assert . throws ( ( ) => docs . parseSendReceipt ( [ "r" , "$e" , "Brake" ] ) , /exactly one/ ) ;
assert . throws ( ( ) => docs . parseSendReceipt ( [ "r" , null , null ] ) , /exactly one/ ) ;
assert . throws ( ( ) => docs . sendReceiptTuple ( { requestId : "r" , refusal : "Invented" } ) ) ;
2026-10-30 02:49:59 +01:00
assert . throws ( ( ) => docs . parseSendReceipt ( [ "r" , null , "Invented" ] ) , /unknown refusal/ ) ;
2026-10-29 19:41:30 +01:00
assert . deepEqual ( docs . parseSendReceipt ( [ "r" , null , "Brake" ] ) , { requestId : "r" , eventId : null , refusal : "Brake" } ) ;
} ) ;
test ( "the cursor is a CultCache file that survives a restart" , async ( ) => {
const path = resolve ( await mkdtemp ( resolve ( tmpdir ( ) , "mouth-cursor-" ) ) , "cursor.cc" ) ;
assert . equal ( await cultcacheState ( { runtime , path } ) . load ( ) , null ) ;
await cultcacheState ( { runtime , path } ) . save ( { nextBatch : "s72_9" , seq : 12 } ) ;
const loaded = await cultcacheState ( { runtime , path } ) . load ( ) ;
assert . deepEqual ( [ loaded . nextBatch , loaded . seq ] , [ "s72_9" , 12 ] ) ;
} ) ;
async function writeControl ( path , brakeTuple ) {
const store = new runtime . SingleFileMessagePackBackingStore ( path ) ;
await store . push ( { key : "brake" , type : "eureka.control.brake.v1" , payload : msgpack . encode ( brakeTuple ) , storedAt : new Date ( ) . toISOString ( ) } ) ;
}
test ( "the brake reader reads a real CultCache control store" , async ( ) => {
const dir = await mkdtemp ( resolve ( tmpdir ( ) , "mouth-control-" ) ) ;
const reader = path => createBrakeReader ( { controlPath : path , readStore : runtime . readSingleFileStore , decode : msgpack . decode } ) ;
assert . equal ( await reader ( resolve ( dir , "missing.cc" ) ) ( ) , false ) ;
await writeControl ( resolve ( dir , "held.cc" ) , [ false , "2026-10-29T00:00:00Z" , "op" ] ) ;
assert . equal ( await reader ( resolve ( dir , "held.cc" ) ) ( ) , false ) ;
await writeControl ( resolve ( dir , "released.cc" ) , [ true , "2026-10-29T00:00:00Z" , "op" ] ) ;
assert . equal ( await reader ( resolve ( dir , "released.cc" ) ) ( ) , true ) ;
await writeFile ( resolve ( dir , "garbage.cc" ) , "not a store" ) ;
assert . equal ( await reader ( resolve ( dir , "garbage.cc" ) ) ( ) , false ) ;
} ) ;
2026-10-31 01:33:36 +01:00
test ( "two clients served at once each get their own answer on their own session" , async ( ) => {
const mouth = {
pendingAfter : ( ) => [ ] ,
ack : async ( ) => { } ,
// Slow enough that the two requests are in flight together.
send : async request => { await new Promise ( r => setTimeout ( r , 150 ) ) ; return { requestId : request . requestId , eventId : ` $ ${ request . requestId } ` , refusal : null } ; } ,
} ;
const key = "connection-key-for-concurrency" ;
const server = await startChatEndpoint ( { mouths : new Map ( [ [ "eureka" , mouth ] ] ) , runtime , port : 0 , connectionKey : key , runtimeId : "mouth-test" } ) ;
const dial = async name => {
const socket = dgram . createSocket ( "udp4" ) ;
await new Promise ( ok => socket . bind ( 0 , "127.0.0.1" , ok ) ) ;
const connection = new runtime . cultnet . CultNetRudpSocketTransportConnection ( {
runtimeId : name , socket , mode : "client" , remoteHost : "127.0.0.1" , remotePort : server . address . port ,
connectionId : connectionIdFor ( key ) , resendDelayMs : 50 , resendPollMs : 10 , maxFragmentBytes : 1200 ,
} ) ;
const receipts = [ ] ;
connection . on ( "frame" , value => {
const message = runtime . cultnet . parseCultNetMessage ( msgpack . decode ( value . payload ) , "cultnet.schema.v0" ) ;
receipts . push ( docs . parseSendReceipt ( msgpack . decode ( message . document . payload ) ) ) ;
} ) ;
connection . connect ( ) ;
for ( let i = 0 ; i < 100 && ! connection . connected ; i ++ ) await new Promise ( r => setTimeout ( r , 20 ) ) ;
assert . ok ( connection . connected , ` ${ name } opened its session ` ) ;
const send = requestId => connection . send ( "schema" , msgpack . encode ( runtime . cultnet . encodeCultNetMessageForWire (
docs . envelope ( { kind : docs . kinds . sendRequest , recordKey : requestId , payload : msgpack . encode ( docs . sendRequestTuple ( { requestId , persona : "eureka" , roomAlias : "#operator:gamecult.org" , text : name } ) ) , sourceRuntimeId : name , sourceAgentId : "eureka" , sourceRole : "persona-organ" } ) , "cultnet.schema.v0" ) ) ) ;
return { connection , receipts , send } ;
} ;
const first = await dial ( "mind-persona-one" ) ;
const second = await dial ( "mind-heartbeat" ) ;
try {
first . send ( "req-first" ) ;
second . send ( "req-second" ) ;
for ( let i = 0 ; i < 250 && ( first . receipts . length < 1 || second . receipts . length < 1 ) ; i ++ ) await new Promise ( r => setTimeout ( r , 20 ) ) ;
assert . deepEqual ( first . receipts , [ { requestId : "req-first" , eventId : "$req-first" , refusal : null } ] ) ;
assert . deepEqual ( second . receipts , [ { requestId : "req-second" , eventId : "$req-second" , refusal : null } ] ) ;
assert . ok ( first . connection . connected && second . connection . connected , "neither session took the other's" ) ;
} finally {
server . close ( ) ;
first . connection . close ( ) ;
second . connection . close ( ) ;
}
} ) ;
2026-10-29 19:41:30 +01:00
test ( "the RUDP endpoint serves pending, ack and send over CultNet, and answers a held brake with a Brake refusal" , async ( ) => {
const calls = { acks : [ ] , sends : [ ] } ;
const mouth = {
pendingAfter : after => ( after < 7 ? [ delivery ] : [ ] ) ,
ack : async through => { calls . acks . push ( through ) ; } ,
send : async request => { calls . sends . push ( request ) ; return request . text === "muted" ? { requestId : request . requestId , eventId : null , refusal : "Brake" } : { requestId : request . requestId , eventId : "$sent" , refusal : null } ; } ,
} ;
const key = "connection-key-for-test" ;
const rejected = [ ] ;
2026-10-30 02:49:59 +01:00
const server = await startChatEndpoint ( { mouths : new Map ( [ [ "eureka" , mouth ] ] ) , runtime , port : 0 , connectionKey : key , runtimeId : "mouth-test" , log : line => rejected . push ( line ) } ) ;
2026-10-29 19:41:30 +01:00
const client = dgram . createSocket ( "udp4" ) ;
await new Promise ( ok => client . bind ( 0 , "127.0.0.1" , ok ) ) ;
const connection = new runtime . cultnet . CultNetRudpSocketTransportConnection ( {
runtimeId : "huginn-test" , socket : client , mode : "client" , remoteHost : "127.0.0.1" , remotePort : server . address . port ,
connectionId : connectionIdFor ( key ) , initialSequence : 1 , resendDelayMs : 50 , resendPollMs : 10 , maxFragmentBytes : 1200 ,
} ) ;
const replies = [ ] ;
const waiting = [ ] ;
connection . on ( "frame" , value => {
const message = runtime . cultnet . parseCultNetMessage ( msgpack . decode ( value . payload ) , "cultnet.schema.v0" ) ;
replies . push ( message . document ) ;
waiting . splice ( 0 ) . forEach ( w => w ( ) ) ;
} ) ;
const nextReply = ( ) => new Promise ( ( ok , fail ) => { const t = setTimeout ( ( ) => fail ( new Error ( ` no reply from the mouth; it logged: ${ rejected . join ( " | " ) } ` ) ) , 5000 ) ; waiting . push ( ( ) => { clearTimeout ( t ) ; ok ( replies . at ( - 1 ) ) ; } ) ; } ) ;
const put = ( kind , recordKey , tuple ) => connection . send ( "schema" , msgpack . encode ( runtime . cultnet . encodeCultNetMessageForWire (
docs . envelope ( { kind , recordKey , payload : msgpack . encode ( tuple ) , sourceRuntimeId : "huginn-test" , sourceAgentId : "eureka" , sourceRole : "persona-organ" } ) , "cultnet.schema.v0" ) ) ) ;
try {
connection . connect ( ) ;
for ( let i = 0 ; i < 100 && ! connection . connected ; i ++ ) await new Promise ( r => setTimeout ( r , 20 ) ) ;
assert . ok ( connection . connected , "the RUDP session opens with the credential-derived connection id" ) ;
let reply = nextReply ( ) ;
put ( docs . kinds . pendingRequest , "eureka" , docs . pendingRequestTuple ( { persona : "eureka" , afterSeq : 0 } ) ) ;
let document = await reply ;
assert . equal ( document . schemaId , docs . kinds . pending ) ;
const pending = docs . parsePending ( msgpack . decode ( document . payload ) ) ;
assert . deepEqual ( pending . deliveries . map ( d => d . eventId ) , [ "$e1" ] ) ;
2026-10-30 02:49:59 +01:00
assert . deepEqual ( pending . deliveries [ 0 ] , delivery , "sender, mentions and the DM flag cross the wire exactly" ) ;
2026-10-29 19:41:30 +01:00
put ( docs . kinds . ack , "eureka:7" , docs . ackTuple ( { persona : "eureka" , throughSeq : 7 } ) ) ;
reply = nextReply ( ) ;
put ( docs . kinds . sendRequest , "req-2" , docs . sendRequestTuple ( { requestId : "req-2" , persona : "eureka" , roomAlias : "#operator:gamecult.org" , text : "muted" } ) ) ;
document = await reply ;
assert . equal ( document . schemaId , docs . kinds . sendReceipt ) ;
assert . deepEqual ( docs . parseSendReceipt ( msgpack . decode ( document . payload ) ) , { requestId : "req-2" , eventId : null , refusal : "Brake" } ) ;
2026-10-30 02:49:59 +01:00
reply = nextReply ( ) ;
2026-10-29 19:41:30 +01:00
put ( docs . kinds . ack , "other:9" , docs . ackTuple ( { persona : "other" , throughSeq : 9 } ) ) ;
2026-10-30 02:49:59 +01:00
document = await reply ;
assert . deepEqual ( [ document . schemaId , docs . parseRefusal ( msgpack . decode ( document . payload ) ) ] , [ docs . kinds . refusal , { persona : "other" , refusal : "UnknownPersona" } ] , "an ack for an unlisted Persona is refused, typed, and not applied" ) ;
2026-10-29 19:41:30 +01:00
assert . deepEqual ( calls . acks , [ 7 ] ) ;
reply = nextReply ( ) ;
put ( docs . kinds . pendingRequest , "other" , docs . pendingRequestTuple ( { persona : "other" , afterSeq : 0 } ) ) ;
document = await reply ;
2026-10-30 02:49:59 +01:00
assert . deepEqual ( [ document . schemaId , docs . parseRefusal ( msgpack . decode ( document . payload ) ) . refusal ] , [ docs . kinds . refusal , "UnknownPersona" ] , "unknown_persona_refused" ) ;
reply = nextReply ( ) ;
put ( docs . kinds . sendRequest , "req-9" , docs . sendRequestTuple ( { requestId : "req-9" , persona : "other" , roomAlias : "#operator:gamecult.org" , text : "speak" } ) ) ;
document = await reply ;
assert . deepEqual ( docs . parseSendReceipt ( msgpack . decode ( document . payload ) ) , { requestId : "req-9" , eventId : null , refusal : "UnknownPersona" } ) ;
assert . equal ( calls . sends . length , 1 , "nothing reached a mouth for the unlisted Persona" ) ;
2026-10-29 19:41:30 +01:00
} finally {
server . close ( ) ;
connection . close ( ) ;
try { client . close ( ) ; } catch { /* the connection already closed its socket */ }
}
} ) ;
test ( "the RUDP endpoint refuses to listen beyond loopback" , async ( ) => {
2026-10-30 02:49:59 +01:00
await assert . rejects ( startChatEndpoint ( { mouths : new Map ( ) , runtime , port : 0 , host : "0.0.0.0" , connectionKey : "k" , runtimeId : "m" } ) , /loopback/ ) ;
2026-10-29 19:41:30 +01:00
} ) ;
test ( "loopback means exactly an IPv4 127.x.y.z address" , async ( ) => {
2026-10-30 02:49:59 +01:00
const refuses = host => assert . rejects ( startChatEndpoint ( { mouths : new Map ( ) , runtime , port : 0 , host , connectionKey : "k" , runtimeId : "m" } ) , /loopback/ ) ;
2026-10-29 19:41:30 +01:00
for ( const host of [ "9127.0.0.1" , "127.0.0.1x" , "10.127.0.0.1" , "127.0.0" , "" ] ) await refuses ( host ) ;
2026-10-30 02:49:59 +01:00
const ok = await startChatEndpoint ( { mouths : new Map ( ) , runtime , port : 0 , host : "127.10.20.30" , connectionKey : "k" , runtimeId : "m" } ) . catch ( e => e ) ;
2026-10-29 19:41:30 +01:00
assert . ok ( ! /loopback/ . test ( ok ? . message ? ? "" ) , "a multi-digit loopback address is accepted by the check" ) ;
ok . close ? . ( ) ;
} ) ;
2026-10-30 02:49:59 +01:00
test ( "one process serves N Personas from systemd credentials: each token only in its own requests, nothing joined while braked, and no token on stderr" , async ( ) => {
const dir = await mkdtemp ( resolve ( tmpdir ( ) , "mouth-main-" ) ) ;
const creds = resolve ( dir , "creds" ) , control = resolve ( dir , "control" ) , stateDir = resolve ( dir , "state" ) ;
const tokens = { eureka : "syt_AAA_eureka_token" , nova : "syt_BBB_nova_token" } ;
await mkdir ( creds , { recursive : true } ) ;
for ( const [ persona , token ] of Object . entries ( tokens ) ) await writeFile ( resolve ( creds , ` matrix-token- ${ persona } ` ) , ` ${ token } \n ` ) ;
await writeFile ( resolve ( creds , "chat-key" ) , "chat-key-credential\n" ) ;
await mkdir ( resolve ( control , "eureka" ) , { recursive : true } ) ;
await mkdir ( resolve ( control , "nova" ) , { recursive : true } ) ;
await writeControl ( resolve ( control , "eureka" , "control.cc" ) , [ true , "2026-10-29T00:00:00Z" , "op" ] ) ;
await writeControl ( resolve ( control , "nova" , "control.cc" ) , [ false , "2026-10-29T00:00:00Z" , "op" ] ) ; // nova is braked
const seen = [ ] ;
const joinedBy = new Map ( ) ;
const hs = createServer ( ( req , res ) => {
const auth = req . headers . authorization ? ? "" ;
const path = decodeURIComponent ( new URL ( req . url , "http://x" ) . pathname ) . replace ( "/_matrix/client/v3" , "" ) ;
seen . push ( { auth , method : req . method , path } ) ;
const reply = body => { res . writeHead ( 200 , { "content-type" : "application/json" } ) ; res . end ( JSON . stringify ( body ) ) ; } ;
if ( path === "/account/whoami" ) return reply ( { user _id : auth . includes ( tokens . eureka ) ? "@eureka:t" : "@nova:t" } ) ;
if ( path === "/joined_rooms" ) return reply ( { joined _rooms : [ ... ( joinedBy . get ( auth ) ? ? [ ] ) ] } ) ;
if ( path . startsWith ( "/join/" ) ) { joinedBy . set ( auth , ( joinedBy . get ( auth ) ? ? new Set ( ) ) . add ( path . slice ( 6 ) ) ) ; return reply ( { room _id : path . slice ( 6 ) } ) ; }
if ( path . startsWith ( "/directory/room/" ) ) return reply ( { room _id : "!swarm:t" } ) ;
if ( path === "/sync" ) return reply ( { next _batch : "b0" } ) ;
res . writeHead ( 404 ) ; res . end ( "{}" ) ;
} ) ;
await new Promise ( ok => hs . listen ( 0 , "127.0.0.1" , ok ) ) ;
const probe = dgram . createSocket ( "udp4" ) ;
await new Promise ( ok => probe . bind ( 0 , "127.0.0.1" , ok ) ) ;
const port = probe . address ( ) . port ;
probe . close ( ) ;
const child = spawn ( process . execPath , [ resolve ( root , "tools" , "persona-matrix-mouth.mjs" ) ] , {
env : {
PATH : process . env . PATH , CULTLIB _ROOT : resolve ( process . env . CULTLIB _ROOT ? ? resolve ( root , ".." , "CultLib" ) ) , PERSONAS : "eureka,nova" , CREDENTIALS _DIRECTORY : creds , CONTROL _ROOT : control ,
STATE _DIRECTORY : stateDir , MATRIX _HOMESERVER : ` http://127.0.0.1: ${ hs . address ( ) . port } ` , MATRIX _ROOMS : "#swarm:t" , MATRIX _SERVER _NAME : "t" , CHAT _RUDP _LISTEN : ` 127.0.0.1: ${ port } ` ,
} ,
} ) ;
let stderr = "" ;
child . stderr . on ( "data" , c => ( stderr += c ) ) ;
try {
for ( let i = 0 ; i < 100 && ! ( seen . some ( r => r . path === "/sync" && r . auth . includes ( tokens . nova ) ) && seen . some ( r => r . path === "/sync" && r . auth . includes ( tokens . eureka ) ) ) ; i ++ ) await new Promise ( r => setTimeout ( r , 50 ) ) ;
const by = persona => seen . filter ( r => r . auth === ` Bearer ${ tokens [ persona ] } ` ) ;
assert . ok ( by ( "eureka" ) . some ( r => r . path === "/sync" ) && by ( "nova" ) . some ( r => r . path === "/sync" ) , ` both accounts sync from one process; stderr: ${ stderr } ` ) ;
assert . equal ( by ( "eureka" ) . length + by ( "nova" ) . length , seen . length , "every request carries exactly one Persona's token" ) ;
assert . deepEqual ( by ( "eureka" ) . filter ( r => r . method === "POST" ) . map ( r => r . path ) , [ "/join/!swarm:t" ] , "the released Persona joins" ) ;
assert . deepEqual ( by ( "nova" ) . filter ( r => r . method === "POST" ) , [ ] , "the braked Persona joins nothing" ) ;
} finally {
child . kill ( ) ;
await new Promise ( ok => hs . close ( ok ) ) ;
}
assert . ok ( ! Object . values ( tokens ) . some ( t => stderr . includes ( t ) ) && ! stderr . includes ( "chat-key-credential" ) , "no secret on stderr" ) ;
} ) ;
test ( "a reply is paged to the byte budget, oldest first, and one delivery that will not encode is skipped and logged without silencing the rest" , ( ) => {
const mk = ( n , body ) => ( { ... delivery , seq : n , eventId : ` $ e ${ n } ` , text : body } ) ;
const logs = [ ] ;
const page = pageOf ( "eureka" , [ mk ( 1 , "ok" ) , { ... mk ( 2 , "bad" ) , sender : 123 } , mk ( 3 , "fine" ) ] , msgpack , l => logs . push ( l ) ) ;
assert . deepEqual ( page [ 1 ] . map ( t => t [ 0 ] ) , [ 1 , 3 ] ) ;
assert . equal ( logs . length , 1 ) ;
assert . ok ( logs [ 0 ] . includes ( "delivery 2 skipped" ) ) ;
const bodies = Array . from ( { length : 200 } , ( _ , i ) => mk ( i + 1 , "x" . repeat ( 3900 ) ) ) ;
const paged = pageOf ( "eureka" , bodies , msgpack ) ;
const size = msgpack . encode ( paged ) . length ;
assert . ok ( size <= docs . limits . replyBytes + 1000 , ` page of ${ size } bytes is over the budget ` ) ;
assert . ok ( paged [ 1 ] . length > 1 && paged [ 1 ] . length < 200 ) ;
assert . deepEqual ( paged [ 1 ] . map ( t => t [ 0 ] ) , paged [ 1 ] . map ( ( _ , i ) => i + 1 ) , "the page is the oldest deliveries" ) ;
assert . equal ( pageOf ( "eureka" , [ mk ( 1 , "x" . repeat ( 4000 ) ) ] , msgpack ) [ 1 ] . length , 1 , "a single delivery is always served" ) ;
} ) ;
2026-11-03 11:01:44 +01:00
// The send pair v2: the bytes the Huginn organ pins from its side.
const v2Dir = resolve ( root , "tests" , "fixtures" , "persona-chat-v2" ) ;
const v2Request = basis => docs . sendRequestV2Tuple ( { requestId : "req-1" , persona : "eureka" , roomAlias : "#swarm:gamecult.org" , text : "hi" , basis } ) ;
const v2Samples = {
"gamecult.persona.chat_send_request.v2.nil-basis" : { tuple : v2Request ( null ) , parse : docs . parseSendRequestV2 } ,
"gamecult.persona.chat_send_request.v2.seen-and-since" : { tuple : v2Request ( { seenEventId : "$seen" , sinceUnixMs : 1793698425998 } ) , parse : docs . parseSendRequestV2 } ,
"gamecult.persona.chat_send_request.v2.since-only" : { tuple : v2Request ( { seenEventId : null , sinceUnixMs : 1793698425998 } ) , parse : docs . parseSendRequestV2 } ,
"gamecult.persona.chat_send_receipt.v2.sent" : { tuple : docs . sendReceiptV2Tuple ( { requestId : "req-1" , eventId : "$sent" } ) , parse : docs . parseSendReceiptV2 } ,
"gamecult.persona.chat_send_receipt.v2.room-moved" : { tuple : docs . sendReceiptV2Tuple ( { requestId : "req-1" , refusal : "RoomMoved" , newer : [ "$a" , "$b" ] } ) , parse : docs . parseSendReceiptV2 } ,
} ;
for ( const [ name , { tuple , parse } ] of Object . entries ( v2Samples ) ) {
test ( ` golden fixture: ${ name } is byte-stable and parses back ` , async ( ) => {
const bytes = Buffer . from ( msgpack . encode ( tuple ) ) ;
const file = resolve ( v2Dir , ` ${ name } .msgpack ` ) ;
if ( process . env . UPDATE _FIXTURES === "1" ) { await mkdir ( v2Dir , { recursive : true } ) ; await writeFile ( file , bytes ) ; }
assert . deepEqual ( bytes , await readFile ( file ) , "wire bytes changed: Huginn decodes these fixtures" ) ;
assert . deepEqual ( parse ( msgpack . decode ( bytes ) ) , parse ( tuple ) ) ;
} ) ;
}
test ( "RoomMoved is v2 only, and the v2 documents are strict about arity and newer" , ( ) => {
assert . throws ( ( ) => docs . sendReceiptTuple ( { requestId : "r" , refusal : "RoomMoved" } ) , /unknown refusal/ , "the v1 receipt cannot carry RoomMoved" ) ;
assert . throws ( ( ) => docs . parseSendReceipt ( [ "r" , null , "RoomMoved" ] ) , /unknown refusal/ ) ;
assert . throws ( ( ) => docs . refusalTuple ( { persona : "p" , refusal : "RoomMoved" } ) ) ;
assert . throws ( ( ) => docs . sendReceiptV2Tuple ( { requestId : "r" , refusal : "RoomMoved" } ) , /newer/ , "RoomMoved names what moved the room" ) ;
assert . throws ( ( ) => docs . sendReceiptV2Tuple ( { requestId : "r" , eventId : "$e" , newer : [ "$x" ] } ) , /newer/ , "a sent receipt names nothing" ) ;
assert . throws ( ( ) => docs . sendReceiptV2Tuple ( { requestId : "r" , refusal : "RoomMoved" , newer : Array . from ( { length : 51 } , ( _ , i ) => ` $ ${ i } ` ) } ) , /50/ ) ;
assert . throws ( ( ) => docs . parseSendRequestV2 ( [ "r" , "eureka" , "#a:b" , "x" ] ) , /5-field/ , "a v1 tuple is not a v2 request" ) ;
assert . throws ( ( ) => docs . parseSendRequest ( v2Request ( null ) ) , /4-field/ , "a v2 tuple is not a v1 request" ) ;
assert . throws ( ( ) => docs . parseSendRequestV2 ( [ "r" , "eureka" , "#a:b" , "x" , [ null ] ] ) , /2-field/ ) ;
assert . throws ( ( ) => docs . parseSendRequestV2 ( [ "r" , "eureka" , "#a:b" , "x" , [ null , - 1 ] ] ) ) ;
} ) ;
test ( "the RUDP endpoint answers a v1 send with the three-field receipt and a v2 send with the four-field one" , async ( ) => {
const seen = [ ] ;
const mouth = { pendingAfter : ( ) => [ ] , ack : async ( ) => { } , send : async request => {
seen . push ( request ) ;
return request . basis ? { requestId : request . requestId , eventId : null , refusal : "RoomMoved" , newer : [ "$a" ] } : { requestId : request . requestId , eventId : "$sent" , refusal : null , newer : [ ] } ;
} } ;
const key = "connection-key-for-v2" ;
const server = await startChatEndpoint ( { mouths : new Map ( [ [ "eureka" , mouth ] ] ) , runtime , port : 0 , connectionKey : key , runtimeId : "mouth-test" } ) ;
const client = dgram . createSocket ( "udp4" ) ;
await new Promise ( ok => client . bind ( 0 , "127.0.0.1" , ok ) ) ;
const connection = new runtime . cultnet . CultNetRudpSocketTransportConnection ( {
runtimeId : "huginn-test" , socket : client , mode : "client" , remoteHost : "127.0.0.1" , remotePort : server . address . port ,
connectionId : connectionIdFor ( key ) , initialSequence : 1 , resendDelayMs : 50 , resendPollMs : 10 , maxFragmentBytes : 1200 ,
} ) ;
const replies = [ ] ;
connection . on ( "frame" , value => replies . push ( runtime . cultnet . parseCultNetMessage ( msgpack . decode ( value . payload ) , "cultnet.schema.v0" ) . document ) ) ;
const put = ( kind , recordKey , tuple ) => connection . send ( "schema" , msgpack . encode ( runtime . cultnet . encodeCultNetMessageForWire (
docs . envelope ( { kind , recordKey , payload : msgpack . encode ( tuple ) , sourceRuntimeId : "huginn-test" , sourceAgentId : "eureka" , sourceRole : "persona-organ" } ) , "cultnet.schema.v0" ) ) ) ;
const reply = async n => { for ( let i = 0 ; i < 250 && replies . length < n ; i ++ ) await new Promise ( r => setTimeout ( r , 20 ) ) ; assert . ok ( replies . length >= n , "the mouth answered" ) ; return replies [ n - 1 ] ; } ;
try {
connection . connect ( ) ;
for ( let i = 0 ; i < 100 && ! connection . connected ; i ++ ) await new Promise ( r => setTimeout ( r , 20 ) ) ;
put ( docs . kinds . sendRequest , "v1" , docs . sendRequestTuple ( { requestId : "v1" , persona : "eureka" , roomAlias : "#swarm:gamecult.org" , text : "old" } ) ) ;
let document = await reply ( 1 ) ;
assert . equal ( document . schemaId , docs . kinds . sendReceipt ) ;
assert . deepEqual ( msgpack . decode ( document . payload ) , [ "v1" , "$sent" , null ] , "three fields, as the old organ parses them" ) ;
put ( docs . kinds . sendRequestV2 , "v2" , docs . sendRequestV2Tuple ( { requestId : "v2" , persona : "eureka" , roomAlias : "#swarm:gamecult.org" , text : "new" , basis : { seenEventId : "$s" , sinceUnixMs : 5 } } ) ) ;
document = await reply ( 2 ) ;
assert . equal ( document . schemaId , docs . kinds . sendReceiptV2 ) ;
assert . deepEqual ( docs . parseSendReceiptV2 ( msgpack . decode ( document . payload ) ) , { requestId : "v2" , eventId : null , refusal : "RoomMoved" , newer : [ "$a" ] } ) ;
assert . deepEqual ( seen . map ( r => r . basis ) , [ undefined , { seenEventId : "$s" , sinceUnixMs : 5 } ] , "v1 reaches the mouth with no basis, v2 with hers" ) ;
put ( docs . kinds . sendRequestV2 , "v2-ghost" , docs . sendRequestV2Tuple ( { requestId : "v2-ghost" , persona : "ghost" , roomAlias : "#swarm:gamecult.org" , text : "x" } ) ) ;
document = await reply ( 3 ) ;
assert . deepEqual ( docs . parseSendReceiptV2 ( msgpack . decode ( document . payload ) ) , { requestId : "v2-ghost" , eventId : null , refusal : "UnknownPersona" , newer : [ ] } ) ;
} finally {
server . close ( ) ;
connection . close ( ) ;
try { client . close ( ) ; } catch { /* the connection already closed its socket */ }
}
} ) ;