import { test } from 'node:test'; import assert from 'node:assert/strict'; import type { ChatMessage } from '#shared/events.ts'; import { createSpikeDetector, type SpikeConfig } from './spike.ts'; const CFG: SpikeConfig = { windowSeconds: 15, baselineMinutes: 10, factor: 3, minDelta: 4, cooldownSeconds: 60, armed: false, laughEmotes: ['KEKW', 'LUL'], }; let clock = Date.parse('2026-09-09T20:00:00.000Z'); function msg(text: string, userId: string, advanceMs = 0): ChatMessage { clock += advanceMs; return { id: `m${clock}-${userId}`, ts: new Date(clock).toISOString(), userId, login: `u${userId}`, displayName: `U${userId}`, text, emotes: [], gifs: [], isFirst: false, isReturning: false, isCommand: text.startsWith('!'), isMod: false, isSub: false, isBroadcaster: false, bits: 0, color: null, }; } test('ruhiger Chat erzeugt keinen Kandidaten', () => { clock = Date.parse('2026-09-09T20:00:00.000Z'); const d = createSpikeDetector(CFG); let hits = 0; // Eine Nachricht alle 20 s - das Fenster ist nie voller als eine for (let i = 0; i < 30; i++) if (d.push(msg('hi', String(i % 4), 20_000))) hits++; assert.equal(hits, 0); }); test('ploetzlicher Ausbruch von mehreren Leuten wird erkannt', () => { clock = Date.parse('2026-09-09T20:00:00.000Z'); const d = createSpikeDetector(CFG); for (let i = 0; i < 30; i++) d.push(msg('ruhig', String(i % 3), 20_000)); let spike = null; for (let i = 0; i < 8 && !spike; i++) spike = d.push(msg('KEKW', `x${i}`, 300)); assert.ok(spike, 'Ausbruch haette einen Kandidaten liefern muessen'); assert.ok(spike.chatters >= 4, `zu wenig verschiedene Chatter: ${spike.chatters}`); assert.ok(spike.emoteHits > 0, 'Lach-Emotes wurden nicht gezaehlt'); assert.equal(spike.overThreshold, false, 'armed=false darf nicht ausloesen'); }); test('Vielschreiber allein erzeugt weniger Punkte als eine Gruppe', () => { const run = (users: string[]) => { clock = Date.parse('2026-09-09T20:00:00.000Z'); const d = createSpikeDetector(CFG); for (let i = 0; i < 30; i++) d.push(msg('ruhig', 'base', 20_000)); let spike = null; for (let i = 0; i < 10 && !spike; i++) spike = d.push(msg('hahaha', users[i % users.length]!, 300)); return spike; }; const solo = run(['einer']); const gruppe = run(['a', 'b', 'c', 'd', 'e']); assert.ok(solo && gruppe); assert.ok(gruppe.score > solo.score, `Gruppe (${gruppe.score}) muss hoeher liegen als Einzelperson (${solo.score})`); }); test('das Wort clip zaehlt stark', () => { clock = Date.parse('2026-09-09T20:00:00.000Z'); const d = createSpikeDetector(CFG); for (let i = 0; i < 30; i++) d.push(msg('ruhig', String(i % 3), 20_000)); let spike = null; for (let i = 0; i < 8 && !spike; i++) spike = d.push(msg('das war ein CLIP wert', `y${i}`, 300)); assert.ok(spike); assert.ok(spike.clipMentions >= 4, `clip-Erwaehnungen: ${spike.clipMentions}`); }); test('Abklingzeit verhindert Kandidaten im Sekundentakt', () => { clock = Date.parse('2026-09-09T20:00:00.000Z'); const d = createSpikeDetector(CFG); for (let i = 0; i < 30; i++) d.push(msg('ruhig', String(i % 3), 20_000)); let hits = 0; for (let i = 0; i < 60; i++) if (d.push(msg('KEKW', `z${i}`, 500))) hits++; // 60 Nachrichten in 30 s: ohne Abklingzeit waeren das viele Kandidaten assert.ok(hits <= 1, `Abklingzeit greift nicht, ${hits} Kandidaten`); });