Files
electric-cherry/tests/unit/collection-launch-performance.test.ts
2026-10-09 17:15:15 -04:00

206 lines
8.7 KiB
TypeScript

import { createHash } from 'node:crypto';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { newFixture, type Fixture } from '../helpers/fixture.js';
vi.mock('../../src/lib/server/pipeline/worker.js', () => ({
kick: vi.fn(async () => {}),
kickProvisioning: vi.fn(async () => {})
}));
let fixture: Fixture;
let hostId: string;
let libraryId: string;
let manifest: string;
const image = 'example.test/plutainer:test';
const serverCount = 100;
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
fixture = await newFixture([]);
vi.stubEnv('GAME_FILES_ROOT', join(fixture.root, 'libraries'));
vi.stubEnv('PLUTAINER_IMAGE', image);
vi.stubEnv('RCON_PASSWORD', 'test-password');
const { createHost, createEnrollment, enrollHost, heartbeat } =
await import('../../src/lib/server/hosts/index.js');
hostId = createHost({ name: 'Large native fleet', portMin: 28960, portMax: 29059 }).id;
const inventory = {
cpuCount: 256,
cpuUsagePercent: 5,
memoryTotal: 512 * 1024 ** 3,
memoryAvailable: 500 * 1024 ** 3,
protocolVersion: 1,
dockerRunning: true,
directoriesWritable: true,
storageAvailable: 1_000_000_000_000,
preparedRuntimeImages: [image]
};
enrollHost(createEnrollment(hostId).token, inventory);
const { createLibraryImport, updateHostLibraryCaches } =
await import('../../src/lib/server/game-files/index.js');
libraryId = createLibraryImport({ hostId, game: 't5' }).id;
const fileCount = 20_000;
const fileSize = 1024 ** 2;
const sha256 = createHash('sha256').update('fixture game file').digest('hex');
manifest = JSON.stringify(
Array.from({ length: fileCount }, (_, index) => ({
path: `zone/base-${String(index).padStart(5, '0')}.ff`,
size: fileSize,
sha256
}))
);
const manifestSha256 = createHash('sha256').update(manifest).digest('hex');
const { db } = await import('../../src/lib/server/db/index.js');
// Seed a published manifest without uploading gigabytes of fixture files.
db().transaction(() => {
db()
.prepare(
`UPDATE game_file_versions SET state='ready',manifest=?,manifest_sha256=?,size_bytes=?,published_at=? WHERE id=?`
)
.run(
manifest,
manifestSha256,
fileCount * fileSize,
new Date().toISOString(),
libraryId
);
db()
.prepare(
`INSERT INTO game_file_entries(version_id,path,size,sha256)
SELECT ?,json_extract(value,'$.path'),json_extract(value,'$.size'),json_extract(value,'$.sha256') FROM json_each(?)`
)
.run(libraryId, manifest);
})();
updateHostLibraryCaches(hostId, [
{ libraryVersionId: libraryId, status: 'ready', manifestSha256 }
]);
heartbeat(hostId, { ...inventory, libraries: [{ id: libraryId, state: 'ready' }] });
});
afterEach(async () => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
await fixture.cleanup();
vi.unstubAllEnvs();
});
describe('large native collection admission', () => {
it('previews and queues every lobby without deserializing the game-file manifest', async () => {
const { saveBlueprint } = await import('../../src/lib/server/fleet/launch-recipes.js');
const { previewCollection, queueCollection, getCollection } =
await import('../../src/lib/server/fleet/collections.js');
const { db } = await import('../../src/lib/server/db/index.js');
const blueprint = saveBlueprint({
name: 'One hundred lobbies',
entries: [
'theater',
'cosmodrome',
'coast',
'temple',
'moon',
'pentagon',
'prototype'
].map((mapSlug, index) => ({ game: 't5', mapSlug, count: index === 6 ? 10 : 15 }))
});
const parse = JSON.parse;
let manifestParses = 0;
const observeParse: typeof JSON.parse = (value, reviver) => {
if (value === manifest) manifestParses++;
return parse(value, reviver);
};
// A mock records all return values and would retain every parsed manifest.
vi.stubGlobal('JSON', Object.assign(Object.create(JSON), { parse: observeParse }));
const preview = previewCollection(blueprint.id);
const previewParses = manifestParses;
expect(preview.ready).toBe(true);
expect(preview.servers).toHaveLength(serverCount);
expect(new Set(preview.servers.map((server) => server.name)).size).toBe(serverCount);
expect(preview.servers.every((server) => server.port === null)).toBe(true);
expect(db().prepare('SELECT * FROM ec_host_ports').all()).toHaveLength(0);
const native = await import('../../src/lib/server/provisioning/native.js');
const validateSelection = vi.spyOn(native, 'nativeLaunchSelection');
manifestParses = 0;
const result = queueCollection({
previewId: preview.id,
keys: Object.fromEntries(
preview.servers.map((server) => [server.name!, 'key-' + server.name])
)
});
const queueParses = manifestParses;
expect(result).toMatchObject({ ok: true, collectionId: expect.any(String) });
expect(queueCollection({ previewId: preview.id })).toEqual(result);
expect(validateSelection).toHaveBeenCalledTimes(1);
expect(getCollection(result.collectionId!)?.collection).toMatchObject({
total: serverCount,
queued: serverCount
});
expect(db().prepare('SELECT host_id,library_id,port FROM native_instances').all()).toEqual(
[]
);
expect(db().prepare('SELECT * FROM ec_host_ports').all()).toEqual([]);
const intents = db().prepare('SELECT selection FROM native_creation_intents').all() as {
selection: string;
}[];
expect(intents).toHaveLength(serverCount);
expect(
intents.every((intent) => JSON.parse(intent.selection).libraryId === libraryId)
).toBe(true);
expect(db().prepare('SELECT * FROM server_collection_members').all()).toHaveLength(
serverCount
);
expect(
previewParses,
'Preview must use library metadata without reading the full manifest'
).toBe(0);
expect(
queueParses,
'Queue admission must use library metadata without reading the full manifest'
).toBe(0);
}, 30_000);
it.each(['newer game files', 'deleted pinned host'])(
'rejects %s after review before queueing any collection work',
async (change) => {
const { saveBlueprint } = await import('../../src/lib/server/fleet/launch-recipes.js');
const { previewCollection, queueCollection } =
await import('../../src/lib/server/fleet/collections.js');
const { db } = await import('../../src/lib/server/db/index.js');
const blueprint = saveBlueprint({
name: 'Reviewed native selection',
entries: [{ game: 't5', mapSlug: 'theater', count: 2 }]
});
const preview = previewCollection(blueprint.id, 'fill', { hostId, libraryId, image });
if (change === 'newer game files') {
const { createLibraryImport } =
await import('../../src/lib/server/game-files/index.js');
const newer = createLibraryImport({ hostId, game: 't5' });
db()
.prepare(
"UPDATE game_file_versions SET state='ready',published_at=? WHERE id=?"
)
.run(new Date(Date.now() + 1000).toISOString(), newer.id);
} else
db()
.prepare('UPDATE ec_hosts SET deleted_at=? WHERE id=?')
.run(new Date().toISOString(), hostId);
expect(() =>
queueCollection({
previewId: preview.id,
keys: Object.fromEntries(
preview.servers.map((server) => [server.name!, 'key-' + server.name])
)
})
).toThrow(change === 'newer game files' ? /game files changed/ : /host was not found/);
expect(db().prepare('SELECT id FROM server_launch_collections').all()).toEqual([]);
expect(db().prepare('SELECT deployment_id FROM native_creation_intents').all()).toEqual(
[]
);
expect(db().prepare('SELECT id FROM server_keys').all()).toEqual([]);
}
);
});