Remove Komodo backend in favor of native hosts
This commit is contained in:
@@ -1,17 +1,9 @@
|
||||
import { prepareNativeHost } from '../helpers/native-host.js';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { access } 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(),
|
||||
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(), rconStatusResult: vi.fn()
|
||||
}));
|
||||
@@ -24,19 +16,18 @@ const server = 't5_zombie_theater_1';
|
||||
beforeEach(async () => {
|
||||
vi.resetModules(); vi.clearAllMocks();
|
||||
fixture = await newFixture([otherServer, server]);
|
||||
for (const [key, value] of Object.entries({ KOMODO_ADDRESS: 'http://komodo.test', KOMODO_API_KEY: 'test',
|
||||
KOMODO_API_SECRET: 'test', KOMODO_TARGET_SERVER: 'test-host', RCON_PASSWORD: 'test',
|
||||
SERVER_PORT_MIN: '28960', SERVER_PORT_MAX: '28980' })) vi.stubEnv(key, value);
|
||||
vi.stubEnv('RCON_PASSWORD', 'test');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
for (const [index, name] of [otherServer, server].entries()) {
|
||||
const map = name === server ? 'theater' : 'cosmodrome';
|
||||
db().prepare(`INSERT INTO servers (name, komodo_type, komodo_name, game, map, game_mode, map_slug, game_port, host_dir)
|
||||
VALUES (?, 'deployment', ?, 't5', ?, 'zombie', ?, ?, ?)`).run(name, name, 'zombie_' + map, map, 28960 + index, join(fixture.t5, name));
|
||||
db().prepare(`INSERT INTO servers (name, game, map, game_mode, map_slug, game_port, host_dir)
|
||||
VALUES (?, 't5', ?, 'zombie', ?, ?, ?)`).run(name, 'zombie_' + map, map, 28960 + index, join(fixture.t5, name));
|
||||
}
|
||||
await prepareNativeHost([otherServer, server]);
|
||||
const { mockAgentFiles } = await import('../helpers/agent-files.js');
|
||||
await mockAgentFiles();
|
||||
const { rconGetPlayerCount } = await import('../../src/lib/server/integrations/rcon.js');
|
||||
const { komodoInspectHealthy } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
vi.mocked(rconGetPlayerCount).mockReset().mockResolvedValue(0);
|
||||
vi.mocked(komodoInspectHealthy).mockReset().mockResolvedValue({ ok: false, state: 'exited', reason: 'stopped' });
|
||||
});
|
||||
afterEach(async () => { await fixture.cleanup(); vi.unstubAllEnvs(); });
|
||||
|
||||
@@ -55,9 +46,7 @@ async function attachBeta(state = 'running', outcome?: string, deleted = false)
|
||||
|
||||
async function expectServerPreserved() {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { komodoDestroyDeployment, komodoDeleteDeployment } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
expect(komodoDestroyDeployment).not.toHaveBeenCalled();
|
||||
expect(komodoDeleteDeployment).not.toHaveBeenCalled();
|
||||
expect(db().prepare("SELECT id FROM ec_host_operations WHERE kind LIKE 'instance.%'").all()).toEqual([]);
|
||||
expect(db().prepare('SELECT name FROM servers WHERE name = ?').get(server)).toEqual({ name: server });
|
||||
await expect(access(join(fixture.t5, server))).resolves.toBeUndefined();
|
||||
}
|
||||
@@ -117,86 +106,6 @@ describe('active beta destruction guard', () => {
|
||||
expect(destroyServer(server)).toMatchObject({ ok: true, deploymentId: expect.any(String) });
|
||||
});
|
||||
|
||||
it('holds a queued destruction until its beta closes, then completes the same request', async () => {
|
||||
const { destroyServer, processDestructions } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { getDeployment } = await import('../../src/lib/server/fleet/deployments.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { rconGetPlayerCount } = await import('../../src/lib/server/integrations/rcon.js');
|
||||
const { komodoDestroyDeployment, komodoDeleteDeployment } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
const queued = destroyServer(server);
|
||||
expect(queued.ok).toBe(true);
|
||||
const id = await attachBeta();
|
||||
await processDestructions();
|
||||
expect(getDeployment(queued.deploymentId!)?.deployment).toMatchObject({ state: 'waiting', stage: expect.stringMatching(/beta/i) });
|
||||
expect(rconGetPlayerCount).not.toHaveBeenCalled();
|
||||
await expectServerPreserved();
|
||||
db().prepare("UPDATE betas SET state = 'rolled-back', closed_at = ? WHERE id = ?").run(new Date().toISOString(), id);
|
||||
await processDestructions();
|
||||
expect(komodoDestroyDeployment).toHaveBeenCalledOnce();
|
||||
expect(komodoDeleteDeployment).toHaveBeenCalledOnce();
|
||||
expect(getDeployment(queued.deploymentId!)?.deployment).toMatchObject({ state: 'done', server_exists: 0 });
|
||||
});
|
||||
|
||||
it('rechecks beta ownership after an in-flight empty-server probe', async () => {
|
||||
const { destroyServer, processDestructions } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { getDeployment } = await import('../../src/lib/server/fleet/deployments.js');
|
||||
const { rconGetPlayerCount } = await import('../../src/lib/server/integrations/rcon.js');
|
||||
let resolveCount!: (players: number) => void;
|
||||
vi.mocked(rconGetPlayerCount).mockImplementationOnce(() => new Promise(resolve => { resolveCount = resolve; }));
|
||||
const queued = destroyServer(server);
|
||||
const work = processDestructions();
|
||||
await vi.waitFor(() => expect(resolveCount).toBeTypeOf('function'));
|
||||
await attachBeta();
|
||||
resolveCount(0);
|
||||
await work;
|
||||
expect(getDeployment(queued.deploymentId!)?.deployment).toMatchObject({ state: 'waiting', stage: expect.stringMatching(/beta/i) });
|
||||
await expectServerPreserved();
|
||||
});
|
||||
|
||||
it('also holds rebuild destruction when a beta appears during the occupancy probe', async () => {
|
||||
const { requestLifecycle, processLifecycle } = await import('../../src/lib/server/fleet/lifecycle.js');
|
||||
const { saveServerKey } = await import('../../src/lib/server/fleet/server-keys.js');
|
||||
const { getDeployment } = await import('../../src/lib/server/fleet/deployments.js');
|
||||
const { rconGetPlayerCount } = await import('../../src/lib/server/integrations/rcon.js');
|
||||
const { komodoInspectHealthy } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
saveServerKey(server, 'saved-key');
|
||||
vi.mocked(komodoInspectHealthy).mockResolvedValueOnce({ ok: true, state: 'running' });
|
||||
let resolveCount!: (players: number) => void;
|
||||
vi.mocked(rconGetPlayerCount).mockImplementationOnce(() => new Promise(resolve => { resolveCount = resolve; }));
|
||||
const queued = requestLifecycle(server, 'rebuild');
|
||||
if (!queued.ok) throw new Error(queued.error);
|
||||
const work = processLifecycle();
|
||||
await vi.waitFor(() => expect(resolveCount).toBeTypeOf('function'));
|
||||
await attachBeta();
|
||||
resolveCount(0);
|
||||
await work;
|
||||
expect(getDeployment(queued.deploymentId)?.deployment).toMatchObject({ state: 'waiting', stage: expect.stringMatching(/beta/i) });
|
||||
await expectServerPreserved();
|
||||
});
|
||||
|
||||
it('preserves the cleanup checkpoint and files while an active beta blocks a legacy cleanup resume', async () => {
|
||||
const { destroyServer, processDestructions } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { deploymentStage, finishDeployment, resumeCleanup, getDeployment, REMOVING_SERVER } = await import('../../src/lib/server/fleet/deployments.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const queued = destroyServer(server);
|
||||
deploymentStage(queued.deploymentId!, REMOVING_SERVER);
|
||||
finishDeployment(queued.deploymentId!, { ok: false, message: 'Interrupted local cleanup' });
|
||||
const id = await attachBeta();
|
||||
expect(resumeCleanup(queued.deploymentId!)).toBe(true);
|
||||
await processDestructions();
|
||||
expect(getDeployment(queued.deploymentId!)?.deployment).toMatchObject({ state: 'failed', stage: REMOVING_SERVER,
|
||||
error: expect.stringMatching(/beta/i) });
|
||||
expect(getDeployment(queued.deploymentId!)?.canResumeCleanup).toBe(true);
|
||||
await expectServerPreserved();
|
||||
db().prepare("UPDATE betas SET state = 'promoted' WHERE id = ?").run(id);
|
||||
expect(resumeCleanup(queued.deploymentId!)).toBe(true);
|
||||
await processDestructions();
|
||||
expect(getDeployment(queued.deploymentId!)?.deployment).toMatchObject({ state: 'done', server_exists: 0 });
|
||||
const { komodoDestroyDeployment, komodoDeleteDeployment } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
expect(komodoDestroyDeployment).not.toHaveBeenCalled();
|
||||
expect(komodoDeleteDeployment).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(['destroy', 'rebuild'] as const)('requires cancelling a pending %s before opening a beta', async action => {
|
||||
const { destroyServer } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { requestLifecycle } = await import('../../src/lib/server/fleet/lifecycle.js');
|
||||
@@ -222,7 +131,7 @@ describe('active beta destruction guard', () => {
|
||||
});
|
||||
|
||||
it.each((['destroy', 'rebuild'] as const).flatMap(action => ['failed', 'interrupted'].flatMap(state =>
|
||||
['Stopping and removing container', 'Deleting Komodo deployment', 'Removing server files and registry'].map(stage => ({ action, state, stage }))
|
||||
['Stopping and removing container', 'Removing server files and registry'].map(stage => ({ action, state, stage }))
|
||||
)))('rejects beta admission after $action is $state at $stage', async ({ action, state, stage }) => {
|
||||
const { beginDeployment } = await import('../../src/lib/server/fleet/deployments.js');
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
|
||||
Reference in New Issue
Block a user