import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; import { prepareNativeHost } from '../helpers/native-host.js'; import { zombieMaps } from '../../src/lib/map-aliases.js'; let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); }); afterEach(async () => { await fixture.cleanup(); }); const settings = { game: 't6', mapSlug: 'town', maximum: 2, mode: 'dry-run' }; async function setup() { const { db } = await import('../../src/lib/server/db/index.js'); const scaling = await import('../../src/lib/server/autoscaling/index.js'); const key = (name: string, value = `key-${name}`) => db().prepare('INSERT INTO server_keys(server_id,pluto_key) VALUES(?,?)').run(name, value); return { db, scaling, key }; } it.each(['off', 'dry-run', 'create-only', 'create-destroy'])('rejects new %s policies without enough keys through both service and API', async mode => { const { db, scaling, key } = await setup(); key('t6_zombie_town_3'); const input = { ...settings, mode }; const preview = scaling.previewScaling(input); expect(preview.blockers[0]).toContain('1 saved; 1 more required'); expect(() => scaling.saveScaling(input, undefined, undefined, preview.token)).toThrow('Save keys'); const { POST } = await import('../../src/routes/api/autoscaling/+server.js'); const response = await POST({ request: new Request('http://localhost/api/autoscaling', { method: 'POST', body: JSON.stringify({ action: 'save', settings: input, token: preview.token }) }) }); expect(response.status).toBe(400); expect(await response.json()).toMatchObject({ error: expect.stringContaining('Save keys') }); expect(db().prepare('SELECT COUNT(*) AS n FROM autoscaling_policies').get()).toEqual({ n: 0 }); }); it('counts only supported, nonblank identities in the selected map and accepts exact nonconsecutive coverage', async () => { const { scaling, key } = await setup(); for (const name of ['t6_zombie_town_0', 't6_zombie_town_21', 't6_zombie_town_01', 't6_zombie_town', 't6_town_2', 't5_zombie_theater_1', 't6_zombie_farm_1']) key(name); key('t6_zombie_town_2', ' '); key('t6_zombie_town_3'); expect(scaling.previewScaling(settings).snapshot.keyedNames).toEqual(['t6_zombie_town_3']); key('t6_zombie_town_8'); const preview = scaling.previewScaling(settings); expect(preview.blockers).toEqual([]); expect(scaling.saveScaling(settings, undefined, undefined, preview.token).maximum).toBe(2); }); it('requires coverage on every game map rather than pooling excess keys from another map', async () => { const { scaling, key } = await setup(); for (const map of zombieMaps('t6')) for (const n of [3, 8]) if (map.slug !== 'town' || n === 3) key(`t6_zombie_${map.slug}_${n}`); key('t6_zombie_farm_9'); const input = { ...settings, mapSlug: '*' }; const preview = scaling.previewScaling(input); expect(preview.blockers).toEqual([expect.stringContaining('Town Survival')]); expect(() => scaling.saveScaling(input, undefined, undefined, preview.token)).toThrow('Town Survival'); key('t6_zombie_town_8'); expect(scaling.saveScaling(input, undefined, undefined, scaling.previewScaling(input).token).mapSlug).toBe('*'); }); it('rechecks key deletion after preview and rejects increases beyond coverage', async () => { const { db, scaling, key } = await setup(); key('t6_zombie_town_3'); key('t6_zombie_town_8'); const preview = scaling.previewScaling(settings); db().prepare("DELETE FROM server_keys WHERE server_id='t6_zombie_town_8'").run(); expect(() => scaling.saveScaling(settings, undefined, undefined, preview.token)).toThrow('preview changed'); expect(() => scaling.saveScaling(settings, undefined, undefined, scaling.previewScaling(settings).token)).toThrow('Save keys'); key('t6_zombie_town_8'); const saved = scaling.saveScaling(settings, undefined, undefined, scaling.previewScaling(settings).token); const higher = { ...settings, maximum: 3 }; expect(() => scaling.saveScaling(higher, saved.id, saved.revision, scaling.previewScaling(higher, saved.id, saved.revision).token)).toThrow('Save keys'); expect(scaling.getScalingPolicy(saved.id)?.maximum).toBe(2); }); it('allows an existing live policy to pause and withdraw pending work after all keys are removed', async () => { await prepareNativeHost(); const { db, scaling, key } = await setup(); key('t6_zombie_town_3'); key('t6_zombie_town_8'); const live = { ...settings, mode: 'create-only' }; const saved = scaling.saveScaling(live, undefined, undefined, scaling.previewScaling(live).token); await scaling.runAutoscaling(); db().prepare('DELETE FROM server_keys').run(); const off = { ...settings, mode: 'off' }; const preview = scaling.previewScaling(off, saved.id, saved.revision); expect(preview.blockers).toEqual([]); expect(scaling.saveScaling(off, saved.id, saved.revision, preview.token).mode).toBe('off'); expect(db().prepare("SELECT state FROM server_deployments WHERE action='create'").get()).toEqual({ state: 'cancelled' }); });