129 lines
5.2 KiB
TypeScript
129 lines
5.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { composeCfg, composeServerCfg, parseCfg, parseOverlay } from '../../src/lib/server/fleet/config-compose.js';
|
|
import { T5_ZOMBIE_MAPS } from '../../src/lib/server/fleet/map-aliases.js';
|
|
|
|
const BASELINE = [
|
|
'// GENERAL',
|
|
'set g_inactivity "190"',
|
|
'set sv_voice "1"',
|
|
'',
|
|
'// GAMEPLAY',
|
|
'set g_playerCollision "0"',
|
|
'set sv_maprotation "map zombie_theater"'
|
|
].join('\n');
|
|
|
|
describe('parseCfg', () => {
|
|
it('captures set keys and preserves comments/blanks', () => {
|
|
const p = parseCfg(BASELINE);
|
|
expect(p.entries.get('g_inactivity')).toBe(1);
|
|
expect(p.entries.get('sv_voice')).toBe(2);
|
|
expect(p.entries.get('sv_maprotation')).toBe(6);
|
|
expect(p.order[0]).toEqual({ kind: 'raw', line: '// GENERAL' });
|
|
expect(p.order[3]).toEqual({ kind: 'raw', line: '' });
|
|
});
|
|
|
|
it('detects CRLF line endings', () => {
|
|
const crlf = 'set a "1"\r\nset b "2"\r\n';
|
|
expect(parseCfg(crlf).eol).toBe('\r\n');
|
|
});
|
|
});
|
|
|
|
describe('parseOverlay', () => {
|
|
it('reads set lines and ignores comments/blanks', () => {
|
|
const overlay = '// disable AFK\nset g_inactivity "0"\n';
|
|
const kv = parseOverlay(overlay);
|
|
expect(kv.size).toBe(1);
|
|
expect(kv.get('g_inactivity')).toBe('set g_inactivity "0"');
|
|
});
|
|
|
|
it('handles bare (unquoted) values', () => {
|
|
const overlay = 'set g_logSync 1';
|
|
const kv = parseOverlay(overlay);
|
|
expect(kv.get('g_logSync')).toBe('set g_logSync 1');
|
|
});
|
|
|
|
it('later duplicate wins within an overlay', () => {
|
|
const overlay = 'set x "1"\nset x "2"';
|
|
expect(parseOverlay(overlay).get('x')).toBe('set x "2"');
|
|
});
|
|
});
|
|
|
|
describe('composeCfg', () => {
|
|
it('replaces matching keys in place', () => {
|
|
const result = composeCfg(BASELINE, [{ id: 1, text: 'set g_inactivity "0"' }]);
|
|
expect(result).toContain('set g_inactivity "0"');
|
|
expect(result).not.toContain('set g_inactivity "190"');
|
|
// Structure preserved: sv_voice should still be at its original spot.
|
|
const lines = result.split('\n');
|
|
expect(lines[1]).toBe('set g_inactivity "0"');
|
|
expect(lines[2]).toBe('set sv_voice "1"');
|
|
});
|
|
|
|
it('appends new keys not present in baseline', () => {
|
|
const result = composeCfg(BASELINE, [{ id: 1, text: 'set new_flag "yes"' }]);
|
|
expect(result).toContain('set new_flag "yes"');
|
|
expect(result).toContain('// --- appended by electric-cherry overlays ---');
|
|
});
|
|
|
|
it('applies multiple overlays deterministically by id', () => {
|
|
const result = composeCfg(BASELINE, [
|
|
{ id: 2, text: 'set g_inactivity "60"' },
|
|
{ id: 1, text: 'set g_inactivity "0"' }
|
|
]);
|
|
// id=2 is later → wins
|
|
expect(result).toContain('set g_inactivity "60"');
|
|
expect(result).not.toContain('set g_inactivity "0"');
|
|
});
|
|
|
|
it('preserves inline comments on the overlay line', () => {
|
|
const result = composeCfg(BASELINE, [
|
|
{ id: 1, text: 'set g_inactivity "0" // AFK disabled' }
|
|
]);
|
|
expect(result).toContain('set g_inactivity "0" // AFK disabled');
|
|
});
|
|
|
|
it('is a no-op when no overlays are given', () => {
|
|
const result = composeCfg(BASELINE, []);
|
|
expect(result.trim()).toBe(BASELINE.trim());
|
|
});
|
|
});
|
|
|
|
describe('automatic map selection', () => {
|
|
it.each(T5_ZOMBIE_MAPS)('selects $friendly without a map overlay', ({ canonical }) => {
|
|
const result = composeServerCfg(BASELINE, [], 't5', canonical);
|
|
expect(result).toContain(`set sv_maprotation "map ${canonical}"`);
|
|
expect(result.match(/^set sv_maprotation /gm)).toHaveLength(1);
|
|
expect(result).toContain('set sv_voice "1"');
|
|
});
|
|
|
|
it('overrides legacy map-choice overlays while keeping other settings', () => {
|
|
const result = composeServerCfg(BASELINE, [{
|
|
id: 5, text: 'set sv_maprotation "map zombie_moon"\nset g_inactivity "0"'
|
|
}], 't5', 'zombie_cosmodrome');
|
|
expect(result).toContain('set sv_maprotation "map zombie_cosmodrome"');
|
|
expect(result).not.toContain('map zombie_moon');
|
|
expect(result).toContain('set g_inactivity "0"');
|
|
});
|
|
|
|
it('appends the rotation when absent and preserves CRLF', () => {
|
|
expect(composeServerCfg('set sv_voice "1"\r\n', [], 't5', 'zombie_moon'))
|
|
.toBe('set sv_voice "1"\r\n// Map selected by Electric Cherry\r\nset sv_maprotation "map zombie_moon"\r\n');
|
|
});
|
|
|
|
it('removes conflicting duplicate assignments regardless of case or command form', () => {
|
|
const result = composeServerCfg([
|
|
'// keep this comment',
|
|
'set sv_maprotation "map zombie_theater"',
|
|
'seta SV_MAPROTATION "map zombie_moon"',
|
|
'sv_mapRotation "map zombie_pentagon"'
|
|
].join('\n'), [], 't5', 'zombie_cod5_sumpf');
|
|
expect(result).toBe('// keep this comment\nset sv_maprotation "map zombie_cod5_sumpf"\n');
|
|
});
|
|
|
|
it.each([
|
|
[null, null], ['t5', null], ['t5', 'custom_map'], ['t6', 'zombie_theater']
|
|
])('preserves existing behavior for unsupported game/map %s/%s', (game, map) => {
|
|
expect(composeServerCfg(BASELINE, [], game, map)).toBe(composeCfg(BASELINE, []));
|
|
});
|
|
});
|