import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; let fixture: Fixture; const server = 't5_zombie_theater_1'; beforeEach(async () => { vi.resetModules(); fixture = await newFixture([server]); const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js'); await seedServerRegistry(); }); afterEach(async () => { await fixture.cleanup(); }); async function seed() { const scripts = await import('../../src/lib/server/fleet/scripts.js'); const { db } = await import('../../src/lib/server/db/index.js'); const baseline = scripts.createScript({ name: 'Base', kind: 'baseline', scopeType: 'game', scopeValue: 't5', content: Buffer.from('set g_inactivity 190') }); db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(baseline.version.id, baseline.script.id); return { ...scripts, baseline, db }; } describe('config workbench API', () => { it('previews a draft without writing versions or changing the promoted config', async () => { const { createScript, baseline, listVersions, getScript } = await seed(); const overlay = createScript({ name: 'AFK', kind: 'config', scopeType: 'map', scopeValue: 'zombie_theater', content: Buffer.from('set g_inactivity 0') }); const { POST, GET } = await import('../../src/routes/api/configs/inspect/+server.js'); const before = await (await GET({ url: new URL(`http://localhost/api/configs/inspect?server=${server}`) })).json(); expect(before.inspection.settings.find((s: { key: string }) => s.key === 'g_inactivity').value).toBe('190'); const response = await POST({ request: new Request('http://localhost', { method: 'POST', body: JSON.stringify({ server, artifactId: overlay.script.id, text: 'set g_inactivity 60' }) }) }); expect(response.status).toBe(200); const result = await response.json(); expect(result.inspection.settings.find((s: { key: string }) => s.key === 'g_inactivity')).toMatchObject({ value: '60', source: { id: overlay.script.id, draft: true } }); expect(result.promoted.settings.find((s: { key: string }) => s.key === 'g_inactivity').value).toBe('190'); expect(listVersions(overlay.script.id)).toHaveLength(1); expect(getScript(baseline.script.id)?.current_version_id).toBe(baseline.version.id); const { getPromotedServerConfig } = await import('../../src/lib/server/fleet/server-config.js'); expect(before.inspection.text).toBe(getPromotedServerConfig('t5', 'zombie_theater')!.toString('utf8')); }); it('uses promoted versions even when a newer untested version exists', async () => { const { baseline, uploadNewVersion } = await seed(); uploadNewVersion(baseline.script.id, Buffer.from('set g_inactivity 0')); const { GET } = await import('../../src/routes/api/configs/inspect/+server.js'); const result = await (await GET({ url: new URL(`http://localhost/?server=${server}`) })).json(); expect(result.inspection.settings.find((s: { key: string }) => s.key === 'g_inactivity').value).toBe('190'); expect(result.inspection.sources[0].version).toBe(1); }); it('handles missing baselines and allows previewing an unpromoted baseline draft', async () => { const { createScript } = await import('../../src/lib/server/fleet/scripts.js'); const { GET, POST } = await import('../../src/routes/api/configs/inspect/+server.js'); const missing = await (await GET({ url: new URL(`http://localhost/?server=${server}`) })).json(); expect(missing.inspection.reason).toMatch(/No promoted baseline/); const baseline = createScript({ name: 'Base', kind: 'baseline', scopeType: 'game', scopeValue: 't5', content: Buffer.from('set a 1') }); const response = await POST({ request: new Request('http://localhost', { method: 'POST', body: JSON.stringify({ server, artifactId: baseline.script.id, text: 'set a 2' }) }) }); expect((await response.json()).inspection.settings.find((s: { key: string }) => s.key === 'a').value).toBe('2'); }); it('rejects mismatched scopes and malformed preview requests', async () => { const { createScript } = await seed(); const config = createScript({ name: 'T6', kind: 'config', scopeType: 'game', scopeValue: 't6', content: Buffer.from('set a 1') }); const { POST } = await import('../../src/routes/api/configs/inspect/+server.js'); const response = await POST({ request: new Request('http://localhost', { method: 'POST', body: JSON.stringify({ server, artifactId: config.script.id, text: 'set a 2' }) }) }); expect(response.status).toBe(400); expect((await response.json()).error).toMatch(/does not apply/); expect((await POST({ request: new Request('http://localhost', { method: 'POST', body: '{' }) })).status).toBe(400); }); it('reads only versions owned by the requested config and rejects other artifact kinds', async () => { const { baseline, createScript } = await seed(); const other = createScript({ name: 'Other', kind: 'config', scopeType: 'game', scopeValue: 't5', content: Buffer.from('set b 1') }); const script = createScript({ name: 'Script', targetRelPath: 'test.gsc', content: Buffer.from('code') }); const { GET } = await import('../../src/routes/api/configs/[id]/+server.js'); const read = (id: number | string, version?: number) => GET({ params: { id: String(id) }, url: new URL(`http://localhost/${version ? `?version=${version}` : ''}`) }); expect((await (await read(baseline.script.id)).json()).text).toBe('set g_inactivity 190'); expect((await read(baseline.script.id, other.version.id)).status).toBe(404); expect((await read(script.script.id)).status).toBe(404); expect((await read('1oops')).status).toBe(400); }); it('saves editor content as an untested version without promoting it', async () => { const { baseline, getScript, getLatestVersion } = await seed(); const { POST } = await import('../../src/routes/api/scripts/[id]/versions/+server.js'); const form = new FormData(); form.append('content', new Blob(['set g_inactivity 0']), 'config.cfg'); form.append('notes', 'Disable timeout'); const response = await POST({ params: { id: String(baseline.script.id) }, request: new Request('http://localhost', { method: 'POST', body: form }) }); expect(response.status).toBe(200); expect(getLatestVersion(baseline.script.id)).toMatchObject({ version: 2, state: 'untested', notes: 'Disable timeout' }); expect(getScript(baseline.script.id)?.current_version_id).toBe(baseline.version.id); }); it('blocks config saves during an active beta', async () => { const { baseline, db, listVersions } = await seed(); const beta = db().prepare("INSERT INTO betas (script_id, script_version_id, trigger_kind, state) VALUES (?, ?, 'manual', 'running')").run(baseline.script.id, baseline.version.id); db().prepare('INSERT INTO beta_scripts (beta_id, script_id, script_version_id) VALUES (?, ?, ?)').run(beta.lastInsertRowid, baseline.script.id, baseline.version.id); const { POST } = await import('../../src/routes/api/scripts/[id]/versions/+server.js'); const form = new FormData(); form.append('content', new Blob(['set g_inactivity 0']), 'config.cfg'); const response = await POST({ params: { id: String(baseline.script.id) }, request: new Request('http://localhost', { method: 'POST', body: form }) }); expect(response.status).toBe(409); expect(listVersions(baseline.script.id)).toHaveLength(1); }); });