Make profile shutdown idempotent

This commit is contained in:
2026-07-25 13:55:20 +00:00
parent 74b2db2420
commit e6cedf3b37
2 changed files with 28 additions and 2 deletions
@@ -963,8 +963,12 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
private async stopProfile(profile: GatewayProfileRecord): Promise<void> {
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 {
@@ -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);