diff --git a/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts b/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts index 1e10e98..2ba32d4 100644 --- a/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts +++ b/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts @@ -963,8 +963,12 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle { private async stopProfile(profile: GatewayProfileRecord): Promise { const apiName = buildProcessName(profile.profileName, 'api'); const daemonName = buildProcessName(profile.profileName, 'daemon'); + const existingNames = new Set((await this.processManager.list()).map((process) => process.name)); const failures: string[] = []; for (const name of [apiName, daemonName]) { + if (!existingNames.has(name)) { + continue; + } try { await this.processManager.stop(name); } catch { diff --git a/app/gateway-api/test/orchestratorOperations.test.ts b/app/gateway-api/test/orchestratorOperations.test.ts index d539395..de3b780 100644 --- a/app/gateway-api/test/orchestratorOperations.test.ts +++ b/app/gateway-api/test/orchestratorOperations.test.ts @@ -36,7 +36,12 @@ const buildOperation = (type: 'START' | 'STOP'): GatewayOperationRecord => ({ updatedAt: '2026-07-25T01:00:00.000Z', }); -const createHarness = (operation: GatewayOperationRecord, failStart = false, failStop = false) => { +const createHarness = ( + operation: GatewayOperationRecord, + failStart = false, + failStop = false, + processesPresent = true +) => { let nextOperation: GatewayOperationRecord | null = operation; const statuses: string[] = []; const completions: GatewayOperationStatus[] = []; @@ -77,7 +82,13 @@ const createHarness = (operation: GatewayOperationRecord, failStart = false, fai retryOperation: async () => null, }; const processManager: ProcessManager = { - list: async () => [], + list: async () => + processesPresent + ? [ + { name: 'sammo:che:2:game-api', status: 'online' }, + { name: 'sammo:che:2:turn-daemon', status: 'online' }, + ] + : [], start: async (definition) => { if (failStart) { throw new Error('pm2 unavailable'); @@ -145,6 +156,17 @@ describe('GatewayOrchestrator first-class operations', () => { expect(harness.completions).toEqual(['SUCCEEDED']); }); + it('treats an already stopped profile as a successful idempotent stop', async () => { + const harness = createHarness(buildOperation('STOP'), false, false, false); + + await harness.orchestrator.runOperationsNow(); + + expect(harness.statuses).toEqual(['STOPPED']); + expect(harness.stopped).toEqual([]); + expect(harness.deleted).toEqual([]); + expect(harness.completions).toEqual(['SUCCEEDED']); + }); + it('records a failed start instead of reporting a false success', async () => { const harness = createHarness(buildOperation('START'), true);