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, runtimeImages: [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([]); } ); });