Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -3,7 +3,10 @@ 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 () => {}) }));
|
||||
vi.mock('../../src/lib/server/pipeline/worker.js', () => ({
|
||||
kick: vi.fn(async () => {}),
|
||||
kickProvisioning: vi.fn(async () => {})
|
||||
}));
|
||||
|
||||
let fixture: Fixture;
|
||||
let hostId: string;
|
||||
@@ -13,39 +16,66 @@ const image = 'example.test/plutainer:test';
|
||||
const serverCount = 100;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules(); vi.clearAllMocks();
|
||||
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');
|
||||
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]
|
||||
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');
|
||||
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
|
||||
})));
|
||||
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(?)`)
|
||||
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 }]);
|
||||
updateHostLibraryCaches(hostId, [
|
||||
{ libraryVersionId: libraryId, status: 'ready', manifestSha256 }
|
||||
]);
|
||||
heartbeat(hostId, { ...inventory, libraries: [{ id: libraryId, state: 'ready' }] });
|
||||
});
|
||||
|
||||
@@ -59,11 +89,21 @@ afterEach(async () => {
|
||||
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 { 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 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;
|
||||
@@ -77,47 +117,89 @@ describe('large native collection admission', () => {
|
||||
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(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 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(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 }[];
|
||||
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);
|
||||
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([]);
|
||||
});
|
||||
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([]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user