mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
89 new node:test cases. The dispatcher suite is the important one: it pins the behavior the registry-driven permission gate replaced a hardcoded action list with, asserting that admin-only commands are still admin-only, that the self-policing commands (goal/reason/verify/deter) still reach their own handlers as a non-admin, that unknown actions are still not public, and that `rsvp` is still not a command. Also covered: - cooldown boundaries, including that a refused call does not extend the window - actor identity keying across transports, and that extra browser tabs do not make a target ambiguous - honk/spin refusing without drive control and being unreachable from Discord - spin honouring applyPrivateDriveSafety instead of bypassing it - boo speaking only canned text, never anything the caller typed - disco obeying the room-light lock and restoring the lights when it ends - mention sanitizing on replies and on stored nicknames rendered by bonkboard - the stats store degrading to empty on a corrupt or wrong-shaped file Full server suite: 126 passing, 0 failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
167 lines
6.1 KiB
JavaScript
167 lines
6.1 KiB
JavaScript
// Operator Fun Stats Command Tests
|
|
// Purpose: Verifies the leaderboard ordering, rover pet counting, and what snitch reports.
|
|
// Scope: In-memory stats and roster doubles only.
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { createFunStatsCommands, formatLeaderboard } = require('./funStats');
|
|
const { createCooldownGate } = require('../cooldowns');
|
|
|
|
const ALICE = { id: 's1', data: { userId: 'u-alice', nickname: 'alice' } };
|
|
const BOB = { id: 's2', data: { userId: 'u-bob', nickname: 'bob' } };
|
|
|
|
function createHarness({
|
|
actorRows = [],
|
|
activeDrivers = {},
|
|
socket = ALICE,
|
|
rovers = new Map([
|
|
['rover-1', { id: 'rover-1', meta: { name: 'Roomba One' } }],
|
|
['rover-2', { id: 'rover-2', meta: { name: 'Roomba Two' } }],
|
|
]),
|
|
} = {}) {
|
|
const pets = new Map();
|
|
const handlers = createFunStatsCommands({
|
|
io: { sockets: { sockets: new Map([[ALICE.id, ALICE], [BOB.id, BOB]]) } },
|
|
rovers,
|
|
getNickname: (entry) => entry?.data?.nickname || '',
|
|
getActiveDrivers: () => activeDrivers,
|
|
getActorSocket: () => socket,
|
|
roverManager: { getPrimaryRoverForSocket: () => 'rover-1' },
|
|
sanitizeMentions: (text) => String(text || '').replace(/@everyone/gi, '[everyone]'),
|
|
funStatsService: {
|
|
listActorStats: () => actorRows,
|
|
bumpRoverPets: (roverId, by) => {
|
|
const next = (pets.get(roverId) || 0) + by;
|
|
pets.set(roverId, next);
|
|
return next;
|
|
},
|
|
},
|
|
cooldowns: createCooldownGate(),
|
|
config: { commands: { prefix: 'rs' } },
|
|
});
|
|
return { handlers, pets };
|
|
}
|
|
|
|
function message(actor = { id: 's1', userId: 'u-alice', label: 'alice' }) {
|
|
const replies = [];
|
|
return {
|
|
transport: 'web-chat',
|
|
actor,
|
|
replies,
|
|
reply: async (payload) => {
|
|
replies.push(payload);
|
|
return null;
|
|
},
|
|
};
|
|
}
|
|
|
|
test('the leaderboard sorts descending and drops zero scores', () => {
|
|
const rows = [
|
|
{ label: 'alice', bonksGiven: 2 },
|
|
{ label: 'bob', bonksGiven: 9 },
|
|
{ label: 'carol', bonksGiven: 0 },
|
|
];
|
|
const rendered = formatLeaderboard('Most bonks dealt', rows, 'bonksGiven');
|
|
const lines = rendered.split('\n');
|
|
assert.equal(lines[1], '1. bob — 9');
|
|
assert.equal(lines[2], '2. alice — 2');
|
|
assert.equal(lines.length, 3, 'carol should not appear with a zero score');
|
|
});
|
|
|
|
test('the leaderboard is capped at ten entries', () => {
|
|
const rows = Array.from({ length: 25 }, (_, index) => ({ label: `user${index}`, bonksGiven: index + 1 }));
|
|
const rendered = formatLeaderboard('Most bonks dealt', rows, 'bonksGiven');
|
|
assert.equal(rendered.split('\n').length - 1, 10);
|
|
});
|
|
|
|
test('an all-zero counter renders no section at all', () => {
|
|
assert.equal(formatLeaderboard('Most bonks dealt', [{ label: 'alice', bonksGiven: 0 }], 'bonksGiven'), null);
|
|
});
|
|
|
|
test('bonkboard says so when nothing has happened yet', async () => {
|
|
const { handlers } = createHarness({ actorRows: [] });
|
|
const msg = message();
|
|
await handlers.bonkboard(msg, []);
|
|
assert.match(msg.replies[0].content, /Nobody has been bonked yet/);
|
|
});
|
|
|
|
test('bonkboard renders each populated section', async () => {
|
|
const { handlers } = createHarness({
|
|
actorRows: [
|
|
{ label: 'alice', bonksGiven: 3, bonksTaken: 0, hugsGiven: 1 },
|
|
{ label: 'bob', bonksGiven: 0, bonksTaken: 3, hugsGiven: 0 },
|
|
],
|
|
});
|
|
const msg = message();
|
|
await handlers.bonkboard(msg, []);
|
|
|
|
assert.match(msg.replies[0].content, /Most bonks dealt/);
|
|
assert.match(msg.replies[0].content, /Most bonks taken/);
|
|
assert.match(msg.replies[0].content, /Most hugs given/);
|
|
});
|
|
|
|
test('bonkboard sanitizes stored labels, so a hostile nickname cannot ping a guild', async () => {
|
|
const { handlers } = createHarness({ actorRows: [{ label: '@everyone', bonksGiven: 1 }] });
|
|
const msg = message();
|
|
await handlers.bonkboard(msg, []);
|
|
assert.doesNotMatch(msg.replies[0].content, /@everyone/);
|
|
});
|
|
|
|
test('pet counts against the rover the caller is on when none is named', async () => {
|
|
const { handlers, pets } = createHarness();
|
|
const msg = message();
|
|
await handlers.pet(msg, []);
|
|
|
|
assert.match(msg.replies[0].content, /pets Roomba One/);
|
|
assert.match(msg.replies[0].content, /petted 1 time\./);
|
|
assert.equal(pets.get('rover-1'), 1);
|
|
});
|
|
|
|
test('pet accepts an explicit rover and keeps a separate count per rover', async () => {
|
|
const { handlers, pets } = createHarness();
|
|
await handlers.pet(message(), ['Roomba Two']);
|
|
await handlers.pet(message({ id: 's2', userId: 'u-bob', label: 'bob' }), ['Roomba Two']);
|
|
|
|
assert.equal(pets.get('rover-2'), 2);
|
|
assert.equal(pets.get('rover-1'), undefined);
|
|
});
|
|
|
|
test('pet pluralizes the running total', async () => {
|
|
const { handlers } = createHarness();
|
|
await handlers.pet(message(), []);
|
|
const second = message({ id: 's2', userId: 'u-bob', label: 'bob' });
|
|
await handlers.pet(second, []);
|
|
assert.match(second.replies[0].content, /petted 2 times\./);
|
|
});
|
|
|
|
test('pet from a transport with no socket asks for a rover name', async () => {
|
|
const { handlers, pets } = createHarness({ socket: null });
|
|
const msg = message({ id: '4242', label: 'DiscordUser' });
|
|
await handlers.pet(msg, []);
|
|
|
|
assert.match(msg.replies[0].content, /Name a rover/);
|
|
assert.equal(pets.size, 0);
|
|
});
|
|
|
|
test('snitch names the active driver and reports idle rovers as nobody', async () => {
|
|
const { handlers } = createHarness({ activeDrivers: { 'rover-1': 's2' } });
|
|
const msg = message();
|
|
await handlers.snitch(msg, []);
|
|
|
|
assert.match(msg.replies[0].content, /Roomba One — bob/);
|
|
assert.match(msg.replies[0].content, /Roomba Two — nobody/);
|
|
});
|
|
|
|
test('snitch handles a driver socket that has already gone away', async () => {
|
|
const { handlers } = createHarness({ activeDrivers: { 'rover-1': 'ghost-socket' } });
|
|
const msg = message();
|
|
await handlers.snitch(msg, []);
|
|
assert.match(msg.replies[0].content, /Roomba One — someone who will not say their name/);
|
|
});
|
|
|
|
test('snitch reports an empty fleet rather than an empty message', async () => {
|
|
const { handlers } = createHarness({ rovers: new Map() });
|
|
const msg = message();
|
|
await handlers.snitch(msg, []);
|
|
assert.match(msg.replies[0].content, /No rovers are online/);
|
|
});
|