new destruction flow
This commit is contained in:
@@ -13,6 +13,7 @@ vi.mock('../../src/lib/server/integrations/komodo.js', () => ({
|
||||
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) }));
|
||||
vi.mock('../../src/lib/server/pipeline/worker.js', () => ({ kick: vi.fn(async () => {}) }));
|
||||
|
||||
let fixture: Fixture;
|
||||
@@ -53,8 +54,69 @@ async function promotedConfig() {
|
||||
}
|
||||
|
||||
describe('server creation map selection', () => {
|
||||
it('returns a deployment URL identifier immediately and serves its lifecycle detail', async () => {
|
||||
const { createServer } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { DELETE } = await import('../../src/routes/api/servers/[name]/+server.js');
|
||||
const { GET } = await import('../../src/routes/api/deployments/[id]/+server.js');
|
||||
const { komodoDestroyDeployment } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
await createServer(input);
|
||||
const response = await DELETE({ params: { name: serverName } });
|
||||
expect(response.status).toBe(202);
|
||||
const body = await response.json();
|
||||
expect(body).toMatchObject({ ok: true, deploymentId: expect.any(String) });
|
||||
expect(komodoDestroyDeployment).not.toHaveBeenCalled();
|
||||
const detail = await (await GET({ params: { id: body.deploymentId } })).json();
|
||||
expect(detail.deployment).toMatchObject({ server: serverName, action: 'destroy', state: 'waiting' });
|
||||
expect(detail.events).toHaveLength(2);
|
||||
const missing = await DELETE({ params: { name: 'missing-server' } });
|
||||
expect(missing.status).toBe(404);
|
||||
});
|
||||
|
||||
it('queues destruction, waits for live zero players, and preserves an ordered timeline', async () => {
|
||||
const { createServer, destroyServer, processDestructions } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { rconGetPlayerCount } = await import('../../src/lib/server/integrations/rcon.js');
|
||||
const { komodoDestroyDeployment, komodoInspectHealthy } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
const { getDeployment, interruptDeployments } = await import('../../src/lib/server/fleet/deployments.js');
|
||||
const { closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
await createServer(input);
|
||||
const queued = destroyServer(serverName);
|
||||
expect(queued).toMatchObject({ ok: true, deploymentId: expect.any(String) });
|
||||
expect(destroyServer(serverName).deploymentId).toBe(queued.deploymentId);
|
||||
expect(komodoDestroyDeployment).not.toHaveBeenCalled();
|
||||
vi.mocked(rconGetPlayerCount).mockResolvedValueOnce(3).mockResolvedValueOnce(null).mockRejectedValueOnce(new Error('RCON unavailable'));
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await processDestructions();
|
||||
expect(komodoDestroyDeployment).not.toHaveBeenCalled();
|
||||
expect(getDeployment(queued.deploymentId!)?.deployment).toMatchObject({ state: 'waiting' });
|
||||
}
|
||||
closeDb();
|
||||
interruptDeployments();
|
||||
expect(getDeployment(queued.deploymentId!)?.deployment).toMatchObject({ state: 'waiting' });
|
||||
vi.mocked(komodoInspectHealthy).mockResolvedValueOnce({ ok: false, reason: 'gone' });
|
||||
await Promise.all([processDestructions(), processDestructions()]);
|
||||
expect(komodoDestroyDeployment).toHaveBeenCalledTimes(1);
|
||||
const detail = getDeployment(queued.deploymentId!);
|
||||
expect(detail?.deployment).toMatchObject({ state: 'done', server_exists: 0 });
|
||||
const messages = (detail?.events as { message: string }[]).map(e => e.message);
|
||||
expect(messages).toEqual(expect.arrayContaining(['Server destruction requested', 'Waiting for server to empty: 3 player(s) online', 'Server is empty (0 players verified)', 'Stopping and removing container', 'Completed']));
|
||||
expect(messages.indexOf('Server is empty (0 players verified)')).toBeLessThan(messages.indexOf('Stopping and removing container'));
|
||||
});
|
||||
|
||||
it('preserves files and registry when Komodo rejects destruction and exposes failure details', async () => {
|
||||
const { createServer, destroyServer, processDestructions } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { komodoDestroyDeployment, komodoDeleteDeployment } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
const { getDeployment } = await import('../../src/lib/server/fleet/deployments.js');
|
||||
await createServer(input);
|
||||
vi.mocked(komodoDestroyDeployment).mockResolvedValueOnce({ ok: false, kind: 'http-error', message: 'denied' });
|
||||
const queued = destroyServer(serverName);
|
||||
await processDestructions();
|
||||
expect(komodoDeleteDeployment).not.toHaveBeenCalled();
|
||||
expect(existsSync(join(fixture.t5, serverName))).toBe(true);
|
||||
expect(getDeployment(queued.deploymentId!)?.deployment).toMatchObject({ state: 'failed', server_exists: 1, error: 'DestroyDeployment failed: denied' });
|
||||
});
|
||||
|
||||
it('records in-flight creation and retains both operations after destruction', async () => {
|
||||
const { createServer, destroyServer } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { createServer, destroyServer, processDestructions } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { listDeployments } = await import('../../src/lib/server/fleet/deployments.js');
|
||||
const { komodoDeploy, komodoInspectHealthy, komodoDestroyDeployment } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
vi.mocked(komodoDeploy).mockImplementationOnce(async () => {
|
||||
@@ -68,6 +130,7 @@ describe('server creation map selection', () => {
|
||||
return { ok: true };
|
||||
});
|
||||
expect((await destroyServer(serverName)).ok).toBe(true);
|
||||
await processDestructions();
|
||||
expect(listDeployments()).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ server: serverName, action: 'create', state: 'done', server_exists: 0 }),
|
||||
expect.objectContaining({ server: serverName, action: 'destroy', state: 'done', server_exists: 0 })
|
||||
@@ -198,7 +261,7 @@ describe('server creation map selection', () => {
|
||||
|
||||
describe('persistent server keys', () => {
|
||||
it('saves a key, retains it through destruction, and reuses it for the exact lobby', async () => {
|
||||
const { createServer, destroyServer } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { createServer, destroyServer, processDestructions } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { getServerKey, listServerKeys } = await import('../../src/lib/server/fleet/server-keys.js');
|
||||
const { komodoCreateDeployment, komodoInspectHealthy } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
vi.mocked(komodoInspectHealthy).mockResolvedValue({ ok: false, kind: 'not-found', message: 'gone' } as never);
|
||||
@@ -207,6 +270,7 @@ describe('persistent server keys', () => {
|
||||
expect(await createServer(input)).toMatchObject({ ok: true, name });
|
||||
expect(getServerKey(name)).toBe('test-key');
|
||||
expect(await destroyServer(name)).toMatchObject({ ok: true });
|
||||
await processDestructions();
|
||||
expect(listServerKeys()).toContainEqual(expect.objectContaining({ server_id: name, deployed: 0 }));
|
||||
expect(JSON.stringify(listServerKeys())).not.toContain('test-key');
|
||||
expect(await createServer({ gameMode: 'zombie', mapSlug: 'cosmodrome', expectedName: name })).toMatchObject({ ok: true, name });
|
||||
@@ -231,13 +295,14 @@ describe('persistent server keys', () => {
|
||||
});
|
||||
|
||||
it('fills the lowest gap and rejects stale previews before saving a key or deploying', async () => {
|
||||
const { createServer, destroyServer } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { createServer, destroyServer, processDestructions } = await import('../../src/lib/server/fleet/create.js');
|
||||
const { getServerKey } = await import('../../src/lib/server/fleet/server-keys.js');
|
||||
const { komodoCreateDeployment, komodoInspectHealthy } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
vi.mocked(komodoInspectHealthy).mockResolvedValue({ ok: false } as never);
|
||||
expect(await createServer(input)).toMatchObject({ ok: true, name: serverName });
|
||||
expect(await createServer(input)).toMatchObject({ ok: true, name: 't5_zombie_cosmodrome_2' });
|
||||
await destroyServer(serverName);
|
||||
await processDestructions();
|
||||
expect(await createServer({ ...input, plutoKey: 'wrong-key', expectedName: 't5_zombie_cosmodrome_3' }))
|
||||
.toMatchObject({ ok: false, kind: 'no-slot' });
|
||||
expect(getServerKey(serverName)).toBe('test-key');
|
||||
|
||||
Reference in New Issue
Block a user