import { test } from 'node:test'; import assert from 'node:assert/strict'; import { loadConfig } from '../src/config.js'; import { openDatabase } from '../src/db.js'; import { buildKnifeCommands, KnifeServers } from '../src/servers/knifeServers.js'; const pools = { competitive: { label: 'Competitive 5 gegen 5', gameType: 0, gameMode: 1, teamSize: 5, maps: [{ id: 'de_mirage', name: 'Mirage' }, { id: 'de_inferno', name: 'Inferno' }] }, wingman: { label: 'Wingman 2 gegen 2', gameType: 0, gameMode: 2, teamSize: 2, maps: [{ id: 'de_vertigo', name: 'Vertigo' }] }, }; const quiet = { log() {}, warn() {}, error() {} }; /** Adapter, dessen Zustand der Test steuert. */ function fakeAdapter() { const a = { name: 'docker', starts: [], configures: [], commands: [], stops: 0, state: 'starting', players: null, async start(args) { a.starts.push(args); return { handle: { container: `cs2mv-${args.matchId}`, n: a.starts.length }, host: '10.0.0.5', port: 27015 + a.starts.length - 1, tvPort: 27115 }; }, async status() { return { state: a.state, playerCount: a.players }; }, async configure(handle, plan) { a.configures.push(plan); }, async sendCommands(handle, cmds) { a.commands.push(...cmds); }, async stop() { a.stops += 1; }, }; return a; } function setup() { const config = loadConfig({ knife: { map: 'de_mirage', warmupSeconds: 60, maxServers: 2 }, docker: { serverNamePrefix: 'MaxLAN' } }); const adapter = fakeAdapter(); const knife = new KnifeServers({ db: openDatabase(':memory:'), adapter, config, pools, logger: quiet }); return { knife, adapter }; } const poll = async (knife) => { knife.lastPoll.clear(); await knife.poll(); }; test('Messerrunde: nur Messer, eine Runde, danach automatisch neues Spiel', () => { const plan = buildKnifeCommands({ map: 'de_inferno', warmupSeconds: 45 }); assert.equal(plan.beforeMapChange.at(-1), 'changelevel de_inferno'); assert.ok(plan.beforeMapChange.includes('sv_password ""'), 'offen, ohne Passwort'); for (const c of ['mp_maxrounds 1', 'mp_match_end_restart 1', 'mp_ct_default_secondary ""', 'mp_startmoney 0', 'mp_warmuptime 45']) { assert.ok(plan.afterMapChange.includes(c), `${c} fehlt`); } }); test('Start, Konfiguration und Ansicht ohne Geheimnisse', async () => { const { knife, adapter } = setup(); const created = await knife.create({}); assert.equal(created.name, 'Messer 1'); assert.equal(created.status, 'starting'); assert.equal(adapter.starts[0].serverName, 'MaxLAN | Messer 1 5v5 (Seitenwahl)'); assert.equal(adapter.starts[0].mode, 'competitive'); assert.equal(adapter.starts[0].secrets.serverPassword, ''); adapter.state = 'running'; await poll(knife); assert.equal(knife.row('knife1').status, 'running'); assert.equal(adapter.configures[0].map, 'de_mirage'); const [view] = knife.list(); assert.equal(view.connect, 'connect 10.0.0.5:27015'); assert.ok(!JSON.stringify(view).includes(knife.row('knife1').rcon_password), 'kein RCON-Passwort in der Ansicht'); }); test('Wingman 2v2: eigener Spielmodus, Maps nur aus dem Wingman-Pool', async () => { const { knife, adapter } = setup(); const k = await knife.create({ mode: 'wingman' }); assert.equal(k.modeShort, '2v2'); assert.equal(k.map, 'de_vertigo', 'Standard-Map gibt es im Wingman-Pool nicht, also die erste'); assert.equal(adapter.starts[0].mode, 'wingman'); assert.equal(adapter.starts[0].serverName, 'MaxLAN | Messer 1 2v2 (Seitenwahl)'); adapter.state = 'running'; await poll(knife); assert.ok(adapter.configures[0].beforeMapChange.includes('game_mode 2')); await assert.rejects(knife.create({ mode: 'wingman', map: 'de_mirage' }), /gibt es nicht für Wingman/); await assert.rejects(knife.create({ mode: 'deathmatch' }), /Unbekannter Modus/); }); test('Grenzen: nur Maps aus den Pools, höchstens maxServers', async () => { const { knife } = setup(); await assert.rejects(knife.create({ map: 'de_unbekannt' }), /gibt es nicht/); await knife.create({ map: 'de_inferno' }); await knife.create({}); await assert.rejects(knife.create({}), /Höchstens 2/); // Freie Nummer wird wiederverwendet await knife.remove('knife1'); assert.equal((await knife.create({})).name, 'Messer 1'); }); test('Teams gehen runter: Server setzt sich für die nächsten zurück', async () => { const { knife, adapter } = setup(); await knife.create({}); adapter.state = 'running'; await poll(knife); adapter.players = 4; await poll(knife); assert.equal(adapter.commands.length, 0); adapter.players = 0; await poll(knife); assert.deepEqual(adapter.commands, ['mp_warmup_start']); assert.match(knife.row('knife1').message, /zurückgesetzt/); }); test('Ausfall: Server startet neu, nach zu vielen Ausfällen gibt die App auf', async () => { const { knife, adapter } = setup(); await knife.create({}); adapter.state = 'failed'; await poll(knife); assert.equal(adapter.starts.length, 2, 'nach Ausfall neu gestartet'); assert.equal(knife.row('knife1').restarts, 1); for (let i = 0; i < 6; i++) await poll(knife); assert.equal(adapter.starts.length, 6, 'höchstens 5 Neustarts'); assert.equal(knife.row('knife1').status, 'failed'); // Neustart durch den Admin setzt den Zähler zurück adapter.state = 'starting'; await knife.restart('knife1'); assert.equal(knife.row('knife1').restarts, 0); assert.equal(adapter.starts.length, 7); });