flybrain/services/bridge/tests/commands.test.ts
acamilo 660c3cf00d
Some checks failed
ci / node 22 (test + typecheck) (push) Has been cancelled
ci / rust stable (cargo test --workspace --release) (push) Has been cancelled
ci / infra/tests/lint.sh (push) Has been cancelled
ci / playwright apps/stage (allowed to fail) (push) Has been cancelled
flybrain v0.4.0: public tree (history retained privately)
2026-09-21 15:09:46 +00:00

140 lines
6.1 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createSend } from '../src/chat';
import { handleCommand, parseCommand, type CommandDeps } from '../src/commands';
import { RateLimiter } from '../src/ratelimit';
import { FakeChatSender, FakeSimClient } from './helpers';
function makeDeps(sim: FakeSimClient, sender: FakeChatSender): CommandDeps {
return {
sim,
send: createSend(sender),
rateLimiter: new RateLimiter({
perUserPerCommandMs: 60_000,
globalReplyMs: 0, // disable the global bucket for most command tests; ratelimit.test.ts covers it
chatMessagesPer30s: 15,
perCommandCooldownMs: { sugar: 0 },
}),
config: { gameTitle: 'Pokemon Red' },
};
}
// -- parseCommand --------------------------------------------------------------------------
void test('parseCommand recognizes each of the five commands, case-insensitively', () => {
assert.equal(parseCommand('!fly'), 'fly');
assert.equal(parseCommand('!Brain'), 'brain');
assert.equal(parseCommand('!HOW'), 'how');
assert.equal(parseCommand('!stuck now please'), 'stuck');
assert.equal(parseCommand('!sugar'), 'sugar');
});
void test('parseCommand returns null for non-commands and unknown commands', () => {
assert.equal(parseCommand('hello there'), null);
assert.equal(parseCommand('!unknown'), null);
assert.equal(parseCommand(''), null);
assert.equal(parseCommand(' '), null);
});
// -- !fly / !brain / !how ------------------------------------------------------------------
void test('!fly replies with the fly template including the configured game title', async () => {
const sender = new FakeChatSender();
const deps = makeDeps(new FakeSimClient(), sender);
await handleCommand('fly', { id: 'u1', displayName: 'alex' }, deps);
assert.equal(sender.sent.length, 1);
assert.match(sender.sent[0]!, /Pokemon Red/);
});
void test('!brain and !how also substitute the configured game title, never a hardcoded one', async () => {
const sender = new FakeChatSender();
const deps = makeDeps(new FakeSimClient(), sender);
await handleCommand('brain', { id: 'u1', displayName: 'alex' }, deps);
await handleCommand('how', { id: 'u1', displayName: 'alex' }, { ...deps, rateLimiter: deps.rateLimiter });
assert.match(sender.sent[0]!, /Pokemon Red/);
});
// -- !stuck ---------------------------------------------------------------------------------
void test('!stuck formats milestone.label and milestone.sinceSeconds from /status', async () => {
const sender = new FakeChatSender();
const sim = new FakeSimClient();
const deps = makeDeps(sim, sender);
await handleCommand('stuck', { id: 'u1', displayName: 'alex' }, deps);
assert.equal(sender.sent.length, 1);
assert.match(sender.sent[0]!, /Left the bedroom/);
assert.match(sender.sent[0]!, /3m 12s/); // 192 seconds
});
void test('!stuck stays silent (no chat noise) when the sim is unreachable', async () => {
const sender = new FakeChatSender();
const sim = new FakeSimClient();
sim.statusResult = { ok: false, kind: 'timeout' };
const deps = makeDeps(sim, sender);
await handleCommand('stuck', { id: 'u1', displayName: 'alex' }, deps);
assert.equal(sender.sent.length, 0);
});
// -- !sugar -----------------------------------------------------------------------------------
void test('!sugar accepted: replies with sugarAccepted and calls sim.stimulate with a validated name and source chat', async () => {
const sender = new FakeChatSender();
const sim = new FakeSimClient();
const deps = makeDeps(sim, sender);
await handleCommand('sugar', { id: 'u1', displayName: 'fly_fan_42' }, deps);
assert.equal(sim.stimulateCalls.length, 1);
assert.deepEqual(sim.stimulateCalls[0], { by: 'fly_fan_42', source: 'chat' });
assert.match(sender.sent[0]!, /fly_fan_42/);
});
void test('!sugar validates a hostile display name to "a viewer" before it ever reaches the sim', async () => {
const sender = new FakeChatSender();
const sim = new FakeSimClient();
const deps = makeDeps(sim, sender);
await handleCommand('sugar', { id: 'u1', displayName: '<script>alert(1)</script>' }, deps);
assert.deepEqual(sim.stimulateCalls[0], { by: 'a viewer', source: 'chat' });
assert.match(sender.sent[0]!, /a viewer/);
assert.doesNotMatch(sender.sent[0]!, /script/);
});
void test('!sugar on cooldown: sim 429 maps to sugarCooldown with a truthful retry time', async () => {
const sender = new FakeChatSender();
const sim = new FakeSimClient();
sim.stimulateResult = { ok: false, kind: 'rate_limited', retryAfterMs: 4_200 };
const deps = makeDeps(sim, sender);
await handleCommand('sugar', { id: 'u1', displayName: 'alex' }, deps);
assert.match(sender.sent[0]!, /cooldown/);
assert.match(sender.sent[0]!, /5s/); // ceil(4200ms) = 5s
});
void test('!sugar disabled: sim forbidden maps to sugarDisabled', async () => {
const sender = new FakeChatSender();
const sim = new FakeSimClient();
sim.stimulateResult = { ok: false, kind: 'forbidden', error: 'nope' };
const deps = makeDeps(sim, sender);
await handleCommand('sugar', { id: 'u1', displayName: 'alex' }, deps);
assert.match(sender.sent[0]!, /turned off/);
});
void test('!sugar disabled: sim timeout also maps to sugarDisabled', async () => {
const sender = new FakeChatSender();
const sim = new FakeSimClient();
sim.stimulateResult = { ok: false, kind: 'timeout' };
const deps = makeDeps(sim, sender);
await handleCommand('sugar', { id: 'u1', displayName: 'alex' }, deps);
assert.match(sender.sent[0]!, /turned off/);
});
// -- rate limiting integration ---------------------------------------------------------------
void test('a rate-limited command is dropped silently: no chat reply, no sim call', async () => {
const sender = new FakeChatSender();
const sim = new FakeSimClient();
const deps = makeDeps(sim, sender);
await handleCommand('sugar', { id: 'u1', displayName: 'alex' }, deps);
assert.equal(sender.sent.length, 1);
// same user, same command, immediately again -> blocked by the per-user 60s bucket
await handleCommand('sugar', { id: 'u1', displayName: 'alex' }, deps);
assert.equal(sender.sent.length, 1);
assert.equal(sim.stimulateCalls.length, 1);
});