Files
electric-cherry/tests/unit/autoscaling-dry-run.test.ts
T

56 lines
3.6 KiB
TypeScript

import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { newFixture, type Fixture } from '../helpers/fixture.js';
import { routePermission, roleAllows } from '../../src/lib/server/auth/access.js';
let fixture: Fixture;
beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); });
afterEach(async () => { await fixture.cleanup(); vi.useRealTimers(); });
const settings = { game: 't6', mapSlug: 'town', maximum: 4, mode: 'dry-run' };
it('persists deduplicated dry-run 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');
db().prepare('INSERT INTO server_keys(server_id,pluto_key) VALUES(?,?)').run('t6_zombie_town_3', 'secret');
const preview = scaling.previewScaling(settings);
const saved = scaling.saveScaling(settings, undefined, undefined, preview.token);
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: 'create', server: 't6_zombie_town_3' });
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('already exists');
expect(() => scaling.previewScaling(settings, saved.id, 0)).toThrow('changed');
expect(() => scaling.saveScaling({ ...settings, maximum: 5 }, saved.id, saved.revision, preview.token)).toThrow('preview changed');
expect(() => scaling.saveScaling({ ...settings, mode: 'create-only' }, saved.id, saved.revision, scaling.previewScaling({ ...settings, mode: 'create-only' }, saved.id, saved.revision).token)).toThrow('not available');
});
it('pauses evaluations and prunes expired observations without deleting current history', async () => {
const scaling = await import('../../src/lib/server/autoscaling/index.js');
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 { db } = await import('../../src/lib/server/db/index.js');
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);
});