Conform blueprint launches to desired fleet counts
This commit is contained in:
@@ -102,6 +102,77 @@ describe('multi-server blueprints', () => {
|
||||
});
|
||||
|
||||
describe('atomic collection launches', () => {
|
||||
it('fills the target shortfall and counts registered/in-flight identities only once', async () => {
|
||||
const { createServer } = await import('../../src/lib/server/fleet/create.js');
|
||||
for (let i = 0; i < 2; i++) expect((await createServer({ gameMode: 'zombie', mapSlug: 'theater', plutoKey: 'existing-key' })).ok).toBe(true);
|
||||
const { saveBlueprint } = await import('../../src/lib/server/fleet/launch-recipes.js');
|
||||
const { previewCollection, queueCollection, getCollection } = await import('../../src/lib/server/fleet/collections.js');
|
||||
const blueprint = saveBlueprint({ name: 'Friday targets', entries: [
|
||||
{ game: 't5', mapSlug: 'theater', count: 4 }, { game: 't5', mapSlug: 'factory', count: 2 }
|
||||
] });
|
||||
const preview = previewCollection(blueprint.id);
|
||||
expect(preview.entries.map(({ count, existing, missing }) => ({ count, existing, missing }))).toEqual([
|
||||
{ count: 4, existing: 2, missing: 2 }, { count: 2, existing: 0, missing: 2 }
|
||||
]);
|
||||
expect(preview.servers.map(server => server.name)).toEqual(['t5_zombie_theater_3', 't5_zombie_theater_4', 't5_zombie_factory_1', 't5_zombie_factory_2']);
|
||||
const queued = queueCollection({ previewId: preview.id, keys: keysFor(preview) });
|
||||
expect(getCollection(queued.collectionId!)?.collection.total).toBe(4);
|
||||
expect(previewCollection(blueprint.id).servers).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps excess and unrelated servers, including stopped servers, without requiring launch settings for a no-op', async () => {
|
||||
const { createServer } = await import('../../src/lib/server/fleet/create.js');
|
||||
for (let i = 0; i < 2; i++) await createServer({ gameMode: 'zombie', mapSlug: 'theater', plutoKey: 'existing-key' });
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
db().prepare("UPDATE servers SET container_state = 'stopped'").run();
|
||||
const { saveBlueprint } = await import('../../src/lib/server/fleet/launch-recipes.js');
|
||||
const { previewCollection, queueCollection } = await import('../../src/lib/server/fleet/collections.js');
|
||||
const { config } = await import('../../src/lib/server/config.js');
|
||||
// Fixture-only configuration changes simulate missing integrations after creation.
|
||||
Object.assign(config.rcon, { password: '' });
|
||||
Object.assign(config.komodo, { targetServer: '' });
|
||||
const blueprint = saveBlueprint({ name: 'One Kino', entries: [{ game: 't5', mapSlug: 'theater', count: 1 }] });
|
||||
const preview = previewCollection(blueprint.id);
|
||||
expect(preview.entries[0]).toMatchObject({ existing: 2, missing: 0 });
|
||||
expect(preview).toMatchObject({ ready: true, blockers: [], servers: [] });
|
||||
expect(queueCollection({ previewId: preview.id })).toEqual({ ok: true, collectionId: null });
|
||||
expect(db().prepare('SELECT * FROM server_launch_collections').all()).toEqual([]);
|
||||
expect(db().prepare('SELECT * FROM servers').all()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('rechecks shrinking deficits at submission and rejects an older additive preview', async () => {
|
||||
const { preview, blueprint } = await previewLineup();
|
||||
const { queueCreateServer } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { queueCollection, previewCollection } = await import('../../src/lib/server/fleet/collections.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
queueCreateServer({ gameMode: 'zombie', mapSlug: 'theater', plutoKey: 'existing-key' });
|
||||
expect(() => queueCollection({ previewId: preview.id, keys: keysFor(preview) })).toThrow(/changed/);
|
||||
expect(db().prepare('SELECT * FROM server_creation_jobs').all()).toHaveLength(1);
|
||||
const fresh = previewCollection(blueprint.id);
|
||||
expect(fresh.servers).toHaveLength(4);
|
||||
const extra = [...fresh.servers, { ...fresh.servers[0], name: 't5_zombie_theater_20', port: 28980 }];
|
||||
db().prepare('UPDATE server_collection_previews SET servers = ? WHERE id = ?').run(JSON.stringify(extra), fresh.id);
|
||||
expect(() => queueCollection({ previewId: fresh.id, keys: keysFor(fresh) })).toThrow(/changed/);
|
||||
});
|
||||
|
||||
it('matches exact T6 variants and excludes failed creations and pending destructions', async () => {
|
||||
const { createServer, queueCreateServer, destroyServer } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { finishDeployment } = await import('../../src/lib/server/fleet/deployments.js');
|
||||
const { saveBlueprint } = await import('../../src/lib/server/fleet/launch-recipes.js');
|
||||
const { previewCollection } = await import('../../src/lib/server/fleet/collections.js');
|
||||
const farm = await createServer({ game: 't6', gameMode: 'zombie', mapSlug: 'farm', plutoKey: 'farm-key' });
|
||||
const town = await createServer({ game: 't6', gameMode: 'zombie', mapSlug: 'town', plutoKey: 'town-key' });
|
||||
if (!farm.ok || !town.ok) throw new Error('Fixture creation failed');
|
||||
destroyServer(town.name);
|
||||
const failed = queueCreateServer({ game: 't6', gameMode: 'zombie', mapSlug: 'town', plutoKey: 'failed-key' });
|
||||
if (!failed.ok) throw new Error(failed.message);
|
||||
finishDeployment(failed.deploymentId, { ok: false, message: 'Fixture failure' });
|
||||
const blueprint = saveBlueprint({ name: 'Town target', entries: [{ game: 't6', mapSlug: 'town', count: 1 }] });
|
||||
const preview = previewCollection(blueprint.id);
|
||||
expect(preview.entries[0]).toMatchObject({ existing: 0, missing: 1 });
|
||||
expect(preview.servers[0].name).toBe('t6_zombie_town_2');
|
||||
});
|
||||
|
||||
it('queues all five servers, persists one group, and completes each independently', async () => {
|
||||
const { preview } = await previewLineup();
|
||||
const { queueCollection, getCollection, listCollections } = await import('../../src/lib/server/fleet/collections.js');
|
||||
@@ -111,10 +182,10 @@ describe('atomic collection launches', () => {
|
||||
const { komodoCreateDeployment } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
const queued = queueCollection({ previewId: preview.id, keys: keysFor(preview) });
|
||||
expect(komodoCreateDeployment).not.toHaveBeenCalled();
|
||||
expect(getCollection(queued.collectionId)?.collection).toMatchObject({ total: 5, active: 5, done: 0, failed: 0 });
|
||||
expect(getCollection(queued.collectionId!)?.collection).toMatchObject({ total: 5, active: 5, done: 0, failed: 0 });
|
||||
closeDb();
|
||||
await processCreations();
|
||||
const detail = getCollection(queued.collectionId)!;
|
||||
const detail = getCollection(queued.collectionId!)!;
|
||||
expect(detail.collection).toMatchObject({ total: 5, active: 0, done: 5, failed: 0 });
|
||||
expect(listCollections()).toHaveLength(1);
|
||||
expect(komodoCreateDeployment).toHaveBeenCalledTimes(5);
|
||||
@@ -167,11 +238,13 @@ describe('atomic collection launches', () => {
|
||||
db().prepare("UPDATE server_collection_previews SET expires_at = '2000-01-01T00:00:00Z' WHERE id = ?").run(ready.id);
|
||||
closeDb();
|
||||
expect(queueCollection({ previewId: ready.id })).toEqual(queued);
|
||||
expect(getCollection(queued.collectionId)?.servers).toHaveLength(5);
|
||||
expect(getCollection(queued.collectionId!)?.servers).toHaveLength(5);
|
||||
expect(db().prepare('SELECT * FROM server_creation_jobs').all()).toHaveLength(5);
|
||||
const next = previewCollection(blueprint.id);
|
||||
expect(next.servers.map(server => server.name)).toEqual(['t5_zombie_theater_3', 't5_zombie_theater_4', 't5_zombie_moon_2', 't6_zombie_town_3', 't6_zombie_town_4']);
|
||||
expect(next.servers.map(server => server.port)).toEqual([28965, 28966, 28967, 28968, 28969]);
|
||||
expect(next.servers).toEqual([]);
|
||||
expect(next.entries.map(entry => entry.existing)).toEqual([2, 1, 2]);
|
||||
expect(queueCollection({ previewId: next.id })).toEqual({ ok: true, collectionId: null });
|
||||
expect(db().prepare('SELECT * FROM server_launch_collections').all()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('keeps the reviewed lineup and bytes after blueprint edits, deletion, and promotion', async () => {
|
||||
@@ -189,7 +262,7 @@ describe('atomic collection launches', () => {
|
||||
deleteBlueprint(blueprint.id);
|
||||
const queued = queueCollection({ previewId: preview.id, keys: keysFor(preview) });
|
||||
await processCreations();
|
||||
expect(getCollection(queued.collectionId)?.collection).toMatchObject({ name: 'Friday Zombies', total: 5, done: 5 });
|
||||
expect(getCollection(queued.collectionId!)?.collection).toMatchObject({ name: 'Friday Zombies', total: 5, done: 5 });
|
||||
for (const name of ['t5_zombie_theater_1', 't5_zombie_theater_2', 't5_zombie_moon_1']) {
|
||||
expect(await readFile(join(fixture.t5, name, 'runtime/plutonium/storage/t5/raw/scripts/sp/perks.gsc'), 'utf8')).toBe('original');
|
||||
}
|
||||
@@ -203,14 +276,20 @@ describe('atomic collection launches', () => {
|
||||
vi.mocked(komodoCreateDeployment).mockResolvedValueOnce({ ok: false, kind: 'http-error', message: 'Host rejected this container' });
|
||||
const queued = queueCollection({ previewId: preview.id, keys: keysFor(preview) });
|
||||
await processCreations();
|
||||
const detail = getCollection(queued.collectionId)!;
|
||||
const detail = getCollection(queued.collectionId!)!;
|
||||
expect(detail.collection).toMatchObject({ total: 5, active: 0, done: 4, failed: 1 });
|
||||
expect(detail.servers[0]).toMatchObject({ state: 'failed', error: expect.stringContaining('Host rejected') });
|
||||
expect(detail.servers.slice(1).every(server => server.state === 'done')).toBe(true);
|
||||
});
|
||||
|
||||
it('validates collection API payloads and rejects an unused expired preview', async () => {
|
||||
const { preview } = await previewLineup();
|
||||
const { preview, blueprint } = await previewLineup();
|
||||
const { POST: singlePreview } = await import('../../src/routes/api/deployments/preview/+server.js');
|
||||
const oldEndpoint = await singlePreview({ request: new Request('http://localhost/api/deployments/preview', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ blueprintId: blueprint.id })
|
||||
}) });
|
||||
expect(oldEndpoint.status).toBe(400);
|
||||
expect((await oldEndpoint.json()).error).toContain('target counts');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { queueCollection } = await import('../../src/lib/server/fleet/collections.js');
|
||||
const { POST } = await import('../../src/routes/api/deployment-collections/+server.js');
|
||||
|
||||
Reference in New Issue
Block a user