feat: add admin scenario operations
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { GatewayOrchestrator } from '../src/orchestrator/gatewayOrchestrator.js';
|
||||
import type { ProcessDefinition, ProcessManager } from '../src/orchestrator/processManager.js';
|
||||
import type {
|
||||
GatewayOperationRecord,
|
||||
GatewayOperationStatus,
|
||||
GatewayProfileRecord,
|
||||
GatewayProfileRepository,
|
||||
} from '../src/orchestrator/profileRepository.js';
|
||||
import { GitWorkspaceManager } from '../src/orchestrator/workspaceManager.js';
|
||||
|
||||
const profile: GatewayProfileRecord = {
|
||||
profileName: 'che:2',
|
||||
profile: 'che',
|
||||
scenario: '2',
|
||||
apiPort: 15003,
|
||||
status: 'STOPPED',
|
||||
buildStatus: 'SUCCEEDED',
|
||||
buildCommitSha: '0123456789abcdef0123456789abcdef01234567',
|
||||
buildWorkspace: '/srv/sammo/worktrees/0123456789abcdef',
|
||||
meta: {},
|
||||
createdAt: '2026-07-25T00:00:00.000Z',
|
||||
updatedAt: '2026-07-25T00:00:00.000Z',
|
||||
};
|
||||
|
||||
const buildOperation = (type: 'START' | 'STOP'): GatewayOperationRecord => ({
|
||||
id: '11111111-1111-4111-8111-111111111111',
|
||||
profileName: profile.profileName,
|
||||
type,
|
||||
status: 'RUNNING',
|
||||
payload: {},
|
||||
requestedBy: 'admin',
|
||||
createdAt: '2026-07-25T01:00:00.000Z',
|
||||
startedAt: '2026-07-25T01:00:00.000Z',
|
||||
updatedAt: '2026-07-25T01:00:00.000Z',
|
||||
});
|
||||
|
||||
const createHarness = (operation: GatewayOperationRecord, failStart = false, failStop = false) => {
|
||||
let nextOperation: GatewayOperationRecord | null = operation;
|
||||
const statuses: string[] = [];
|
||||
const completions: GatewayOperationStatus[] = [];
|
||||
const started: ProcessDefinition[] = [];
|
||||
const stopped: string[] = [];
|
||||
const deleted: string[] = [];
|
||||
|
||||
const repository: GatewayProfileRepository = {
|
||||
listProfiles: async () => [profile],
|
||||
getProfile: async () => profile,
|
||||
upsertProfile: async () => profile,
|
||||
updateScenario: async () => profile,
|
||||
updateStatus: async (_profileName, status) => {
|
||||
statuses.push(status);
|
||||
return { ...profile, status };
|
||||
},
|
||||
updateBuildStatus: async () => profile,
|
||||
updateMeta: async () => profile,
|
||||
listReservedToStart: async () => [],
|
||||
findQueuedBuild: async () => null,
|
||||
updateLastError: async () => {},
|
||||
updateWorkspaceUsage: async () => {},
|
||||
clearWorkspaceUsage: async () => {},
|
||||
listOperations: async () => [],
|
||||
getOperation: async () => operation,
|
||||
createOperation: async () => operation,
|
||||
claimNextOperation: async () => {
|
||||
const result = nextOperation;
|
||||
nextOperation = null;
|
||||
return result;
|
||||
},
|
||||
completeOperation: async (_id, status) => {
|
||||
completions.push(status);
|
||||
return { ...operation, status };
|
||||
},
|
||||
requeueOperation: async () => ({ ...operation, status: 'QUEUED' }),
|
||||
cancelOperation: async () => false,
|
||||
retryOperation: async () => null,
|
||||
};
|
||||
const processManager: ProcessManager = {
|
||||
list: async () => [],
|
||||
start: async (definition) => {
|
||||
if (failStart) {
|
||||
throw new Error('pm2 unavailable');
|
||||
}
|
||||
started.push(definition);
|
||||
},
|
||||
stop: async (name) => {
|
||||
stopped.push(name);
|
||||
if (failStop) {
|
||||
throw new Error('pm2 stop failed');
|
||||
}
|
||||
},
|
||||
delete: async (name) => {
|
||||
deleted.push(name);
|
||||
if (failStop) {
|
||||
throw new Error('pm2 delete failed');
|
||||
}
|
||||
},
|
||||
};
|
||||
const orchestrator = new GatewayOrchestrator({
|
||||
repository,
|
||||
processManager,
|
||||
buildRunner: {
|
||||
run: async () => ({ ok: true, exitCode: 0, output: '' }),
|
||||
},
|
||||
workspaceManager: new GitWorkspaceManager({
|
||||
repoRoot: '/tmp/not-used',
|
||||
worktreeRoot: '/tmp/not-used-worktrees',
|
||||
}),
|
||||
processConfig: {
|
||||
workspaceRoot: '/srv/sammo',
|
||||
redisKeyPrefix: 'sammo:test',
|
||||
gameTokenSecret: 'test-secret',
|
||||
},
|
||||
reconcileIntervalMs: 60_000,
|
||||
scheduleIntervalMs: 60_000,
|
||||
buildIntervalMs: 60_000,
|
||||
adminActionIntervalMs: 60_000,
|
||||
});
|
||||
|
||||
return { orchestrator, statuses, completions, started, stopped, deleted };
|
||||
};
|
||||
|
||||
describe('GatewayOrchestrator first-class operations', () => {
|
||||
it('starts both profile processes and records success', async () => {
|
||||
const harness = createHarness(buildOperation('START'));
|
||||
|
||||
await harness.orchestrator.runOperationsNow();
|
||||
|
||||
expect(harness.statuses).toEqual(['RUNNING']);
|
||||
expect(harness.started.map((definition) => definition.name)).toEqual([
|
||||
'sammo:che:2:game-api',
|
||||
'sammo:che:2:turn-daemon',
|
||||
]);
|
||||
expect(harness.completions).toEqual(['SUCCEEDED']);
|
||||
});
|
||||
|
||||
it('stops both profile processes and records success', async () => {
|
||||
const harness = createHarness(buildOperation('STOP'));
|
||||
|
||||
await harness.orchestrator.runOperationsNow();
|
||||
|
||||
expect(harness.statuses).toEqual(['STOPPED']);
|
||||
expect(harness.stopped).toEqual(['sammo:che:2:game-api', 'sammo:che:2:turn-daemon']);
|
||||
expect(harness.completions).toEqual(['SUCCEEDED']);
|
||||
});
|
||||
|
||||
it('records a failed start instead of reporting a false success', async () => {
|
||||
const harness = createHarness(buildOperation('START'), true);
|
||||
|
||||
await harness.orchestrator.runOperationsNow();
|
||||
|
||||
expect(harness.completions).toEqual(['FAILED']);
|
||||
});
|
||||
|
||||
it('attempts to stop both roles before reporting a partial PM2 failure', async () => {
|
||||
const harness = createHarness(buildOperation('STOP'), false, true);
|
||||
|
||||
await harness.orchestrator.runOperationsNow();
|
||||
|
||||
expect(harness.stopped).toEqual(['sammo:che:2:game-api', 'sammo:che:2:turn-daemon']);
|
||||
expect(harness.deleted).toEqual(['sammo:che:2:game-api', 'sammo:che:2:turn-daemon']);
|
||||
expect(harness.completions).toEqual(['FAILED']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user