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
+57 -4
View File
@@ -13,14 +13,14 @@ const server = 't5_zombie_theater_1';
const healthy = { exists: true, healthy: true, state: 'running' };
beforeEach(async () => {
vi.resetModules(); vi.clearAllMocks(); fixture = await newFixture([]);
vi.stubEnv('KOMODO_ADDRESS', ''); vi.stubEnv('KOMODO_API_KEY', ''); vi.stubEnv('KOMODO_API_SECRET', '');
const { createHost, createEnrollment, enrollHost } = await import('../../src/lib/server/hosts/index.js');
hostId = createHost({ name: 'Safety test host' }).id;
enrollHost(createEnrollment(hostId).token, { protocolVersion: 1, dockerRunning: true, directoriesWritable: true,
storageAvailable: 100_000_000_000, runtimeImages: ['plutainer:test'], libraries: [{ id: 'published-game-version', state: 'ready' }] });
const { db } = await import('../../src/lib/server/db/index.js');
instanceId = randomUUID();
db().prepare("INSERT INTO servers(name,komodo_name,game,map,game_mode,map_slug,game_port,container_state) VALUES(?,'','t5','zombie_theater','zombie','theater',28960,'running')").run(server);
db().prepare("INSERT INTO servers(name,game,map,game_mode,map_slug,game_port,container_state) VALUES(?,'t5','zombie_theater','zombie','theater',28960,'running')").run(server);
db().prepare(`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at) VALUES(?,?,?,'published-game-version','plutainer:test',28960,?)`).run(instanceId, server, hostId, new Date().toISOString());
const backend = await import('../../src/lib/server/backends/native.js');
vi.spyOn(backend, 'nativeRequest').mockImplementation(async (_instance, kind) => {
@@ -31,8 +31,8 @@ beforeEach(async () => {
afterEach(async () => { await fixture.cleanup(); vi.unstubAllEnvs(); vi.restoreAllMocks(); });
async function advance() {
const { processNativeProvisioning } = await import('../../src/lib/server/provisioning/native.js');
await processNativeProvisioning(() => null);
const { processProvisioning } = await import('../../src/lib/server/provisioning/worker.js');
await processProvisioning();
}
async function operationId(id: string) {
const { db } = await import('../../src/lib/server/db/index.js');
@@ -119,3 +119,56 @@ describe('native disruption safety at dispatch', () => {
expect(db().prepare("SELECT COUNT(*) AS count FROM ec_host_operations WHERE kind='instance.restart'").get()).toEqual({ count: 0 });
});
});
describe('native scheduler ownership', () => {
it('rejects lifecycle commands for a registry row without a host instance', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
const { requestLifecycle } = await import('../../src/lib/server/provisioning/requests.js');
const { destroyServer } = await import('../../src/lib/server/provisioning/instances.js');
db().prepare("INSERT INTO servers(name) VALUES('unplaced-registry')").run();
expect(requestLifecycle('unplaced-registry', 'restart')).toMatchObject({ ok: false, status: 503 });
expect(destroyServer('unplaced-registry')).toMatchObject({ ok: false, kind: 'conflict' });
expect(db().prepare("SELECT id FROM server_deployments WHERE server='unplaced-registry'").all()).toEqual([]);
});
it('shares restart capacity until an agent receipt and fresh health release the first slot', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
const { fleetSettings, saveFleetSettings } = await import('../../src/lib/server/settings.js');
const { requestLifecycle } = await import('../../src/lib/server/provisioning/requests.js');
const { reportHostOperation } = await import('../../src/lib/server/hosts/index.js');
saveFleetSettings({ ...fleetSettings(), restartConcurrency: 1 });
db().prepare("INSERT INTO servers(name,game,map,game_port) VALUES('second','t5','zombie_theater',28961)").run();
db().prepare(`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at)
SELECT 'second-instance','second',host_id,library_id,image,28961,created_at FROM native_instances WHERE id=?`).run(instanceId);
const first = await waitingRestart();
const second = requestLifecycle('second', 'restart');
if (!second.ok) throw new Error(second.error);
await advance();
const command = await operationId(first);
expect(command).toEqual(expect.any(String));
expect(await operationId(second.deploymentId)).toBeNull();
await advance();
expect(await operationId(first)).toBe(command);
expect(await operationId(second.deploymentId)).toBeNull();
reportHostOperation(hostId, command!, { state: 'completed', result: healthy });
await advance();
await advance();
expect(db().prepare('SELECT state FROM server_deployments WHERE id=?').get(first)).toEqual({ state: 'done' });
expect(await operationId(second.deploymentId)).toEqual(expect.any(String));
expect(db().prepare("SELECT id FROM ec_host_operations WHERE kind='instance.restart'").all()).toHaveLength(2);
});
it('holds a paused campaign and cancels withdrawn work before delivering a restart', async () => {
const id = await stageCampaign();
const { db } = await import('../../src/lib/server/db/index.js');
db().prepare("UPDATE deployment_servers SET restart_state='held' WHERE deployment_id='campaign-race'").run();
await advance();
expect(await operationId(id)).toBeNull();
expect(db().prepare('SELECT state FROM server_deployments WHERE id=?').get(id)).toEqual({ state: 'waiting' });
db().prepare("UPDATE deployment_servers SET restart_state='done' WHERE deployment_id='campaign-race'").run();
await advance();
expect(await operationId(id)).toBeNull();
expect(db().prepare('SELECT state FROM server_deployments WHERE id=?').get(id)).toEqual({ state: 'cancelled' });
expect(db().prepare("SELECT id FROM ec_host_operations WHERE kind='instance.restart'").all()).toEqual([]);
});
});