219 lines
7.2 KiB
TypeScript
219 lines
7.2 KiB
TypeScript
/*
|
|
* Veskslop — unit tests for the pure communication logic.
|
|
*
|
|
* Usage: pnpm test:comm
|
|
*/
|
|
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
MessageLog,
|
|
TRUSTED_LOG_LIMIT,
|
|
UNTRUSTED_LOG_LIMIT,
|
|
assembleContext,
|
|
compareSnowflakes,
|
|
extractMentionIds,
|
|
formatContextLine,
|
|
hasTier,
|
|
isTrusted,
|
|
parseCommand,
|
|
parsePoll,
|
|
parseTier,
|
|
redactSecrets,
|
|
stripMentions,
|
|
toCachedMessage,
|
|
type CachedMessage,
|
|
type PuppetMessage
|
|
} from "./communication.js";
|
|
import { PACING, planSend } from "./pacing.js";
|
|
|
|
function cached(id: number, content = `m${id}`): CachedMessage {
|
|
return { id: String(id), authorId: "1", authorName: "a", content, timestamp: "" };
|
|
}
|
|
|
|
function entry(id: number, trusted: boolean): PuppetMessage {
|
|
return {
|
|
id: String(id),
|
|
channelId: "c",
|
|
authorId: trusted ? "owner" : "stranger",
|
|
authorName: "tester",
|
|
content: `msg ${id}`,
|
|
timestamp: new Date().toISOString(),
|
|
trusted
|
|
};
|
|
}
|
|
|
|
// ring buffer limits
|
|
{
|
|
const log = new MessageLog();
|
|
for (let i = 0; i < 150; i++) log.add(entry(i, true));
|
|
assert.equal(log.trusted.length, TRUSTED_LOG_LIMIT);
|
|
assert.equal(log.trusted[0].id, "50");
|
|
|
|
for (let i = 0; i < 40; i++) log.add(entry(i, false));
|
|
assert.equal(log.untrusted.length, UNTRUSTED_LOG_LIMIT);
|
|
assert.equal(log.untrusted[0].id, "10");
|
|
}
|
|
|
|
// mentions
|
|
{
|
|
const ids = extractMentionIds("<@!42> hi <@7>", [{ id: "9" }]);
|
|
assert.deepEqual([...ids].sort(), ["42", "7", "9"]);
|
|
assert.equal(stripMentions("<@!42> whitelist list", new Set(["42"])), "whitelist list");
|
|
assert.equal(stripMentions("<@42> <@99> hey", new Set(["42"])), "<@99> hey");
|
|
}
|
|
|
|
// commands
|
|
{
|
|
assert.deepEqual(parseCommand(" WHITELIST add <@123> "), { name: "whitelist", args: ["add", "<@123>"] });
|
|
assert.deepEqual(parseCommand("whitelist list"), { name: "whitelist", args: ["list"] });
|
|
assert.equal(parseCommand(" "), undefined);
|
|
}
|
|
|
|
// trust
|
|
{
|
|
assert.equal(isTrusted("owner", "owner", []), true);
|
|
assert.equal(isTrusted("trusted", "owner", ["trusted"]), true);
|
|
assert.equal(isTrusted("stranger", "owner", ["trusted"]), false);
|
|
}
|
|
|
|
// pacing: fast first send in a burst, 500-2800 ms afterwards, variable reset
|
|
{
|
|
for (let i = 0; i < 200; i++) {
|
|
const first = planSend(1_000_000, 0);
|
|
assert.ok(
|
|
first.delay >= PACING.firstMinMs && first.delay <= PACING.firstMaxMs,
|
|
`first delay out of range: ${first.delay}`
|
|
);
|
|
assert.ok(
|
|
first.burstUntil - 1_000_000 >= PACING.idleMinMs && first.burstUntil - 1_000_000 <= PACING.idleMaxMs,
|
|
`burst window out of range: ${first.burstUntil}`
|
|
);
|
|
|
|
const followUp = planSend(1_000_000, 1_001_000);
|
|
assert.ok(
|
|
followUp.delay >= PACING.burstMinMs && followUp.delay <= PACING.burstMaxMs,
|
|
`burst delay out of range: ${followUp.delay}`
|
|
);
|
|
|
|
// A fresh burst after the window expired is fast again.
|
|
const resumed = planSend(2_000_000, 1_000_000);
|
|
assert.ok(
|
|
resumed.delay >= PACING.firstMinMs && resumed.delay <= PACING.firstMaxMs,
|
|
`resumed delay out of range: ${resumed.delay}`
|
|
);
|
|
}
|
|
}
|
|
|
|
// context assembly
|
|
{
|
|
const messages = Array.from({ length: 20 }, (_, i) => cached(100 + i));
|
|
const byCount = assembleContext(messages, { maxMessages: 15, maxChars: 1_000_000 });
|
|
assert.equal(byCount.messages.length, 15);
|
|
assert.equal(byCount.messages[0].id, "105");
|
|
assert.equal(byCount.truncated, true);
|
|
|
|
const long = Array.from({ length: 10 }, (_, i) => cached(i, "y".repeat(50)));
|
|
const byChars = assembleContext(long, { maxMessages: 15, maxChars: 120 });
|
|
assert.equal(byChars.messages.length, 2);
|
|
assert.ok(byChars.chars <= 120);
|
|
assert.equal(byChars.truncated, true);
|
|
|
|
// the newest message is always kept, even when it exceeds the budget
|
|
const single = assembleContext([cached(1, "z".repeat(5000))], { maxChars: 100 });
|
|
assert.equal(single.messages.length, 1);
|
|
|
|
// the trigger message can be excluded
|
|
const excluded = assembleContext([cached(1), cached(2), cached(3)], { excludeId: "3" });
|
|
assert.deepEqual(
|
|
excluded.messages.map(m => m.id),
|
|
["1", "2"]
|
|
);
|
|
}
|
|
|
|
// context formatting and snowflake compare
|
|
{
|
|
const withFile: CachedMessage = { ...cached(1, "look"), attachments: ["clip.mp4"] };
|
|
assert.equal(formatContextLine(withFile), "a: look [file: clip.mp4]");
|
|
assert.equal(formatContextLine({ ...cached(2, ""), attachments: ["a.png"] }), "a: [file: a.png]");
|
|
|
|
assert.equal(compareSnowflakes("10", "9"), 1);
|
|
assert.equal(compareSnowflakes("9", "10"), -1);
|
|
assert.equal(compareSnowflakes("5", "5"), 0);
|
|
}
|
|
|
|
// raw message conversion keeps file names only
|
|
{
|
|
const converted = toCachedMessage({
|
|
id: 5,
|
|
author: { id: 7, username: "u" },
|
|
content: "see this",
|
|
timestamp: "t",
|
|
attachments: [{ filename: "a.png" }, { filename: "b.mp4" }]
|
|
});
|
|
assert.deepEqual(converted, {
|
|
id: "5",
|
|
authorId: "7",
|
|
authorName: "u",
|
|
content: "see this",
|
|
timestamp: "t",
|
|
attachments: ["a.png", "b.mp4"]
|
|
});
|
|
}
|
|
|
|
// timestamps in context lines
|
|
{
|
|
const withTime: CachedMessage = {
|
|
...cached(1, "hi"),
|
|
timestamp: "2026-09-11T14:03:00.000Z"
|
|
};
|
|
assert.match(formatContextLine(withTime), /^\[\d\d:\d\d\] a: hi$/);
|
|
assert.equal(formatContextLine(cached(2, "no time")), "a: no time");
|
|
}
|
|
|
|
// secret redaction
|
|
{
|
|
const fakeToken = `${"x".repeat(24)}.${"abcde"}.${"y".repeat(25)}`;
|
|
assert.equal(redactSecrets(`token ${fakeToken} here`), "token [redacted] here");
|
|
assert.equal(redactSecrets("mfa." + "z".repeat(30)), "[redacted]");
|
|
assert.equal(redactSecrets("nothing to hide"), "nothing to hide");
|
|
}
|
|
|
|
// trust tiers
|
|
{
|
|
assert.equal(hasTier("admin", "admin"), true);
|
|
assert.equal(hasTier("trusted", "admin"), false);
|
|
assert.equal(hasTier("trusted", "user"), true);
|
|
assert.equal(hasTier(undefined, "user"), false);
|
|
assert.equal(parseTier("trusted"), "trusted");
|
|
assert.equal(parseTier("bogus"), undefined);
|
|
}
|
|
|
|
// log lookup by ID
|
|
{
|
|
const log = new MessageLog();
|
|
log.add({ ...entry(1, true), context: [] });
|
|
assert.equal(log.find("1")?.id, "1");
|
|
assert.equal(log.find("nope"), undefined);
|
|
}
|
|
|
|
// poll parser
|
|
{
|
|
assert.deepEqual(parsePoll("Lunch? | Pizza | Sushi"), { question: "Lunch?", options: ["Pizza", "Sushi"] });
|
|
assert.deepEqual(parsePoll("Q | A | B | C | D"), { question: "Q", options: ["A", "B", "C", "D"] });
|
|
assert.deepEqual(parsePoll('"What country is best" Sweden // Denmark // Madagaskar'), {
|
|
question: "What country is best",
|
|
options: ["Sweden", "Denmark", "Madagaskar"]
|
|
});
|
|
// the whitespace guard keeps URLs together
|
|
assert.deepEqual(parsePoll('"Links?" https://a.example // https://b.example'), {
|
|
question: "Links?",
|
|
options: ["https://a.example", "https://b.example"]
|
|
});
|
|
assert.equal(parsePoll("Q | A"), undefined);
|
|
assert.equal(parsePoll("Q | A | B | C | D | E"), undefined);
|
|
assert.equal(parsePoll(`Q | ${"x".repeat(100)} | B`), undefined);
|
|
}
|
|
|
|
console.log("all communication tests passed");
|