import { describe, expect, it } from 'vitest'; import { inspectConfig, type ConfigLayer } from '../../src/lib/server/fleet/config-inspect.js'; import { composeServerCfg } from '../../src/lib/server/fleet/config-compose.js'; import { changeConfigValue, configWarnings } from '../../src/lib/config-workbench.js'; const base = (text: string): ConfigLayer => ({ text, source: { id: 1, name: 'Base', version: 2, kind: 'baseline' } }); const overlay = (id: number, text: string): ConfigLayer => ({ text, source: { id, name: `Overlay ${id}`, version: 3, kind: 'config' } }); describe('config source inspection', () => { it('traces the baseline and every override in actual ID order', () => { const baseline = base('set g_inactivity "190"\r\nset sv_voice "1"\r\n'); const overlays = [overlay(5, 'set g_inactivity "60"'), overlay(2, 'set g_inactivity "0"')]; const result = inspectConfig(baseline, overlays, 't5', 'zombie_theater'); expect(result.text).toBe(composeServerCfg(baseline.text, overlays.map(o => ({ id: o.source.id!, text: o.text })), 't5', 'zombie_theater')); const setting = result.settings.find(s => s.key === 'g_inactivity')!; expect(setting.value).toBe('"60"'); expect(setting.source.id).toBe(5); expect(setting.history.map(h => h.source.id)).toEqual([1, 2, 5]); expect(result.settings.find(s => s.key === 'sv_voice')?.source.id).toBe(1); }); it('explains duplicates within baselines and within overlays', () => { const result = inspectConfig(base('set x 1\nset x 2'), [overlay(2, 'set x 3\nset x 4')], null, null); expect(result.text).toBe('set x 1\nset x 4\n'); expect(result.settings[0].history.map(h => h.value)).toEqual(['1', '2', '3', '4']); expect(result.settings[0].value).toBe('4'); expect(result.warnings.filter(w => w.includes('also assigned'))).toHaveLength(2); }); it('uses final file order for differently cased keys rather than assuming layer order', () => { const result = inspectConfig(base('set X 1'), [overlay(2, 'set x 2'), overlay(3, 'set X 3')], null, null); expect(result.text).toContain('set X 3'); expect(result.settings).toHaveLength(1); expect(result.settings[0].value).toBe('2'); expect(result.settings[0].source.id).toBe(2); }); it('traces map overrides across set, seta, and bare assignments', () => { const result = inspectConfig(base('set sv_maprotation "old"\nseta SV_MAPROTATION "older"\nsv_maprotation "last"'), [overlay(2, 'set sv_maprotation "overlay"')], 't5', 'zombie_theater'); const setting = result.settings[0]; expect(setting.key).toBe('sv_maprotation'); expect(setting.value).toBe('"map zombie_theater"'); expect(setting.source.kind).toBe('map'); expect(setting.history.map(h => h.value)).toEqual(['"old"', '"overlay"', '"older"', '"last"', '"map zombie_theater"']); expect(result.text.match(/set sv_maprotation/g)).toHaveLength(1); }); it('preserves T6 map variants and does not invent map selection for unknown maps', () => { const t6 = inspectConfig(base('set sv_voice 1'), [], 't6', 'zm_transit', 'town'); expect(t6.settings.find(s => s.key === 'sv_maprotation')?.value).toContain('execgts zm_standard_town.cfg'); const legacy = inspectConfig(base('set sv_maprotation "custom"'), [], 't5', 'unknown'); expect(legacy.settings[0].source.kind).toBe('baseline'); expect(legacy.text).toBe('set sv_maprotation "custom"\n'); }); it('reports ignored overlay commands while retaining raw baseline commands', () => { const result = inspectConfig(base('exec custom.cfg\nset a 1'), [overlay(2, 'seta a 2\nSET a 3\n// note\nset b 4')], null, null); expect(result.warnings.filter(w => w.includes('ignored'))).toHaveLength(2); expect(result.text).toContain('exec custom.cfg'); expect(result.settings.find(s => s.key === 'a')?.value).toBe('1'); expect(result.settings.find(s => s.key === 'b')?.source.id).toBe(2); }); }); describe('config settings editor', () => { it('changes only the selected assignment, preserving CRLF, spacing and raw commands', () => { const text = '// note\r\n set a "1" // original\r\nexec other.cfg\r\nset a 5\n'; expect(changeConfigValue(text, 1, '"2" // changed')).toBe('// note\r\n set a "2" // changed\r\nexec other.cfg\r\nset a 5\n'); }); it('does not silently rewrite unsupported lines and reports missing values', () => { expect(changeConfigValue('seta a 1', 0, '2')).toBe('seta a 1'); expect(configWarnings('set a // missing', true)).toContain('Line 1: a has no value.'); }); });