Remove Komodo backend in favor of native hosts

This commit is contained in:
2026-10-05 18:07:07 -04:00
parent 701066aaf8
commit 4b0f341d64
116 changed files with 557 additions and 8132 deletions
+4 -251
View File
@@ -1,17 +1,9 @@
import { prepareNativeHost } from '../helpers/native-host.js';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { newFixture, type Fixture } from '../helpers/fixture.js';
vi.mock('../../src/lib/server/integrations/komodo.js', () => ({
komodoCreateDeployment: vi.fn(async () => ({ ok: true })),
komodoDeploy: vi.fn(async () => ({ ok: true })),
komodoDestroyDeployment: vi.fn(async () => ({ ok: true })),
komodoDeleteDeployment: vi.fn(async () => ({ ok: true })),
komodoInspectHealthy: vi.fn(async () => ({ ok: true, state: 'running' })),
komodoEnsureTag: vi.fn(async (name: string) => ({ ok: true, id: name })),
komodoUpdateTagsOnResource: vi.fn(async () => ({ ok: true }))
}));
vi.mock('../../src/lib/server/integrations/rcon.js', () => ({
rconGetPlayerCount: vi.fn(async () => 0),
rconPlutoniumVersion: vi.fn(async () => ({ revision: 5354, error: null })),
@@ -27,11 +19,9 @@ const input = { ...selection, gameMode: 'zombie' as const, plutoKey: 'private-pl
beforeEach(async () => {
vi.resetModules(); vi.clearAllMocks();
fixture = await newFixture([]);
await prepareNativeHost();
vi.stubGlobal('fetch', vi.fn(async () => Response.json({ product: 'plutonium-core-prod', revision: 5354, files: [] })));
vi.stubEnv('KOMODO_ADDRESS', 'http://komodo.test');
vi.stubEnv('KOMODO_API_KEY', 'private-api-key');
vi.stubEnv('KOMODO_API_SECRET', 'private-api-secret');
vi.stubEnv('KOMODO_TARGET_SERVER', 'test-host');
vi.stubEnv('RCON_PASSWORD', 'private-rcon-password');
});
afterEach(async () => { await fixture.cleanup(); vi.unstubAllEnvs(); vi.unstubAllGlobals(); });
@@ -46,42 +36,11 @@ async function seedArtifacts() {
}
describe('launch recipes and previews', () => {
it('keeps detail polling independent of retained plugin bytes after launching', async () => {
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
const plugin = createScript({ name: 'Large utility plugin', kind: 'plugin', scopeType: 'game', scopeValue: 't5',
targetRelPath: 'large.dll', content: Buffer.alloc(8 * 1024 ** 2, 42) });
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(plugin.version.id, plugin.script.id);
const { queueCreateServer, processCreations } = await import('../../src/lib/server/fleet/create.js');
const { getDeployment } = await import('../../src/lib/server/fleet/deployments.js');
const { recipeForDeployment, summarizeRecipe } = await import('../../src/lib/server/fleet/launch-recipes.js');
const queued = queueCreateServer(input);
if (!queued.ok) throw new Error(queued.message);
const queuedSummary = getDeployment(queued.deploymentId)!.recipe!;
await processCreations();
const full = recipeForDeployment(queued.deploymentId)!.recipe;
const expected = summarizeRecipe(full);
expect(expected.fingerprint).not.toBe(queuedSummary.fingerprint);
expect(full.files.find(file => file.path === 'large.dll')?.content).toBe(plugin.version.content.toString('base64'));
closeDb();
const parse = JSON.parse;
const parsing = vi.spyOn(JSON, 'parse').mockImplementation((value, reviver) => {
if (value.length > 1024 * 1024) throw new Error('Detail polling parsed plugin bytes.');
return parse(value, reviver);
});
try {
for (let read = 0; read < 3; read++) expect(getDeployment(queued.deploymentId)?.recipe).toEqual(expected);
const row = db().prepare('SELECT length(recipe_summary) AS bytes FROM server_launch_recipes WHERE deployment_id = ?')
.get(queued.deploymentId) as { bytes: number };
expect(row.bytes).toBeLessThan(10_000);
} finally { parsing.mockRestore(); }
});
it('previews without reserving capacity, saving keys, exposing bytes, or calling Komodo', async () => {
it('previews without reserving capacity, saving keys, exposing bytes, or submitting host commands', async () => {
await seedArtifacts();
const { previewCreation } = await import('../../src/lib/server/fleet/create.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { komodoCreateDeployment } = await import('../../src/lib/server/integrations/komodo.js');
const first = previewCreation(selection);
const second = previewCreation(selection);
expect(first).toMatchObject({ name: 't5_zombie_theater_1', port: 28960, hasSavedKey: false, ready: true });
@@ -91,68 +50,9 @@ describe('launch recipes and previews', () => {
expect(first.recipe.files[0]).not.toHaveProperty('content');
expect(db().prepare('SELECT * FROM server_keys').all()).toEqual([]);
expect(db().prepare('SELECT * FROM server_creation_jobs').all()).toEqual([]);
expect(komodoCreateDeployment).not.toHaveBeenCalled();
expect(JSON.stringify(first)).not.toContain('private-');
});
it('uses promotions made after review and queueing, including plugins and composed configuration', async () => {
const { baseline, script } = await seedArtifacts();
const { previewCreation, queueCreateServer, processCreations } = await import('../../src/lib/server/fleet/create.js');
const { createScript, uploadNewVersion } = await import('../../src/lib/server/fleet/scripts.js');
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
const { getDeployment } = await import('../../src/lib/server/fleet/deployments.js');
const { getServerScript } = await import('../../src/lib/server/fleet/inventory.js');
const { recipeForDeployment } = await import('../../src/lib/server/fleet/launch-recipes.js');
const plugin = createScript({ name: 'Game utilities', kind: 'plugin', scopeType: 'game', scopeValue: 't5', targetRelPath: 'utilities.dll', content: Buffer.from('old plugin') });
const overlay = createScript({ name: 'Kino settings', kind: 'config', scopeType: 'map', scopeValue: 'zombie_theater', content: Buffer.from('set g_speed "180"\n') });
for (const item of [plugin, overlay]) db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(item.version.id, item.script.id);
const preview = previewCreation(selection);
const newScript = uploadNewVersion(script.script.id, Buffer.from('main(){ new_perks(); }'));
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(newScript.id, script.script.id);
const newOverlay = uploadNewVersion(overlay.script.id, Buffer.from('set g_speed "200"\n'));
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(newOverlay.id, overlay.script.id);
uploadNewVersion(script.script.id, Buffer.from('main(){ unpromoted_draft(); }'));
const queued = queueCreateServer({ ...input, previewId: preview.id });
if (!queued.ok) throw new Error(queued.message);
expect(recipeForDeployment(queued.deploymentId)?.recipe.files).toContainEqual(expect.objectContaining({ path: 'perks.gsc', versionId: newScript.id }));
const newBaseline = uploadNewVersion(baseline.script.id, Buffer.from('set sv_voice "0"\n'));
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(newBaseline.id, baseline.script.id);
const newPlugin = uploadNewVersion(plugin.script.id, Buffer.from('new plugin'));
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(newPlugin.id, plugin.script.id);
uploadNewVersion(plugin.script.id, Buffer.from('unpromoted plugin'));
closeDb();
await processCreations();
expect(await readFile(join(fixture.t5, queued.name, 'runtime/plutonium/storage/t5/raw/scripts/sp/perks.gsc'), 'utf8')).toBe('main(){ new_perks(); }');
const deployedConfig = await readFile(join(fixture.t5, queued.name, 'configs/dedicated_sp.cfg'), 'utf8');
expect(deployedConfig).toContain('set sv_voice "0"');
expect(deployedConfig).toContain('set g_speed "200"');
expect(await readFile(join(fixture.t5, queued.name, 'runtime/plutonium/plugins/utilities.dll'), 'utf8')).toBe('new plugin');
expect(getDeployment(queued.deploymentId)?.recipe?.files).toContainEqual(expect.objectContaining({ path: 'utilities.dll', versionId: newPlugin.id }));
expect(getServerScript(queued.name, 'perks.gsc')).toMatchObject({ script_version_id: newScript.id, sha256: newScript.sha256 });
expect(getServerScript(queued.name, 'utilities.dll')).toMatchObject({ script_version_id: newPlugin.id, sha256: newPlugin.sha256 });
const recordedConfig = recipeForDeployment(queued.deploymentId)?.recipe.files.find(file => file.path === 'dedicated_sp.cfg');
if (recordedConfig?.content === undefined) throw new Error('Attempted launch must retain the configuration bytes.');
expect(Buffer.from(recordedConfig.content, 'base64').toString()).toBe(deployedConfig);
expect(getDeployment(queued.deploymentId)?.recipe?.fingerprint).not.toBe(preview.recipe.fingerprint);
expect(getDeployment(queued.deploymentId)?.deployment).toMatchObject({ state: 'awaiting-health' });
await processCreations();
expect(getDeployment(queued.deploymentId)?.deployment).toMatchObject({ state: 'done' });
expect(JSON.stringify(getDeployment(queued.deploymentId))).not.toContain('private-');
});
it('uses the latest promotion at provisioning for API clients without a preview', async () => {
const { script } = await seedArtifacts();
const { queueCreateServer, processCreations } = await import('../../src/lib/server/fleet/create.js');
const { uploadNewVersion } = await import('../../src/lib/server/fleet/scripts.js');
const { db } = await import('../../src/lib/server/db/index.js');
const queued = queueCreateServer(input);
if (!queued.ok) throw new Error(queued.message);
const changed = uploadNewVersion(script.script.id, Buffer.from('changed'));
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(changed.id, script.script.id);
await processCreations();
expect(await readFile(join(fixture.t5, queued.name, 'runtime/plutonium/storage/t5/raw/scripts/sp/perks.gsc'), 'utf8')).toBe('changed');
});
it('returns the same operation for repeated submission, even after expiry and restart', async () => {
const { previewCreation, queueCreateServer } = await import('../../src/lib/server/fleet/create.js');
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
@@ -167,31 +67,6 @@ describe('launch recipes and previews', () => {
expect(getServerKey(preview.name!)).toBe('private-pluto-key');
});
it('rejects stale slots and ports before saving a key or queueing a second server', async () => {
const { previewCreation, queueCreateServer } = await import('../../src/lib/server/fleet/create.js');
const { getServerKey } = await import('../../src/lib/server/fleet/server-keys.js');
const preview = previewCreation(selection);
expect(queueCreateServer({ ...input, mapSlug: 'moon' }).ok).toBe(true);
expect(queueCreateServer({ ...input, previewId: preview.id })).toMatchObject({ ok: false, kind: 'no-slot' });
expect(getServerKey(preview.name!)).toBeUndefined();
const fresh = previewCreation(selection);
expect(fresh.port).toBe(28961);
expect(queueCreateServer({ ...input, previewId: fresh.id }).ok).toBe(true);
expect(queueCreateServer({ ...input, previewId: preview.id })).toMatchObject({ ok: false, kind: 'no-slot' });
expect(getServerKey('t5_zombie_theater_2')).toBeUndefined();
});
it('does not provision an already accepted preview again through internal callers', async () => {
const { previewCreation, createServer } = await import('../../src/lib/server/fleet/create.js');
const { komodoCreateDeployment, komodoDeploy } = await import('../../src/lib/server/integrations/komodo.js');
const preview = previewCreation(selection);
const first = await createServer({ ...input, previewId: preview.id });
expect(first.ok).toBe(true);
expect(await createServer({ ...input, previewId: preview.id })).toEqual(first);
expect(komodoCreateDeployment).toHaveBeenCalledTimes(1);
expect(komodoDeploy).toHaveBeenCalledTimes(1);
});
it('carries preview identity through the public creation API and rejects malformed requests', async () => {
const { POST: preview } = await import('../../src/routes/api/deployments/preview/+server.js');
const { POST: launch } = await import('../../src/routes/api/servers/+server.js');
@@ -218,93 +93,6 @@ describe('launch recipes and previews', () => {
expect(db().prepare('SELECT * FROM server_keys').all()).toEqual([]);
});
it('reports configuration blockers while still allowing recipes to be saved', async () => {
vi.stubEnv('KOMODO_TARGET_SERVER', ''); vi.stubEnv('RCON_PASSWORD', '');
const { previewCreation, queueCreateServer } = await import('../../src/lib/server/fleet/create.js');
const { saveBlueprint } = await import('../../src/lib/server/fleet/launch-recipes.js');
const preview = previewCreation(selection);
expect(preview.ready).toBe(false);
expect(preview.checks.filter(c => c.state === 'blocked')).toHaveLength(2);
expect(saveBlueprint({ name: 'For later', previewId: preview.id }).name).toBe('For later');
expect(queueCreateServer({ ...input, previewId: preview.id })).toMatchObject({ ok: false, kind: 'unconfigured' });
});
it('resolves current file eligibility after a blueprint review and queued launch', async () => {
const { script } = await seedArtifacts();
const { previewCreation, queueCreateServer, processCreations } = await import('../../src/lib/server/fleet/create.js');
const { saveBlueprint, deleteBlueprint, listBlueprints, recipeForDeployment } = await import('../../src/lib/server/fleet/launch-recipes.js');
const { createScript, setScope } = await import('../../src/lib/server/fleet/scripts.js');
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
const removed = createScript({ name: 'Removed script', targetRelPath: 'removed.gsc', scopeType: 'game', scopeValue: 't5', content: Buffer.from('removed') });
const unpromoted = createScript({ name: 'Unpromoted script', targetRelPath: 'unpromoted.gsc', scopeType: 'game', scopeValue: 't5', content: Buffer.from('unpromoted') });
for (const item of [removed, unpromoted]) db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(item.version.id, item.script.id);
const blueprint = saveBlueprint({ name: 'Classic night', description: 'Kino lineup', previewId: previewCreation(selection).id });
const stored = db().prepare('SELECT recipe FROM server_blueprints WHERE id = ?').get(blueprint.id) as { recipe: string };
expect(JSON.parse(stored.recipe)).toEqual({ format: 3, entries: [{ count: 1, ...selection }] });
closeDb();
expect(listBlueprints()[0]).toEqual(blueprint);
const preview = previewCreation({ blueprintId: blueprint.id });
deleteBlueprint(blueprint.id);
const queued = queueCreateServer({ ...input, previewId: preview.id });
if (!queued.ok) throw new Error(queued.message);
db().prepare('UPDATE scripts SET current_version_id = NULL WHERE id = ?').run(removed.script.id);
db().prepare('DELETE FROM scripts WHERE id = ?').run(removed.script.id);
db().prepare('UPDATE scripts SET current_version_id = NULL WHERE id = ?').run(unpromoted.script.id);
setScope({ scriptId: script.script.id, scopeType: 'map', scopeValue: 'zombie_moon' });
const added = createScript({ name: 'New telemetry', targetRelPath: 'telemetry.gsc', scopeType: 'map', scopeValue: 'zombie_theater', content: Buffer.from('newly promoted telemetry') });
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(added.version.id, added.script.id);
createScript({ name: 'Draft telemetry', targetRelPath: 'draft.gsc', scopeType: 'game', scopeValue: 't5', content: Buffer.from('never promoted') });
await processCreations();
for (const file of ['perks.gsc', 'removed.gsc', 'unpromoted.gsc', 'draft.gsc']) {
await expect(readFile(join(fixture.t5, queued.name, 'runtime/plutonium/storage/t5/raw/scripts/sp', file))).rejects.toMatchObject({ code: 'ENOENT' });
}
expect(await readFile(join(fixture.t5, queued.name, 'runtime/plutonium/storage/t5/raw/scripts/sp/telemetry.gsc'), 'utf8')).toBe('newly promoted telemetry');
expect(recipeForDeployment(queued.deploymentId)?.blueprintName).toBe('Classic night');
expect(recipeForDeployment(queued.deploymentId)?.recipe.files.filter(file => file.kind === 'script')).toEqual([
expect.objectContaining({ path: 'telemetry.gsc', versionId: added.version.id })
]);
});
it('launches again with current promotions while preserving the completed deployment record', async () => {
const { script } = await seedArtifacts();
const { previewCreation, createServer, queueCreateServer, processCreations } = await import('../../src/lib/server/fleet/create.js');
const { saveBlueprint, recipeForDeployment } = await import('../../src/lib/server/fleet/launch-recipes.js');
const { uploadNewVersion } = await import('../../src/lib/server/fleet/scripts.js');
const { db } = await import('../../src/lib/server/db/index.js');
const original = await createServer(input);
if (!original.ok) throw new Error(original.message);
const historicalRecipe = recipeForDeployment(original.deploymentId);
const promoted = uploadNewVersion(script.script.id, Buffer.from('current telemetry'));
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(promoted.id, script.script.id);
const replay = previewCreation({ deploymentId: original.deploymentId });
expect(replay).toMatchObject({ name: 't5_zombie_theater_2', port: 28961, hasSavedKey: false });
expect(replay.recipe.files).toContainEqual(expect.objectContaining({ path: 'perks.gsc', versionId: promoted.id }));
const blueprint = saveBlueprint({ name: 'Restored', deploymentId: original.deploymentId });
expect(blueprint.entries).toEqual([{ count: 1, game: 't5', mapSlug: 'theater', mapName: 'Kino Der Toten' }]);
const stored = db().prepare('SELECT recipe FROM server_blueprints WHERE id = ?').get(blueprint.id) as { recipe: string };
expect(JSON.parse(stored.recipe)).toEqual({ format: 3, entries: [{ count: 1, ...selection }] });
expect(previewCreation({ blueprintId: blueprint.id }).recipe.files).toContainEqual(expect.objectContaining({ path: 'perks.gsc', versionId: promoted.id }));
const queued = queueCreateServer({ ...input, previewId: replay.id });
if (!queued.ok) throw new Error(queued.message);
await processCreations();
expect(await readFile(join(fixture.t5, queued.name, 'runtime/plutonium/storage/t5/raw/scripts/sp/perks.gsc'), 'utf8')).toBe('current telemetry');
expect(recipeForDeployment(original.deploymentId)).toEqual(historicalRecipe);
expect(await readFile(join(fixture.t5, original.name, 'runtime/plutonium/storage/t5/raw/scripts/sp/perks.gsc'), 'utf8')).toBe('main(){ old_perks(); }');
});
it('preserves the exact T6 map variant in a blueprint', async () => {
const { previewCreation, queueCreateServer, processCreations } = await import('../../src/lib/server/fleet/create.js');
const { saveBlueprint } = await import('../../src/lib/server/fleet/launch-recipes.js');
const preview = previewCreation({ game: 't6', mapSlug: 'town' });
const blueprint = saveBlueprint({ name: 'Town survival', previewId: preview.id });
const saved = previewCreation({ blueprintId: blueprint.id });
expect(saved.recipe).toMatchObject({ game: 't6', mapSlug: 'town', expectedMap: 'zm_transit', configFile: 'dedicated_zm.cfg' });
const queued = queueCreateServer({ ...input, game: 't6', mapSlug: 'town', previewId: saved.id });
if (!queued.ok) throw new Error(queued.message);
await processCreations();
expect(await readFile(join(fixture.t6, queued.name, 'configs/electric_cherry_map.cfg'), 'utf8')).toContain('execgts zm_standard_town.cfg map zm_transit');
});
it('validates blueprint names, sources, and case-insensitive duplicates', async () => {
const { previewCreation } = await import('../../src/lib/server/fleet/create.js');
const { saveBlueprint } = await import('../../src/lib/server/fleet/launch-recipes.js');
@@ -316,38 +104,3 @@ describe('launch recipes and previews', () => {
expect(() => saveBlueprint({ name: ' classic night ', previewId })).toThrow(/already exists/);
});
});
describe('resuming verification', () => {
it('resumes an interrupted creation without creating or restarting its container again', async () => {
const { createServer, processCreations } = await import('../../src/lib/server/fleet/create.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { interruptDeployments, getDeployment, resumeVerification } = await import('../../src/lib/server/fleet/deployments.js');
const { komodoCreateDeployment, komodoDeploy } = await import('../../src/lib/server/integrations/komodo.js');
const created = await createServer(input);
if (!created.ok) throw new Error(created.message);
db().prepare("UPDATE server_deployments SET state = 'running' WHERE id = ?").run(created.deploymentId);
interruptDeployments();
expect(getDeployment(created.deploymentId)?.canResumeVerification).toBe(true);
expect(resumeVerification(created.deploymentId)).toBe(true);
expect(resumeVerification(created.deploymentId)).toBe(false);
await processCreations();
expect(getDeployment(created.deploymentId)?.deployment).toMatchObject({ state: 'done', error: null });
expect(komodoCreateDeployment).toHaveBeenCalledTimes(1);
expect(komodoDeploy).toHaveBeenCalledTimes(1);
});
it('refuses to observe a replacement server or interfere with a later destruction', async () => {
const { createServer, destroyServer } = await import('../../src/lib/server/fleet/create.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { finishDeployment, resumeVerification, getDeployment } = await import('../../src/lib/server/fleet/deployments.js');
const created = await createServer(input);
if (!created.ok) throw new Error(created.message);
finishDeployment(created.deploymentId, { ok: false, message: 'Interrupted' });
db().prepare('UPDATE servers SET game_port = game_port + 1 WHERE name = ?').run(created.name);
expect(resumeVerification(created.deploymentId)).toBe(false);
db().prepare('UPDATE servers SET game_port = ? WHERE name = ?').run(created.port, created.name);
expect(getDeployment(created.deploymentId)?.canResumeVerification).toBe(true);
destroyServer(created.name);
expect(resumeVerification(created.deploymentId)).toBe(false);
});
});