feat: add provisioning lifecycle actions

Rename Deployments to Provisioning with compatible redirects and APIs. Add durable Start, Restart, Stop, and Rebuild operations alongside Create and Destroy; rebuild wipes the instance and recreates it in the same slot and port.

Add lifecycle, migration, recovery, and Komodo command tests. Type checks and production build pass; the full suite retains a telemetry assertion failure reproduced on the unchanged branch.

Refs #39
This commit is contained in:
2026-09-27 14:20:04 -04:00
parent 0b0e5b5bdb
commit cfed584614
42 changed files with 886 additions and 108 deletions
+36
View File
@@ -0,0 +1,36 @@
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
vi.mock('../../src/lib/server/fleet/servers.js', () => ({ getServer: vi.fn() }));
beforeEach(() => {
vi.resetModules(); vi.clearAllMocks();
vi.stubEnv('KOMODO_ADDRESS', 'http://komodo.test');
vi.stubEnv('KOMODO_API_KEY', 'test'); vi.stubEnv('KOMODO_API_SECRET', 'test');
vi.stubGlobal('fetch', vi.fn(async () => Response.json({ success: true })));
});
afterEach(() => { vi.unstubAllGlobals(); vi.unstubAllEnvs(); });
it.each(['start', 'stop'] as const)('sends %s to the saved resource for each Komodo type', async action => {
const { getServer } = await import('../../src/lib/server/fleet/servers.js');
const { komodoRuntimeAction } = await import('../../src/lib/server/integrations/komodo.js');
const verb = action === 'start' ? 'Start' : 'Stop';
for (const [type, suffix, body] of [
['deployment', 'Deployment', { deployment: 'saved-resource' }],
['stack', 'Stack', { stack: 'saved-resource' }],
['container', 'Container', { server: 'host', container: 'saved-resource' }]
] as const) {
vi.mocked(getServer).mockReturnValue({ komodo_type: type, komodo_name: 'saved-resource', komodo_server: 'host' } as ReturnType<typeof getServer>);
expect(await komodoRuntimeAction('ec-server-id', action)).toEqual({ ok: true });
expect(fetch).toHaveBeenLastCalledWith(`http://komodo.test/execute/${verb}${suffix}`, expect.objectContaining({ method: 'POST', body: JSON.stringify(body) }));
}
});
it('rejects missing resource identity and surfaces a rejected stop response', async () => {
const { getServer } = await import('../../src/lib/server/fleet/servers.js');
const { komodoRuntimeAction } = await import('../../src/lib/server/integrations/komodo.js');
vi.mocked(getServer).mockReturnValue(undefined);
expect(await komodoRuntimeAction('missing', 'stop')).toMatchObject({ ok: false, kind: 'no-name' });
expect(fetch).not.toHaveBeenCalled();
vi.mocked(getServer).mockReturnValue({ komodo_type: 'deployment', komodo_name: 'saved' } as ReturnType<typeof getServer>);
vi.mocked(fetch).mockResolvedValueOnce(Response.json({ success: false, message: 'rejected' }));
expect(await komodoRuntimeAction('server', 'stop')).toMatchObject({ ok: false, message: 'rejected' });
});