// Behavior registry: the kit-free two-stack seam. Clients arrive as plain
// objects inside ctx — these fakes prove the behaviors never need a kit
// import. Runs against the committed dist/.
import { test } from "node:test";
import assert from "node:assert/strict";
const bot = await import("../stack-a/dist/main.js");
class FakeEmitter {
handlers = {};
on(ev, cb) {
(this.handlers[ev] ??= []).push(cb);
}
emit(ev, ...a) {
for (const cb of this.handlers[ev] ?? []) cb(...a);
}
}
test("chat:echo echoes fresh messages from others only", async () => {
const chat = new FakeEmitter();
const sent = [];
chat.send = (text, thread) => {
sent.push([text, thread]);
return Promise.resolve();
};
const logs = [];
bot.attachBehavior("echo", { kind: "chat", chat, self: "ME", nick: "b", log: (l) => logs.push(l) });
const ts = Date.now() + 1000;
chat.emit("message", { ts, from: "OTHER", name: "ana", text: "hi", thread: "t1" });
chat.emit("message", { ts, from: "ME", name: "bot", text: "mine" });
chat.emit("message", { ts, from: "OTHER", name: "ana", text: "echo: hi" });
chat.emit("message", { ts: 1, from: "OTHER", name: "ana", text: "old history" });
chat.emit("message", { ts, from: "OTHER", name: "ana", text: null });
assert.deepEqual(sent, [["echo: hi", "t1"]]);
assert.equal(logs.length, 5);
assert.equal(logs[4], "chat ana: (img)");
});
test("chat:default falls back to echo; unknown name falls back to default", () => {
const chat = new FakeEmitter();
const sent = [];
chat.send = (t) => (sent.push(t), Promise.resolve());
bot.attachBehavior("no-such-behavior", { kind: "chat", chat, self: "ME", nick: "b", log: () => {} });
chat.emit("message", { ts: Date.now() + 1000, from: "OTHER", name: "x", text: "yo" });
assert.deepEqual(sent, ["echo: yo"]);
});
test("board:sign signs once and is idempotent", async () => {
const logs = [];
// unsigned board → addText after the 2s settle delay
const board = new FakeEmitter();
const added = [];
board.elements = () => [];
board.addText = (x, y, text, opts) => {
added.push([x, y, text, opts.size, opts.color]);
return Promise.resolve();
};
bot.attachBehavior("sign", { kind: "board", board, nick: "duhul", log: (l) => logs.push(l) });
board.emit("ready");
board.emit("element", { k: "path" }, "PEERABCDEFGH");
assert.deepEqual(added, []);
await new Promise((r) => setTimeout(r, 2300));
assert.deepEqual(added, [[40, 40, "duhul was here", 14, 5]]);
assert.ok(logs.includes("signed the board"));
assert.ok(logs.includes("board +path from PEERABCD"));
// already signed → no addText scheduled
const board2 = new FakeEmitter();
let added2 = 0;
board2.elements = () => [{ k: "text", text: "duhul was here" }];
board2.addText = () => (added2++, Promise.resolve());
bot.attachBehavior("sign", { kind: "board", board: board2, nick: "duhul", log: () => {} });
board2.emit("ready");
await new Promise((r) => setTimeout(r, 2300));
assert.equal(added2, 0);
});
test("game:default logs host changes and receipts", () => {
const room = new FakeEmitter();
room.app = "seance";
const logs = [];
bot.attachBehavior("default", { kind: "game", room, nick: "b", log: (l) => logs.push(l) });
room.emit("host", "PEER123456");
room.emit("host", null);
room.emit("receipt", { sc: [{ nm: "a", s: 3 }, { nm: "b", s: 1 }], sig: ["s1", "s2"] });
assert.deepEqual(logs, [
"seance: host 123456",
"seance: host lost",
"seance: receipt a:3,b:1 (2 sigs)",
]);
});