Pace collection startups and clarify deployment queue progress

This commit is contained in:
2026-09-19 20:33:08 -04:00
parent 1d073779fe
commit f171889dfb
18 changed files with 693 additions and 81 deletions
+27 -6
View File
@@ -34,6 +34,13 @@ beforeEach(async () => {
vi.stubEnv('KOMODO_API_SECRET', 'test');
vi.stubEnv('KOMODO_TARGET_SERVER', 'test-host');
vi.stubEnv('RCON_PASSWORD', 'test-rcon');
const { komodoInspectHealthy } = await import('../../src/lib/server/integrations/komodo.js');
const { rconStatusResult, rconGetPlayerCount } = await import('../../src/lib/server/integrations/rcon.js');
vi.mocked(komodoInspectHealthy).mockReset().mockResolvedValue({ ok: true, state: 'running' });
vi.mocked(rconStatusResult).mockReset().mockResolvedValue({ ok: true,
endpoint: { host: 'localhost', port: 27016 }, rawReply: '',
status: { players: 0, map: 'zombie_cosmodrome', gametype: null, roster: [] } });
vi.mocked(rconGetPlayerCount).mockReset().mockResolvedValue(0);
});
afterEach(async () => {
@@ -48,7 +55,10 @@ describe('destruction cleanup recovery', () => {
const { db } = await import('../../src/lib/server/db/index.js');
const { getDeployment } = await import('../../src/lib/server/fleet/deployments.js');
const { komodoInspectHealthy } = await import('../../src/lib/server/integrations/komodo.js');
vi.mocked(komodoInspectHealthy).mockResolvedValue({ ok: false, reason: 'gone' });
const { rconStatusResult } = await import('../../src/lib/server/integrations/rcon.js');
vi.mocked(rconStatusResult).mockResolvedValueOnce({ ok: true,
endpoint: { host: 'localhost', port: 28960 }, rawReply: '',
status: { players: 0, map: 'zm_transit', gametype: null, roster: [] } });
const name = 't6_zombie_town_1';
expect(await createServer({ ...input, game: 't6', mapSlug: 'town' })).toMatchObject({ ok: true, name });
expect(await createServer(input)).toMatchObject({ ok: true });
@@ -61,6 +71,7 @@ describe('destruction cleanup recovery', () => {
db().prepare("INSERT INTO beta_file_changes (beta_id, server, rel_path) VALUES (?, ?, 'test.gsc')").run(beta, server);
}
const queued = destroyServer(name);
vi.mocked(komodoInspectHealthy).mockResolvedValue({ ok: false, reason: 'gone' });
await processDestructions();
expect(getDeployment(queued.deploymentId!)?.deployment).toMatchObject({ state: 'done', server_exists: 0 });
for (const table of ['beta_test_runs', 'beta_test_coverage', 'beta_file_changes']) {
@@ -269,6 +280,12 @@ describe('T6 Zombies provisioning', () => {
vi.mocked(rconStatusResult).mockImplementation(async (server) => ({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: '',
status: { players: 0, map: server.startsWith('t6_') ? 'zm_transit' : 'zombie_cosmodrome', gametype: null, roster: [] } }));
await processCreations();
expect(getDeployment(t5.deploymentId)?.deployment).toMatchObject({ state: 'awaiting-health' });
expect(getDeployment(t6.deploymentId)?.deployment).toMatchObject({ state: 'queued' });
await processCreations();
expect(getDeployment(t5.deploymentId)?.deployment).toMatchObject({ state: 'done' });
expect(getDeployment(t6.deploymentId)?.deployment).toMatchObject({ state: 'awaiting-health' });
await processCreations();
expect(getDeployment(t6.deploymentId)?.deployment).toMatchObject({ state: 'done' });
const content = await readFile(join(fixture.t6, t6.name, 'configs/electric_cherry_map.cfg'), 'utf8');
expect(content).toContain('execgts zm_standard_town.cfg map zm_transit');
@@ -340,10 +357,13 @@ describe('server creation map selection', () => {
closeDb();
interruptDeployments();
expect(getDeployment(body.deploymentId)?.deployment).toMatchObject({ state: 'queued' });
vi.mocked(komodoInspectHealthy).mockResolvedValueOnce({ ok: false, reason: 'starting' }).mockResolvedValueOnce({ ok: false, reason: 'starting' });
await Promise.all([processCreations(), processCreations()]);
expect(komodoCreateDeployment).toHaveBeenCalledTimes(2);
expect(komodoCreateDeployment).toHaveBeenCalledTimes(1);
expect(getDeployment(body.deploymentId)?.deployment).toMatchObject({ state: 'awaiting-health', finished_at: null });
if (second.ok) expect(getDeployment(second.deploymentId)?.deployment).toMatchObject({ state: 'queued' });
vi.mocked(komodoInspectHealthy).mockResolvedValueOnce({ ok: false, reason: 'starting' });
await processCreations();
expect(komodoCreateDeployment).toHaveBeenCalledTimes(1);
});
it('completes creation only when Komodo, RCON, and the selected map all pass', async () => {
@@ -469,7 +489,8 @@ describe('server creation map selection', () => {
expect(listDeployments()).toContainEqual(expect.objectContaining({ state: 'failed', error: 'Deploy: test failure' }));
vi.mocked(komodoDeploy).mockRejectedValueOnce(new Error('connection lost'));
await expect(createServer(input)).rejects.toThrow('connection lost');
expect(listDeployments()).toContainEqual(expect.objectContaining({ state: 'failed', error: 'connection lost' }));
expect(listDeployments()).toContainEqual(expect.objectContaining({ state: 'awaiting-health',
stage: 'Verifying existing server after interrupted startup', finished_at: null }));
const id = beginDeployment('interrupted-server', 'destroy');
interruptDeployments();
expect(listDeployments()).toContainEqual(expect.objectContaining({ id, state: 'interrupted', finished_at: expect.any(String) }));
@@ -587,12 +608,12 @@ describe('persistent server keys', () => {
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);
const name = 't5_zombie_cosmodrome_2';
expect(await createServer(input)).toMatchObject({ ok: true, name: serverName });
expect(await createServer(input)).toMatchObject({ ok: true, name });
expect(getServerKey(name)).toBe('test-key');
expect(await destroyServer(name)).toMatchObject({ ok: true });
vi.mocked(komodoInspectHealthy).mockResolvedValue({ ok: false, reason: 'gone' });
await processDestructions();
expect(listServerKeys()).toContainEqual(expect.objectContaining({ server_id: name, deployed: 0 }));
expect(JSON.stringify(listServerKeys())).not.toContain('test-key');
@@ -621,10 +642,10 @@ describe('persistent server keys', () => {
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);
vi.mocked(komodoInspectHealthy).mockResolvedValue({ ok: false, reason: 'gone' });
await processDestructions();
expect(await createServer({ ...input, plutoKey: 'wrong-key', expectedName: 't5_zombie_cosmodrome_3' }))
.toMatchObject({ ok: false, kind: 'no-slot' });