'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { decideLobbySafety, findAdminMember, normalizeIdentity, toDotaLobbyDetails, validateLobbySettings, } = require('../lib/lobby-settings'); const { parseWorkshopPublishData } = require('../lib/workshop-metadata'); function validSettings(overrides = {}) { return { lobbyName: 'Long queue lobby', serverRegion: 3, customGameId: '123456789012345678', customGameMode: 'my_addon', customMapName: 'my_map', customMinPlayers: 2, customMaxPlayers: 10, notifyAtPlayers: 8, leaveAtPlayers: 9, adminNames: 'Alice, Bob', ...overrides, }; } test('validates per-lobby thresholds excluding the bot slot', () => { const settings = validateLobbySettings(validSettings({ leaveAtPlayers: 9 })); assert.equal(settings.leaveAtPlayers, 9); assert.equal(settings.customMaxPlayers, 10); assert.deepEqual(settings.adminNames, ['Alice', 'Bob']); }); test('rejects a leave threshold above the custom-game player maximum', () => { assert.throws( () => validateLobbySettings(validSettings({ leaveAtPlayers: 11 })), /cannot exceed the custom-game maximum/, ); }); test('rejects an unreachable leave threshold while the lobby owner occupies a slot', () => { assert.throws( () => validateLobbySettings(validSettings({ leaveAtPlayers: 10 })), /must be below the custom-game maximum/, ); }); test('rejects notification after auto-leave', () => { assert.throws( () => validateLobbySettings(validSettings({ notifyAtPlayers: 9, leaveAtPlayers: 8 })), /must not exceed/, ); }); test('normalizes unicode persona names and accepts SteamID64 as an admin identity', () => { const settings = validateLobbySettings(validSettings({ adminNames: ['Alice', '76561198000000000'] })); const byName = findAdminMember(settings, [{ steamId: '1', name: 'Alice' }]); const byId = findAdminMember(settings, [{ steamId: '76561198000000000', name: 'Renamed' }]); assert.equal(byName.name, 'Alice'); assert.equal(byId.name, 'Renamed'); assert.equal(normalizeIdentity(' ALICE '), 'alice'); }); test('maps validated settings to current protobuf field names', () => { const settings = validateLobbySettings(validSettings({ customGameCrc: '42', customGameTimestamp: '1234', })); assert.deepEqual(toDotaLobbyDetails(settings), { gameName: 'Long queue lobby', serverRegion: 3, gameMode: 15, allowCheats: false, fillWithBots: false, allowSpectating: true, passKey: '', lan: false, customGameMode: 'my_addon', customMapName: 'my_map', customDifficulty: 0, customGameId: '123456789012345678', customMinPlayers: 2, customMaxPlayers: 10, visibility: 0, customGameCrc: '42', customGameTimestamp: 1234, customGamePenalties: true, }); }); test('rejects a public custom lobby without its internal addon name', () => { assert.throws( () => validateLobbySettings(validSettings({ customGameMode: '' })), /Internal addon name is required/, ); }); test('leaves at the selected real-player count before doing name lookups', () => { const settings = validateLobbySettings(validSettings({ notifyAtPlayers: 2, leaveAtPlayers: 3 })); const members = [ { steamId: '1', name: '' }, { steamId: '2', name: '' }, { steamId: '3', name: '' }, ]; assert.deepEqual(decideLobbySafety(settings, members, 0), { action: 'player-limit', admin: null }); }); test('never recommends an abandon-style leave after lobby launch starts', () => { const settings = validateLobbySettings(validSettings({ notifyAtPlayers: 2, leaveAtPlayers: 3 })); const members = Array.from({ length: 3 }, (_, i) => ({ steamId: String(i + 1), name: '' })); assert.deepEqual(decideLobbySafety(settings, members, 1), { action: 'unsafe-state', admin: null }); }); test('reads the internal addon name from Steam Workshop publish_data', () => { assert.deepEqual(parseWorkshopPublishData(`"publish_data" { "source_folder" "custom_hero_clash" "publish_time" "1785566650" }`), { sourceFolder: 'custom_hero_clash', publishTime: '1785566650', }); });