430 lines
18 KiB
TypeScript
430 lines
18 KiB
TypeScript
/**
|
||
* The chat-to-screen path (`src/onscreen-chat.ts`), plus the static lint that keeps it the ONLY
|
||
* path: no raw chat may reach a sim payload by any other route.
|
||
*
|
||
* Two kinds of test here:
|
||
* - behavioural, against `FakeSimClient` and (for the real HTTP status codes) `tests/fake-sim.ts`;
|
||
* - a static-analysis lint over `src/`, in the same spirit as the `send(...)` template lint in
|
||
* `tests/templates.test.ts`.
|
||
*/
|
||
import assert from 'node:assert/strict';
|
||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||
import { dirname, join, relative } from 'node:path';
|
||
import test from 'node:test';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { HttpSimClient } from '../src/sim';
|
||
import { FakeClock } from '../src/ratelimit';
|
||
import { sanitizeChatText } from '@flybrain/feed';
|
||
import {
|
||
ECHO_WINDOW_MS,
|
||
KNOWN_BOT_LOGINS,
|
||
OnscreenChat,
|
||
isCommand,
|
||
shortenForPanel,
|
||
wrapSendWithOnscreenEcho,
|
||
type IncomingChatMessage,
|
||
} from '../src/onscreen-chat';
|
||
import { renderTemplate } from '../src/templates';
|
||
import { createSend } from '../src/chat';
|
||
import { FakeChatSender, FakeSimClient } from './helpers';
|
||
import { startFakeSim, type FakeSimHandle } from './fake-sim';
|
||
|
||
const SRC_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'src');
|
||
|
||
function message(overrides: Partial<IncomingChatMessage> = {}): IncomingChatMessage {
|
||
return { login: 'mothra_fan', displayName: 'mothra_fan', messageText: 'go left!', ...overrides };
|
||
}
|
||
|
||
function forwarder(
|
||
options: { enabled?: boolean; clock?: FakeClock } = {},
|
||
): { onscreen: OnscreenChat; sim: FakeSimClient } {
|
||
const sim = new FakeSimClient();
|
||
const onscreen = new OnscreenChat({
|
||
sim,
|
||
config: { featureOnscreenChat: options.enabled ?? true, botUser: 'flybridgebot' },
|
||
...(options.clock ? { clock: options.clock } : {}),
|
||
});
|
||
return { onscreen, sim };
|
||
}
|
||
|
||
// -- forwarding viewer messages ----------------------------------------------------------------
|
||
|
||
void test('an ordinary viewer message is sanitized and posted to /chat', async () => {
|
||
const { onscreen, sim } = forwarder();
|
||
assert.equal(await onscreen.forwardViewerMessage(message({ messageText: ' go LEFT! ' })), 'sent');
|
||
assert.deepEqual(sim.chatCalls, [{ by: 'mothra_fan', text: 'go LEFT!', bot: false }]);
|
||
});
|
||
|
||
void test('commands are not chatter and never reach the sim', async () => {
|
||
const { onscreen, sim } = forwarder();
|
||
for (const text of ['!sugar', ' !how', '!fly please', '!!!']) {
|
||
assert.equal(await onscreen.forwardViewerMessage(message({ messageText: text })), 'command', text);
|
||
}
|
||
assert.deepEqual(sim.chatCalls, []);
|
||
// A bang in the middle of a sentence is not a command.
|
||
assert.equal(await onscreen.forwardViewerMessage(message({ messageText: 'go left!' })), 'sent');
|
||
});
|
||
|
||
void test('known third-party bots are dropped', async () => {
|
||
const { onscreen, sim } = forwarder();
|
||
for (const login of KNOWN_BOT_LOGINS) {
|
||
const outcome = await onscreen.forwardViewerMessage(
|
||
message({ login: login.toUpperCase(), displayName: login, messageText: 'follow for more' }),
|
||
);
|
||
assert.equal(outcome, 'bot', login);
|
||
}
|
||
assert.deepEqual(sim.chatCalls, []);
|
||
});
|
||
|
||
void test('hostile messages are dropped before they leave the process', async () => {
|
||
const { onscreen, sim } = forwarder();
|
||
const hostile = [
|
||
'check out https://evil.example/pwn',
|
||
'www.evil.tv',
|
||
'discord.gg/abcd',
|
||
'zerowidth',
|
||
'gnippot ma I',
|
||
' |