Zugangsdaten kommen jetzt aus der App statt aus einer .env: ein Einrichtungs-Assistent auf http://localhost:4792/setup nimmt Client-ID, Secret und OBS-Passwort entgegen, der OAuth-Rueckruf laeuft auf denselben Server. Gespeichert wird verschlueselt (AES-256-GCM) in einem Volume, bewusst nicht in der Datenbank - die wird auf die Windows-Seite gesichert. Siehe ADR-0005. Technikbasis vereinfacht: Node 24 bringt node:sqlite, WebSocket, fetch und die Ausfuehrung von TypeScript mit. Das Projekt hat damit null Laufzeitabhaengigkeiten und keinen Build-Schritt; pnpm-Workspaces und die Paketmanifeste sind entfallen, Importe laufen ueber die imports-Map der Wurzel-package.json. erasableSyntaxOnly verhindert Syntax, die das Type Stripping stillschweigend entfernen wuerde. Siehe Nachtrag 2 in ADR-0001. Enthalten: - packages/core: Config, Layout, Logger mit Secret-Maskierung, Retry, YAML - packages/db: Migrationen, Repositories, alle SQL-Anweisungen an einem Ort - packages/twitch: Secret-Speicher, OAuth, Helix mit Token-Bucket, IRC - apps/ingest: Daemon, Einrichtungsseite, Spike-Erkennung - 20 Tests (IRC-Parser, Spike-Detektor, Datenbankschicht) Der Chat verbindet sich anonym, solange nichts hinterlegt ist - damit laesst sich die Datenaufnahme pruefen, bevor die Twitch-Apps existieren. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VTGjd92LsDDWF3frd9FTB
115 lines
4.5 KiB
TypeScript
115 lines
4.5 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import type { ChatMessage } from '#shared/events.ts';
|
|
import { migrate } from './index.ts';
|
|
import { repos } from './repos.ts';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
|
|
function freshDb() {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'abdulive-test-'));
|
|
const db = new DatabaseSync(path.join(dir, 't.db'));
|
|
db.exec('PRAGMA foreign_keys = ON');
|
|
migrate(db);
|
|
return { db, repo: repos(db), dir };
|
|
}
|
|
|
|
function msg(over: Partial<ChatMessage> = {}): ChatMessage {
|
|
return {
|
|
id: `m-${Math.random().toString(36).slice(2)}`,
|
|
ts: '2026-09-09T20:00:00.000Z',
|
|
userId: '42', login: 'maxi_2k', displayName: 'maxi_2k',
|
|
text: 'moin', emotes: [], isFirst: false, isReturning: false,
|
|
isCommand: false, isMod: false, isSub: false, isBroadcaster: false,
|
|
bits: 0, color: null, ...over,
|
|
};
|
|
}
|
|
|
|
test('legt Nutzer an und zaehlt Nachrichten', () => {
|
|
const { db, repo } = freshDb();
|
|
repo.openStream('s1', '2026-09-09T19:00:00.000Z');
|
|
repo.recordMessage(msg(), 's1');
|
|
repo.recordMessage(msg({ text: 'zweite' }), 's1');
|
|
|
|
const user = db.prepare('SELECT * FROM users WHERE id = ?').get('42') as
|
|
{ message_count: number; login: string; first_seen_at: string };
|
|
assert.equal(user.message_count, 2);
|
|
assert.equal(user.login, 'maxi_2k');
|
|
const n = db.prepare('SELECT COUNT(*) AS n FROM chat_messages').get() as { n: number };
|
|
assert.equal(n.n, 2);
|
|
});
|
|
|
|
test('Namenswechsel erhaelt die Historie', () => {
|
|
const { db, repo } = freshDb();
|
|
repo.openStream('s1', '2026-09-09T19:00:00.000Z');
|
|
repo.recordMessage(msg(), 's1');
|
|
// Gleiche user-id, neuer Login - so sieht eine Twitch-Namensaenderung aus
|
|
repo.recordMessage(msg({ login: 'maxi_neu', displayName: 'Maxi_Neu' }), 's1');
|
|
|
|
const rows = db.prepare('SELECT * FROM users').all() as Array<{ login: string; message_count: number }>;
|
|
assert.equal(rows.length, 1, 'darf keinen zweiten Nutzer angelegt haben');
|
|
assert.equal(rows[0]!.login, 'maxi_neu');
|
|
assert.equal(rows[0]!.message_count, 2);
|
|
});
|
|
|
|
test('zaehlt Streams, an denen jemand geschrieben hat', () => {
|
|
const { repo } = freshDb();
|
|
repo.openStream('s1', '2026-09-09T19:00:00.000Z');
|
|
repo.openStream('s2', '2026-09-10T19:00:00.000Z');
|
|
assert.equal(repo.recordMessage(msg(), 's1').streamsChatted, 1);
|
|
assert.equal(repo.recordMessage(msg(), 's1').streamsChatted, 1);
|
|
assert.equal(repo.recordMessage(msg(), 's2').streamsChatted, 2);
|
|
});
|
|
|
|
test('doppelte Nachrichten-IDs werden nicht zweimal gespeichert', () => {
|
|
const { db, repo } = freshDb();
|
|
repo.openStream('s1', '2026-09-09T19:00:00.000Z');
|
|
const m = msg({ id: 'immer-gleich' });
|
|
repo.recordMessage(m, 's1');
|
|
repo.recordMessage(m, 's1');
|
|
const n = db.prepare('SELECT COUNT(*) AS n FROM chat_messages').get() as { n: number };
|
|
assert.equal(n.n, 1);
|
|
});
|
|
|
|
test('Zuschauerzahlen setzen den Hoechststand', () => {
|
|
const { repo } = freshDb();
|
|
repo.openStream('s1', '2026-09-09T19:00:00.000Z');
|
|
repo.sample('s1', '2026-09-09T19:01:00.000Z', 8);
|
|
repo.sample('s1', '2026-09-09T19:02:00.000Z', 17);
|
|
repo.sample('s1', '2026-09-09T19:03:00.000Z', 11);
|
|
assert.equal(repo.getStream('s1')?.peak_viewers, 17);
|
|
});
|
|
|
|
test('offene Sitzung wird gefunden und geschlossen', () => {
|
|
const { repo } = freshDb();
|
|
repo.openStream('s1', '2026-09-09T19:00:00.000Z');
|
|
assert.equal(repo.latestOpenStream()?.id, 's1');
|
|
repo.closeStream('s1', '2026-09-09T22:00:00.000Z');
|
|
assert.equal(repo.latestOpenStream(), undefined);
|
|
});
|
|
|
|
test('erneutes openStream ueberschreibt eine laufende Sitzung nicht', () => {
|
|
const { repo } = freshDb();
|
|
repo.openStream('s1', '2026-09-09T19:00:00.000Z');
|
|
repo.openStream('s1', '2026-09-09T21:00:00.000Z');
|
|
assert.equal(repo.getStream('s1')?.started_at, '2026-09-09T19:00:00.000Z');
|
|
});
|
|
|
|
test('Nachrichten ohne laufenden Stream gehen trotzdem nicht verloren', () => {
|
|
const { db, repo } = freshDb();
|
|
repo.recordMessage(msg(), null);
|
|
const row = db.prepare('SELECT stream_id FROM chat_messages').get() as { stream_id: string | null };
|
|
assert.equal(row.stream_id, null);
|
|
});
|
|
|
|
test('unique_chatters zaehlt Personen, nicht Nachrichten', () => {
|
|
const { repo } = freshDb();
|
|
repo.openStream('s1', '2026-09-09T19:00:00.000Z');
|
|
repo.recordMessage(msg({ userId: '1', login: 'a' }), 's1');
|
|
repo.recordMessage(msg({ userId: '1', login: 'a' }), 's1');
|
|
repo.recordMessage(msg({ userId: '2', login: 'b' }), 's1');
|
|
assert.equal(repo.getStream('s1')?.unique_chatters, 2);
|
|
});
|