import { test } from 'node:test';
import assert from 'node:assert/strict';
import { TEXT, TEXT_MARK, HAND_OVER, MAX_HTML, TextAssembler, encodeText, encodeHistory, parseMsg, parseHistory, mergeMsgs, isTextChannel, markText, stripMark, newId, type TextMsg } from '../src/core/text-signal.ts';
const msg = (i: string, t: number, h = 'hi'): TextMsg => ({ i, c: 3, t, n: 'ann', k: 'abc', h });
test('the marker turns a description into a text channel and comes off again', () => {
assert.equal(isTextChannel('Rules'), false);
const marked = markText('Rules', true);
assert.equal(marked, TEXT_MARK + 'Rules');
assert.equal(isTextChannel(marked), true);
// Marking twice leaves one marker
assert.equal(markText(marked, true), marked);
assert.equal(markText(marked, false), 'Rules');
assert.equal(stripMark(marked), 'Rules');
});
test('a message survives the trip, in packets Murmur accepts', async () => {
const body = { i: newId(), c: 7, h: 'hello ' + 'x'.repeat(3000) };
const packets = await encodeText(TEXT.msg, body);
assert.ok(packets.every(p => p.length <= 1000));
const assembler = new TextAssembler();
let signal = null;
for (const p of packets) signal = await assembler.push(1, p);
assert.equal(signal?.type, TEXT.msg);
assert.deepEqual(parseMsg(signal?.text), body);
});
test('a short message is one packet', async () => {
assert.equal((await encodeText(TEXT.msg, { i: newId(), c: 1, h: 'How is everyone doing today?' })).length, 1);
});
test('malformed messages are refused', () => {
assert.equal(parseMsg('nope'), null);
assert.equal(parseMsg('[]'), null);
assert.equal(parseMsg(JSON.stringify({ i: 'a', c: -1, h: 'x' })), null);
assert.equal(parseMsg(JSON.stringify({ i: 'a', c: 1, h: '' })), null);
assert.equal(parseMsg(JSON.stringify({ i: '', c: 1, h: 'x' })), null);
assert.equal(parseMsg(JSON.stringify({ i: 'a', c: 1, h: 'x'.repeat(MAX_HTML + 1) })), null);
});
test('history keeps valid entries, limits their number and pulls future times back', () => {
const now = 1_000_000;
const m = [
{ i: 'a', t: 500, n: 'ann', k: 'h1', h: 'one' },
{ i: 'b', t: now + 99999, n: 'bob', k: '', h: 'two' },
{ i: 'c', t: 'soon', n: 'eve', k: '', h: 'bad time' },
{ i: 'd', t: 600, n: 'eve', k: '', h: '' },
null
];
assert.deepEqual(parseHistory(JSON.stringify({ c: 4, m }), now), [
{ i: 'a', c: 4, t: 500, n: 'ann', k: 'h1', h: 'one' },
{ i: 'b', c: 4, t: now, n: 'bob', k: '', h: 'two' }
]);
const many = Array.from({ length: HAND_OVER + 30 }, (_, i) => ({ i: `m${i}`, t: i, n: 'ann', k: '', h: 'x' }));
assert.equal(parseHistory(JSON.stringify({ c: 4, m: many }), now).length, HAND_OVER);
assert.deepEqual(parseHistory('{"c":4}', now), []);
});
test('merging adds only unknown messages, in order of time, and keeps the newest', () => {
const have = [msg('a', 10), msg('c', 30)];
const { all, added } = mergeMsgs(have, [msg('b', 20), msg('a', 10), msg('d', 40)]);
assert.deepEqual(all.map(m => m.i), ['a', 'b', 'c', 'd']);
assert.deepEqual(added.map(m => m.i), ['b', 'd']);
assert.equal(mergeMsgs(have, [msg('a', 10)]).added.length, 0);
// Over the limit the oldest go, and an old message that does not make it is not reported as added
const kept = mergeMsgs([msg('x', 50), msg('y', 60)], [msg('old', 1), msg('z', 70)], 2);
assert.deepEqual(kept.all.map(m => m.i), ['y', 'z']);
assert.deepEqual(kept.added.map(m => m.i), ['z']);
});
test('history that is too large for one signal is cut down to the newest messages', async () => {
// Incompressible text, so 50 messages do not fit
const noise = (seed: number) => Array.from({ length: 1500 }, (_, i) => ((i + 1) * (seed + 7) * 2654435761 >>> 0).toString(36)).join('');
const msgs = Array.from({ length: 50 }, (_, i) => msg(`m${i}`, i, noise(i)));
const packets = await encodeHistory(3, msgs);
assert.ok(packets.length > 0 && packets.length <= 32);
const assembler = new TextAssembler();
let signal = null;
for (const p of packets) signal = await assembler.push(1, p);
const back = parseHistory(signal?.text);
assert.ok(back.length > 0 && back.length < 50);
assert.equal(back[back.length - 1].i, 'm49');
});