Bifrost/tests/persona-matrix-mouth-runtime.test.mjs
Meta Vens a76c70cc39 persona matrix mouth: a v2 send is refused if the room moved past her basis; check and PUT are one step per room across every Persona
Removes the bare PUT; the one PUT now lives inside the room gate. v1 stays served unchecked through the same gate.
Wire: chat_send_request.v2 (basis) and chat_send_receipt.v2 (newer); RoomMoved is v2 only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:01:44 +01:00

355 lines
24 KiB
JavaScript

// 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";
import { createServer } from "node:http";
import { spawn } from "node:child_process";
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";
import { cultcacheState, connectionIdFor, createBrakeReader, loadRuntime, pageOf, startChatEndpoint } from "../tools/persona-matrix-mouth.mjs";
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 }],
mentions: ["@eureka:gamecult.org", "@nova:gamecult.org"], isDirect: true,
};
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 },
[docs.kinds.refusal]: { tuple: docs.refusalTuple({ persona: "ghost", refusal: "UnknownPersona" }), parse: docs.parseRefusal },
};
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]));
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" }));
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" }));
assert.throws(() => docs.parseSendReceipt(["r", null, "Invented"]), /unknown refusal/);
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);
});
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();
}
});
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 = [];
const server = await startChatEndpoint({ mouths: new Map([["eureka", mouth]]), runtime, port: 0, connectionKey: key, runtimeId: "mouth-test", log: line => rejected.push(line) });
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"]);
assert.deepEqual(pending.deliveries[0], delivery, "sender, mentions and the DM flag cross the wire exactly");
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" });
reply = nextReply();
put(docs.kinds.ack, "other:9", docs.ackTuple({ persona: "other", throughSeq: 9 }));
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");
assert.deepEqual(calls.acks, [7]);
reply = nextReply();
put(docs.kinds.pendingRequest, "other", docs.pendingRequestTuple({ persona: "other", afterSeq: 0 }));
document = await reply;
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");
} 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 () => {
await assert.rejects(startChatEndpoint({ mouths: new Map(), runtime, port: 0, host: "0.0.0.0", connectionKey: "k", runtimeId: "m" }), /loopback/);
});
test("loopback means exactly an IPv4 127.x.y.z address", async () => {
const refuses = host => assert.rejects(startChatEndpoint({ mouths: new Map(), runtime, port: 0, host, connectionKey: "k", runtimeId: "m" }), /loopback/);
for (const host of ["9127.0.0.1", "127.0.0.1x", "10.127.0.0.1", "127.0.0", ""]) await refuses(host);
const ok = await startChatEndpoint({ mouths: new Map(), runtime, port: 0, host: "127.10.20.30", connectionKey: "k", runtimeId: "m" }).catch(e => e);
assert.ok(!/loopback/.test(ok?.message ?? ""), "a multi-digit loopback address is accepted by the check");
ok.close?.();
});
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");
});
// 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 */ }
}
});