import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; import { prepareNativeHost } from '../helpers/native-host.js'; let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); }); afterEach(async () => { await fixture.cleanup(); }); const settings = { game: 't6', mapSlug: 'town', maximum: 1, mode: 'create-destroy' }; async function setup(mode = 'create-destroy') { if (mode.startsWith('create')) await prepareNativeHost(); const { db } = await import('../../src/lib/server/db/index.js'); const scaling = await import('../../src/lib/server/autoscaling/index.js'); db().prepare("INSERT INTO server_keys(id,game,pluto_key) VALUES('key-1','t6','test-key')").run(); const input = { ...settings, mode }; const saved = scaling.saveScaling(input, undefined, undefined, scaling.previewScaling(input).token); return { db, scaling, saved, input }; } it('deletes policy evidence atomically while preserving lobbies, keys, and unrelated policies', async () => { const { db, scaling, saved } = await setup(); db().prepare("INSERT INTO servers(name,game,map_slug,map) VALUES('t6_zombie_town_1','t6','town','zm_transit')").run(); await scaling.runAutoscaling(); db().prepare("INSERT INTO server_keys(id,game,pluto_key) VALUES('other-key','t5','other-key')").run(); const otherSettings = { ...settings, game: 't5', mapSlug: 'theater', mode: 'off' }; const other = scaling.saveScaling(otherSettings, undefined, undefined, scaling.previewScaling(otherSettings).token); for (const table of ['autoscaling_decisions', 'autoscaling_members', 'autoscaling_samples']) expect(db().prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE policy_id=?`).get(saved.id)).toEqual({ n: 1 }); scaling.deleteScaling(saved.id, saved.revision); expect(scaling.getScalingPolicy(saved.id)).toBeUndefined(); expect(scaling.listScalingPolicies().map(p => p.id)).toEqual([other.id]); for (const table of ['autoscaling_decisions', 'autoscaling_members', 'autoscaling_samples', 'autoscaling_authorizations']) expect(db().prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE policy_id=?`).get(saved.id)).toEqual({ n: 0 }); expect(db().prepare('SELECT name FROM servers').all()).toEqual([{ name: 't6_zombie_town_1' }]); expect(db().prepare('SELECT COUNT(*) AS n FROM server_keys').get()).toEqual({ n: 2 }); expect(db().prepare('PRAGMA foreign_key_check').all()).toEqual([]); expect(scaling.saveScaling({ ...settings, mode: 'off' }, undefined, undefined, scaling.previewScaling({ ...settings, mode: 'off' }).token).id).not.toBe(saved.id); }); it.each(['off', 'create-destroy'])('deletes a settled %s policy even after key loss and releases ownership', async mode => { const { db, scaling, saved } = await setup(mode); db().prepare('DELETE FROM server_keys').run(); scaling.deleteScaling(saved.id, saved.revision); const { scalingOwner } = await import('../../src/lib/server/autoscaling/ownership.js'); expect(scalingOwner('t6', 'town')).toBeNull(); expect(await scaling.runAutoscaling()).toBe(false); expect(db().prepare('SELECT COUNT(*) AS n FROM server_deployments').get()).toEqual({ n: 0 }); }); it('rejects stale revisions and preserves the latest settings', async () => { const { scaling, saved } = await setup(); const off = { ...settings, mode: 'off' }; const edited = scaling.saveScaling(off, saved.id, saved.revision, scaling.previewScaling(off, saved.id, saved.revision).token); expect(() => scaling.deleteScaling(saved.id, saved.revision)).toThrow('changed'); expect(scaling.getScalingPolicy(saved.id)).toEqual(edited); expect(() => scaling.deleteScaling('missing', 1)).toThrow('not found'); }); it('blocks deletion with pending intent, then keeps Provisioning history after pause and settlement', async () => { const { db, scaling, saved } = await setup('create-destroy'); await scaling.runAutoscaling(); const { operation_id: id } = db().prepare('SELECT operation_id FROM autoscaling_decisions WHERE resolved=0').get() as { operation_id: string }; expect(() => scaling.deleteScaling(saved.id, saved.revision)).toThrow('outstanding Provisioning'); expect(scaling.getScalingPolicy(saved.id)?.mode).toBe('create-destroy'); const off = { ...settings, mode: 'off' }; const paused = scaling.saveScaling(off, saved.id, saved.revision, scaling.previewScaling(off, saved.id, saved.revision).token); scaling.deleteScaling(saved.id, paused.revision); const { getDeployment } = await import('../../src/lib/server/provisioning/history.js'); expect(getDeployment(id)).toMatchObject({ deployment: { state: 'cancelled' }, sources: [expect.objectContaining({ kind: 'autoscaling', source_id: saved.id, label: 'Deleted autoscaling policy', href: null })] }); }); it('validates delete requests and returns conflict, not-found, and success statuses', async () => { const { scaling, saved } = await setup(); const { POST } = await import('../../src/routes/api/autoscaling/+server.js'); const request = (body: object) => POST({ request: new Request('http://localhost/api/autoscaling', { method: 'POST', body: JSON.stringify({ action: 'delete', ...body }) }) }); for (const body of [{}, { id: saved.id }, { id: saved.id, revision: '1' }, { id: saved.id, revision: 0 }]) expect((await request(body)).status).toBe(400); expect((await request({ id: saved.id, revision: saved.revision + 1 })).status).toBe(409); expect((await request({ id: saved.id, revision: saved.revision })).status).toBe(200); expect(scaling.getScalingPolicy(saved.id)).toBeUndefined(); expect((await request({ id: saved.id, revision: saved.revision })).status).toBe(404); });