Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -13,7 +13,11 @@ beforeEach(async () => {
|
||||
|
||||
vi.stubEnv('RCON_PASSWORD', '');
|
||||
});
|
||||
afterEach(async () => { vi.restoreAllMocks(); await fixture.cleanup(); vi.unstubAllEnvs(); });
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await fixture.cleanup();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
async function intent() {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
@@ -22,10 +26,18 @@ async function intent() {
|
||||
const { beginDeployment } = await import('../../src/lib/server/provisioning/history.js');
|
||||
const id = beginDeployment(server, 'create');
|
||||
db().prepare("UPDATE server_deployments SET state='queued' WHERE id=?").run(id);
|
||||
db().prepare('INSERT INTO native_creation_intents(deployment_id,selection) VALUES(?,?)')
|
||||
.run(id, JSON.stringify({ automatic: true, libraryId: 'library', image: 'plutainer:test' }));
|
||||
db().prepare(`INSERT INTO server_creation_jobs(deployment_id,server,port,game_mode,map_slug,expected_map)
|
||||
VALUES(?,?,NULL,'zombie','theater','zombie_theater')`).run(id, server);
|
||||
db()
|
||||
.prepare('INSERT INTO native_creation_intents(deployment_id,selection) VALUES(?,?)')
|
||||
.run(
|
||||
id,
|
||||
JSON.stringify({ automatic: true, libraryId: 'library', image: 'plutainer:test' })
|
||||
);
|
||||
db()
|
||||
.prepare(
|
||||
`INSERT INTO server_creation_jobs(deployment_id,server,port,game_mode,map_slug,expected_map)
|
||||
VALUES(?,?,NULL,'zombie','theater','zombie_theater')`
|
||||
)
|
||||
.run(id, server);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -33,8 +45,10 @@ describe('native creation intents before placement', () => {
|
||||
it('retains its identity and native history across restart without changing ownership', async () => {
|
||||
const id = await intent();
|
||||
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
const { getDeployment, interruptDeployments } = await import('../../src/lib/server/provisioning/history.js');
|
||||
const { nativeDeployment, unplacedNativeCreation } = await import('../../src/lib/server/backends/native.js');
|
||||
const { getDeployment, interruptDeployments } =
|
||||
await import('../../src/lib/server/provisioning/history.js');
|
||||
const { nativeDeployment, unplacedNativeCreation } =
|
||||
await import('../../src/lib/server/backends/native.js');
|
||||
const { nextLobbyNumber } = await import('../../src/lib/server/provisioning/instances.js');
|
||||
const { requestLifecycle } = await import('../../src/lib/server/provisioning/requests.js');
|
||||
expect(nativeDeployment(id)).toBe(true);
|
||||
@@ -45,9 +59,15 @@ describe('native creation intents before placement', () => {
|
||||
interruptDeployments();
|
||||
const { processProvisioning } = await import('../../src/lib/server/provisioning/worker.js');
|
||||
await processProvisioning();
|
||||
expect(getDeployment(id)).toMatchObject({ backend: 'native', canCancelCreation: true, agentOperation: null,
|
||||
deployment: { state: 'queued', server_exists: 0 } });
|
||||
expect(db().prepare('SELECT port FROM server_creation_jobs WHERE deployment_id=?').get(id)).toEqual({ port: null });
|
||||
expect(getDeployment(id)).toMatchObject({
|
||||
backend: 'native',
|
||||
canCancelCreation: true,
|
||||
agentOperation: null,
|
||||
deployment: { state: 'queued', server_exists: 0 }
|
||||
});
|
||||
expect(
|
||||
db().prepare('SELECT port FROM server_creation_jobs WHERE deployment_id=?').get(id)
|
||||
).toEqual({ port: null });
|
||||
expect(db().prepare('SELECT id FROM native_instances').all()).toEqual([]);
|
||||
expect(db().prepare('SELECT deployment_id FROM native_provisioning').all()).toEqual([]);
|
||||
});
|
||||
@@ -55,10 +75,13 @@ describe('native creation intents before placement', () => {
|
||||
it('cancels unplaced work locally once, retains keys, and frees its logical server name', async () => {
|
||||
const id = await intent();
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { reserveServerKey, getServerKey, serverKeyPool } = await import('../../src/lib/server/fleet/server-keys.js');
|
||||
const { cancelCreation, canCancelCreation } = await import('../../src/lib/server/provisioning/cancel-creation.js');
|
||||
const { reserveServerKey, getServerKey, serverKeyPool } =
|
||||
await import('../../src/lib/server/fleet/server-keys.js');
|
||||
const { cancelCreation, canCancelCreation } =
|
||||
await import('../../src/lib/server/provisioning/cancel-creation.js');
|
||||
const { getDeployment } = await import('../../src/lib/server/provisioning/history.js');
|
||||
const { nativeDeployment, unplacedNativeCreation } = await import('../../src/lib/server/backends/native.js');
|
||||
const { nativeDeployment, unplacedNativeCreation } =
|
||||
await import('../../src/lib/server/backends/native.js');
|
||||
const { nextLobbyNumber } = await import('../../src/lib/server/provisioning/instances.js');
|
||||
reserveServerKey(server, 't5', 'saved-key');
|
||||
expect(canCancelCreation(id)).toBe(true);
|
||||
@@ -67,8 +90,15 @@ describe('native creation intents before placement', () => {
|
||||
expect(cancelCreation(id)).toBe(cleanup);
|
||||
expect(canCancelCreation(id)).toBe(false);
|
||||
expect(nativeDeployment(cleanup)).toBe(true);
|
||||
expect(getDeployment(id)).toMatchObject({ backend: 'native', cancellation: { state: 'done' }, deployment: { state: 'cancelled' } });
|
||||
expect(getDeployment(cleanup)).toMatchObject({ backend: 'native', deployment: { state: 'done', server_deleted_at: expect.any(String) } });
|
||||
expect(getDeployment(id)).toMatchObject({
|
||||
backend: 'native',
|
||||
cancellation: { state: 'done' },
|
||||
deployment: { state: 'cancelled' }
|
||||
});
|
||||
expect(getDeployment(cleanup)).toMatchObject({
|
||||
backend: 'native',
|
||||
deployment: { state: 'done', server_deleted_at: expect.any(String) }
|
||||
});
|
||||
expect(unplacedNativeCreation(server)).toBeNull();
|
||||
expect(nextLobbyNumber('t5_zombie_theater')).toBe(1);
|
||||
expect(getServerKey(server)).toBeUndefined();
|
||||
@@ -80,43 +110,72 @@ describe('native creation intents before placement', () => {
|
||||
it('will not cancel an unplaced historical intent against a replacement server', async () => {
|
||||
const id = await intent();
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
db().prepare("INSERT INTO servers(name) VALUES(?)").run(server);
|
||||
const { cancelCreation, canCancelCreation } = await import('../../src/lib/server/provisioning/cancel-creation.js');
|
||||
db().prepare('INSERT INTO servers(name) VALUES(?)').run(server);
|
||||
const { cancelCreation, canCancelCreation } =
|
||||
await import('../../src/lib/server/provisioning/cancel-creation.js');
|
||||
expect(canCancelCreation(id)).toBe(false);
|
||||
expect(cancelCreation(id)).toBeNull();
|
||||
expect(db().prepare('SELECT name FROM servers WHERE name=?').get(server)).toEqual({ name: server });
|
||||
expect(db().prepare('SELECT cleanup_id FROM native_creation_cancellations').all()).toEqual([]);
|
||||
expect(db().prepare('SELECT name FROM servers WHERE name=?').get(server)).toEqual({
|
||||
name: server
|
||||
});
|
||||
expect(db().prepare('SELECT cleanup_id FROM native_creation_cancellations').all()).toEqual(
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it.each([false, true])('does not adopt a replacement instance during placement (previously bound: %s)', async previouslyBound => {
|
||||
const id = await intent();
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { createHost } = await import('../../src/lib/server/hosts/index.js');
|
||||
const { captureRecipePlan, recipeStorage } = await import('../../src/lib/server/fleet/launch-recipes.js');
|
||||
const hostId = createHost({ name: 'Replacement host' }).id;
|
||||
const insert = db().prepare(`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at,retired_at)
|
||||
it.each([false, true])(
|
||||
'does not adopt a replacement instance during placement (previously bound: %s)',
|
||||
async (previouslyBound) => {
|
||||
const id = await intent();
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { createHost } = await import('../../src/lib/server/hosts/index.js');
|
||||
const { captureRecipePlan, recipeStorage } =
|
||||
await import('../../src/lib/server/fleet/launch-recipes.js');
|
||||
const hostId = createHost({ name: 'Replacement host' }).id;
|
||||
const insert = db()
|
||||
.prepare(`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at,retired_at)
|
||||
VALUES(?,?,?,'library','plutainer:test',28960,?,?)`);
|
||||
if (previouslyBound) {
|
||||
insert.run('original-instance', server, hostId, '2025-01-01', '2025-01-02');
|
||||
db().prepare('INSERT INTO native_provisioning(deployment_id,instance_id,generation) VALUES(?,?,1)').run(id, 'original-instance');
|
||||
if (previouslyBound) {
|
||||
insert.run('original-instance', server, hostId, '2025-01-01', '2025-01-02');
|
||||
db()
|
||||
.prepare(
|
||||
'INSERT INTO native_provisioning(deployment_id,instance_id,generation) VALUES(?,?,1)'
|
||||
)
|
||||
.run(id, 'original-instance');
|
||||
}
|
||||
insert.run('replacement-instance', server, hostId, '2025-01-02', null);
|
||||
const before = db().prepare('SELECT * FROM native_instances ORDER BY id').all();
|
||||
const stored = recipeStorage(captureRecipePlan('t5', 'theater'));
|
||||
db()
|
||||
.prepare(
|
||||
'INSERT INTO server_launch_recipes(deployment_id,recipe,recipe_summary) VALUES(?,?,?)'
|
||||
)
|
||||
.run(id, stored.recipe, stored.recipeSummary);
|
||||
const preparation =
|
||||
await import('../../src/lib/server/provisioning/launch-preparation.js');
|
||||
const placement = vi
|
||||
.spyOn(preparation, 'evaluateNativePlacement')
|
||||
.mockRejectedValue(new Error('Unexpected placement'));
|
||||
const { processNativeProvisioning } =
|
||||
await import('../../src/lib/server/provisioning/native.js');
|
||||
await processNativeProvisioning(() => null);
|
||||
expect(placement).not.toHaveBeenCalled();
|
||||
expect(
|
||||
db().prepare('SELECT state,error FROM server_deployments WHERE id=?').get(id)
|
||||
).toEqual({
|
||||
state: 'failed',
|
||||
error: 'Original instance was retired or replaced; no command was sent.'
|
||||
});
|
||||
expect(db().prepare('SELECT * FROM native_instances ORDER BY id').all()).toEqual(
|
||||
before
|
||||
);
|
||||
expect(db().prepare('SELECT id FROM ec_host_operations').all()).toEqual([]);
|
||||
expect(db().prepare('SELECT instance_id FROM ec_host_ports').all()).toEqual([]);
|
||||
expect(
|
||||
db().prepare('SELECT port FROM server_creation_jobs WHERE deployment_id=?').get(id)
|
||||
).toEqual({ port: null });
|
||||
}
|
||||
insert.run('replacement-instance', server, hostId, '2025-01-02', null);
|
||||
const before = db().prepare('SELECT * FROM native_instances ORDER BY id').all();
|
||||
const stored = recipeStorage(captureRecipePlan('t5', 'theater'));
|
||||
db().prepare('INSERT INTO server_launch_recipes(deployment_id,recipe,recipe_summary) VALUES(?,?,?)')
|
||||
.run(id, stored.recipe, stored.recipeSummary);
|
||||
const preparation = await import('../../src/lib/server/provisioning/launch-preparation.js');
|
||||
const placement = vi.spyOn(preparation, 'evaluateNativePlacement').mockRejectedValue(new Error('Unexpected placement'));
|
||||
const { processNativeProvisioning } = await import('../../src/lib/server/provisioning/native.js');
|
||||
await processNativeProvisioning(() => null);
|
||||
expect(placement).not.toHaveBeenCalled();
|
||||
expect(db().prepare('SELECT state,error FROM server_deployments WHERE id=?').get(id))
|
||||
.toEqual({ state: 'failed', error: 'Original instance was retired or replaced; no command was sent.' });
|
||||
expect(db().prepare('SELECT * FROM native_instances ORDER BY id').all()).toEqual(before);
|
||||
expect(db().prepare('SELECT id FROM ec_host_operations').all()).toEqual([]);
|
||||
expect(db().prepare('SELECT instance_id FROM ec_host_ports').all()).toEqual([]);
|
||||
expect(db().prepare('SELECT port FROM server_creation_jobs WHERE deployment_id=?').get(id)).toEqual({ port: null });
|
||||
});
|
||||
);
|
||||
|
||||
it('starts work on a ready host when an older pinned intent is waiting for an offline host', async () => {
|
||||
vi.stubEnv('GAME_FILES_ROOT', join(fixture.root, 'libraries'));
|
||||
@@ -125,35 +184,93 @@ describe('native creation intents before placement', () => {
|
||||
const hosts = await import('../../src/lib/server/hosts/index.js');
|
||||
const offline = hosts.createHost({ name: 'Offline host' });
|
||||
const ready = hosts.createHost({ name: 'Ready host' });
|
||||
const inventory = { protocolVersion: 1, dockerRunning: true, directoriesWritable: true,
|
||||
cpuCount: 16, cpuUsagePercent: 5, memoryTotal: 32 * 1024 ** 3, memoryAvailable: 30 * 1024 ** 3,
|
||||
storageAvailable: 100_000_000_000, runtimeImages: [image] };
|
||||
const inventory = {
|
||||
protocolVersion: 1,
|
||||
dockerRunning: true,
|
||||
directoriesWritable: true,
|
||||
cpuCount: 16,
|
||||
cpuUsagePercent: 5,
|
||||
memoryTotal: 32 * 1024 ** 3,
|
||||
memoryAvailable: 30 * 1024 ** 3,
|
||||
storageAvailable: 100_000_000_000,
|
||||
runtimeImages: [image]
|
||||
};
|
||||
hosts.enrollHost(hosts.createEnrollment(ready.id).token, inventory);
|
||||
const libraries = await import('../../src/lib/server/game-files/index.js');
|
||||
const imported = libraries.createLibraryImport({ hostId: ready.id, game: 't5' });
|
||||
const bytes = Buffer.from('verified base game');
|
||||
const file = { path: 'zone/base.ff', size: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') };
|
||||
const file = {
|
||||
path: 'zone/base.ff',
|
||||
size: bytes.length,
|
||||
sha256: createHash('sha256').update(bytes).digest('hex')
|
||||
};
|
||||
libraries.setImportManifest(ready.id, imported.id, [file]);
|
||||
await libraries.uploadImportFile(ready.id, imported.id, file.path, new Response(bytes).body);
|
||||
await libraries.uploadImportFile(
|
||||
ready.id,
|
||||
imported.id,
|
||||
file.path,
|
||||
new Response(bytes).body
|
||||
);
|
||||
const library = await libraries.publishImport(ready.id, imported.id);
|
||||
libraries.updateHostLibraryCaches(ready.id, [{ libraryVersionId: library.id, status: 'ready', manifestSha256: library.manifestSha256 }]);
|
||||
hosts.heartbeat(ready.id, { ...inventory, libraries: [{ id: library.id, state: 'ready' }] });
|
||||
const { fleetSettings, saveFleetSettings } = await import('../../src/lib/server/settings.js');
|
||||
libraries.updateHostLibraryCaches(ready.id, [
|
||||
{
|
||||
libraryVersionId: library.id,
|
||||
status: 'ready',
|
||||
manifestSha256: library.manifestSha256
|
||||
}
|
||||
]);
|
||||
hosts.heartbeat(ready.id, {
|
||||
...inventory,
|
||||
libraries: [{ id: library.id, state: 'ready' }]
|
||||
});
|
||||
const { fleetSettings, saveFleetSettings } =
|
||||
await import('../../src/lib/server/settings.js');
|
||||
saveFleetSettings({ ...fleetSettings(), createConcurrency: 1 });
|
||||
const { queueNativeCreation, processNativeProvisioning } = await import('../../src/lib/server/provisioning/native.js');
|
||||
const input = { game: 't5' as const, gameMode: 'zombie' as const, mapSlug: 'theater', libraryId: library.id, image, plutoKey: 'saved-key' };
|
||||
const { queueNativeCreation, processNativeProvisioning } =
|
||||
await import('../../src/lib/server/provisioning/native.js');
|
||||
const input = {
|
||||
game: 't5' as const,
|
||||
gameMode: 'zombie' as const,
|
||||
mapSlug: 'theater',
|
||||
libraryId: library.id,
|
||||
image,
|
||||
plutoKey: 'saved-key'
|
||||
};
|
||||
const blocked = queueNativeCreation({ ...input, hostId: offline.id });
|
||||
const available = queueNativeCreation({ ...input, hostId: ready.id, plutoKey: 'ready-key' });
|
||||
const available = queueNativeCreation({
|
||||
...input,
|
||||
hostId: ready.id,
|
||||
plutoKey: 'ready-key'
|
||||
});
|
||||
expect(blocked.ok && available.ok).toBe(true);
|
||||
if (!blocked.ok || !available.ok) throw new Error('Could not queue launch fixtures.');
|
||||
await processNativeProvisioning(() => null);
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
expect(db().prepare('SELECT state FROM server_deployments WHERE id=?').get(blocked.deploymentId)).toEqual({ state: 'queued' });
|
||||
expect(db().prepare('SELECT state FROM server_deployments WHERE id=?').get(available.deploymentId)).toEqual({ state: 'running' });
|
||||
expect(db().prepare("SELECT host_id FROM ec_host_operations WHERE kind='instance.create'").all()).toEqual([{ host_id: ready.id }]);
|
||||
expect(db().prepare('SELECT server,host_id FROM native_instances WHERE retired_at IS NULL').all())
|
||||
.toEqual([{ server: available.name, host_id: ready.id }]);
|
||||
expect(db().prepare('SELECT port FROM server_creation_jobs WHERE deployment_id=?').get(blocked.deploymentId)).toEqual({ port: null });
|
||||
expect(
|
||||
db()
|
||||
.prepare('SELECT state FROM server_deployments WHERE id=?')
|
||||
.get(blocked.deploymentId)
|
||||
).toEqual({ state: 'queued' });
|
||||
expect(
|
||||
db()
|
||||
.prepare('SELECT state FROM server_deployments WHERE id=?')
|
||||
.get(available.deploymentId)
|
||||
).toEqual({ state: 'running' });
|
||||
expect(
|
||||
db()
|
||||
.prepare("SELECT host_id FROM ec_host_operations WHERE kind='instance.create'")
|
||||
.all()
|
||||
).toEqual([{ host_id: ready.id }]);
|
||||
expect(
|
||||
db()
|
||||
.prepare('SELECT server,host_id FROM native_instances WHERE retired_at IS NULL')
|
||||
.all()
|
||||
).toEqual([{ server: available.name, host_id: ready.id }]);
|
||||
expect(
|
||||
db()
|
||||
.prepare('SELECT port FROM server_creation_jobs WHERE deployment_id=?')
|
||||
.get(blocked.deploymentId)
|
||||
).toEqual({ port: null });
|
||||
});
|
||||
|
||||
it('handles exact-match removal of an unplaced launch as local cancellation', async () => {
|
||||
@@ -171,8 +288,12 @@ describe('native creation intents before placement', () => {
|
||||
it('migrates existing port reservations without losing rows, references, indexes, or triggers', async () => {
|
||||
const legacy = new Database(fixture.dbPath);
|
||||
try {
|
||||
legacy.exec(schema.replace(/CREATE TABLE IF NOT EXISTS server_creation_jobs \([\s\S]*?\n\);/,
|
||||
table => table.replace(' port INTEGER,', ' port INTEGER NOT NULL,')));
|
||||
legacy.exec(
|
||||
schema.replace(
|
||||
/CREATE TABLE IF NOT EXISTS server_creation_jobs \([\s\S]*?\n\);/,
|
||||
(table) => table.replace(' port INTEGER,', ' port INTEGER NOT NULL,')
|
||||
)
|
||||
);
|
||||
legacy.exec(`INSERT INTO server_deployments(id,server,action,state,stage,started_at,triggered_by)
|
||||
VALUES('existing','old-server','create','queued','Queued','2025-01-01','fixture');
|
||||
INSERT INTO server_creation_jobs(rowid,deployment_id,server,port,game_mode,map_slug,expected_map)
|
||||
@@ -183,18 +304,33 @@ describe('native creation intents before placement', () => {
|
||||
CREATE TABLE fixture_creation_audit(port INTEGER);
|
||||
CREATE TRIGGER fixture_creation_update AFTER UPDATE OF port ON server_creation_jobs
|
||||
BEGIN INSERT INTO fixture_creation_audit(port) VALUES(NEW.port); END;`);
|
||||
} finally { legacy.close(); }
|
||||
} finally {
|
||||
legacy.close();
|
||||
}
|
||||
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
for (let open = 0; open < 2; open++) {
|
||||
const handle = db();
|
||||
expect(handle.prepare('SELECT rowid,deployment_id,port FROM server_creation_jobs').get())
|
||||
.toEqual({ rowid: 8, deployment_id: 'existing', port: 28960 });
|
||||
expect(handle.prepare('SELECT id FROM fixture_creation_reference').get()).toEqual({ id: 'existing' });
|
||||
expect(handle.prepare("SELECT name FROM sqlite_master WHERE name IN ('fixture_creation_port','fixture_creation_update')").all()).toHaveLength(2);
|
||||
expect(
|
||||
handle.prepare('SELECT rowid,deployment_id,port FROM server_creation_jobs').get()
|
||||
).toEqual({ rowid: 8, deployment_id: 'existing', port: 28960 });
|
||||
expect(handle.prepare('SELECT id FROM fixture_creation_reference').get()).toEqual({
|
||||
id: 'existing'
|
||||
});
|
||||
expect(
|
||||
handle
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE name IN ('fixture_creation_port','fixture_creation_update')"
|
||||
)
|
||||
.all()
|
||||
).toHaveLength(2);
|
||||
expect(handle.pragma('foreign_key_check')).toEqual([]);
|
||||
closeDb();
|
||||
}
|
||||
db().prepare('UPDATE server_creation_jobs SET port=NULL WHERE deployment_id=?').run('existing');
|
||||
expect(db().prepare('SELECT port FROM fixture_creation_audit').all()).toEqual([{ port: null }]);
|
||||
db()
|
||||
.prepare('UPDATE server_creation_jobs SET port=NULL WHERE deployment_id=?')
|
||||
.run('existing');
|
||||
expect(db().prepare('SELECT port FROM fixture_creation_audit').all()).toEqual([
|
||||
{ port: null }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user