import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; import { prepareNativeHost } from '../helpers/native-host.js'; import { routePermission, roleAllows } from '../../src/lib/server/auth/access.js'; let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); await prepareNativeHost(); const { db } = await import('../../src/lib/server/db/index.js'); for (const n of [3, 8, 9, 10]) db().prepare("INSERT INTO server_keys(id,game,pluto_key) VALUES(?,'t6',?)").run(`key-${n}`, `secret-${n}`); }); afterEach(async () => { await fixture.cleanup(); vi.useRealTimers(); }); const settings = { game: 't6', mapSlug: 'town', maximum: 4, mode: 'create-destroy' }; it('persists deduplicated paused decisions across restart without provisioning or reservations', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const scaling = await import('../../src/lib/server/autoscaling/index.js'); const preview = scaling.previewScaling(settings); const saved = scaling.saveScaling(settings, undefined, undefined, preview.token); scaling.setScalingGlobalPause(true); await scaling.runAutoscaling(); await scaling.runAutoscaling(); closeDb(); await scaling.runAutoscaling(); const view = scaling.scalingReport().policies[0]; expect(view.policy.id).toBe(saved.id); expect(view.history).toHaveLength(1); expect(view.decision).toMatchObject({ action: 'wait', server: null }); expect(JSON.stringify(view)).not.toContain('secret'); for (const table of ['server_deployments', 'native_creation_intents', 'native_instances']) expect(db().prepare(`SELECT COUNT(*) AS n FROM ${table}`).get()).toEqual({ n: 0 }); }); it('rejects duplicate policies, stale edits and changed previews', async () => { const scaling = await import('../../src/lib/server/autoscaling/index.js'); const preview = scaling.previewScaling(settings); const saved = scaling.saveScaling(settings, undefined, undefined, preview.token); expect(() => scaling.saveScaling(settings, undefined, undefined, preview.token)).toThrow('overlapping policy'); expect(() => scaling.previewScaling(settings, saved.id, 0)).toThrow('changed'); expect(() => scaling.saveScaling({ ...settings, maximum: 5 }, saved.id, saved.revision, preview.token)).toThrow('preview changed'); const { db } = await import('../../src/lib/server/db/index.js'); const beforeKeyLoss = scaling.previewScaling(settings, saved.id, saved.revision); db().prepare("DELETE FROM server_keys WHERE id='key-10'").run(); expect(() => scaling.saveScaling(settings, saved.id, saved.revision, beforeKeyLoss.token)).toThrow('preview changed'); expect(scaling.saveScaling(settings, saved.id, saved.revision, scaling.previewScaling(settings, saved.id, saved.revision).token).maximum).toBe(4); }); it('pauses evaluations and prunes expired observations without deleting current history', async () => { const scaling = await import('../../src/lib/server/autoscaling/index.js'); const { db } = await import('../../src/lib/server/db/index.js'); db().prepare('DELETE FROM server_keys').run(); scaling.saveScaling(settings, undefined, undefined, scaling.previewScaling(settings).token); await scaling.runAutoscaling(); scaling.setScalingGlobalPause(true); await scaling.runAutoscaling(); expect(scaling.scalingReport().policies[0].decision.reason).toContain('globally paused'); const id = scaling.scalingReport().policies[0].history[0].id; db().prepare('UPDATE autoscaling_decisions SET last_seen_at=0 WHERE id!=?').run(id); scaling.pruneAutoscaling(); expect(scaling.scalingReport().policies[0].history).toHaveLength(1); }); it('uses fleet authorization for page, reads, and mutations', () => { expect(routePermission('/autoscaling', 'GET')).toBe('viewer'); expect(routePermission('/api/autoscaling', 'GET')).toBe('viewer'); expect(roleAllows('viewer', routePermission('/api/autoscaling', 'POST'))).toBe(false); expect(roleAllows('operator', routePermission('/api/autoscaling', 'POST'))).toBe(true); }); it.each(['dry-run', 'create-only'])('rejects removed %s mode through the API', async mode => { const { POST } = await import('../../src/routes/api/autoscaling/+server.js'); for (const action of ['preview', 'save']) { const response = await POST({ request: new Request('http://localhost/api/autoscaling', { method: 'POST', body: JSON.stringify({ action, settings: { ...settings, mode } }) }) }); expect(response.status).toBe(400); expect(await response.json()).toMatchObject({ error: 'Choose a valid autoscaling mode.' }); } }); it.each(['dry-run', 'create-only'])('turns saved %s policies off once on upgrade', async mode => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const scaling = await import('../../src/lib/server/autoscaling/index.js'); const saved = scaling.saveScaling(settings, undefined, undefined, scaling.previewScaling(settings).token); const otherSettings = { ...settings, mapSlug: 'farm' }; const other = scaling.saveScaling(otherSettings, undefined, undefined, scaling.previewScaling(otherSettings).token); db().prepare('UPDATE autoscaling_policies SET mode=?,settings=json_set(settings,\'$.mode\',?) WHERE id=?').run(mode, mode, saved.id); closeDb(); const migrated = scaling.getScalingPolicy(saved.id)!; expect(migrated).toMatchObject({ mode: 'off', revision: saved.revision + 1, maximum: saved.maximum, pausedReason: expect.stringContaining('mode was removed') }); expect(db().prepare('SELECT owns_map,json_extract(settings,\'$.mode\') AS mode FROM autoscaling_policies WHERE id=?').get(saved.id)) .toEqual({ owns_map: 0, mode: 'off' }); expect(scaling.getScalingPolicy(other.id)).toEqual(other); closeDb(); expect(scaling.getScalingPolicy(saved.id)).toEqual(migrated); const resumed = scaling.saveScaling(settings, migrated.id, migrated.revision, scaling.previewScaling(settings, migrated.id, migrated.revision).token); expect(resumed).toMatchObject({ mode: 'create-destroy', pausedReason: null }); }); it.each([false, true])('reconciles removed-mode creation while preserving submitted work: %s', async submitted => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const scaling = await import('../../src/lib/server/autoscaling/index.js'); const saved = scaling.saveScaling(settings, undefined, undefined, scaling.previewScaling(settings).token); await scaling.runAutoscaling(); const { operation_id: id } = db().prepare('SELECT operation_id FROM autoscaling_decisions WHERE resolved=0').get() as { operation_id: string }; db().prepare("UPDATE autoscaling_policies SET mode='create-only',settings=json_set(settings,'$.mode','create-only') WHERE id=?").run(saved.id); if (submitted) db().prepare('UPDATE server_deployments SET submitted_at=? WHERE id=?').run(new Date().toISOString(), id); closeDb(); expect(scaling.getScalingPolicy(saved.id)?.mode).toBe('off'); expect(db().prepare('SELECT owns_map FROM autoscaling_policies WHERE id=?').get(saved.id)).toEqual({ owns_map: 1 }); expect(await scaling.runAutoscaling()).toBe(false); expect(db().prepare('SELECT state FROM server_deployments WHERE id=?').get(id)).toEqual({ state: submitted ? 'queued' : 'cancelled' }); expect(db().prepare('SELECT owns_map FROM autoscaling_policies WHERE id=?').get(saved.id)).toEqual({ owns_map: submitted ? 1 : 0 }); expect(db().prepare('SELECT COUNT(*) AS n FROM server_key_leases').get()).toEqual({ n: submitted ? 1 : 0 }); expect(db().prepare("SELECT COUNT(*) AS n FROM server_deployments WHERE action='create'").get()).toEqual({ n: 1 }); });