import { describe, expect, it } from 'vitest'; import path from 'node:path'; import { buildProfileMigrationCommand, buildProcessDefinitions, buildWorkspaceCommands, planProfileReconcile, } from '../src/orchestrator/gatewayOrchestrator.js'; import { sanitizeManagedProcessEnv } from '../src/orchestrator/processManager.js'; import type { GatewayProfileRecord } from '../src/orchestrator/profileRepository.js'; const buildProfile = (buildWorkspace?: string): GatewayProfileRecord => ({ profileName: 'che:2', profile: 'che', scenario: '2', apiPort: 15003, status: 'RUNNING', buildStatus: 'SUCCEEDED', buildCommitSha: '0123456789abcdef0123456789abcdef01234567', buildWorkspace, meta: {}, createdAt: '2026-07-25T00:00:00.000Z', updatedAt: '2026-07-25T00:00:00.000Z', }); describe('planProfileReconcile', () => { it('starts missing processes for running profiles', () => { expect( planProfileReconcile('RUNNING', { frontendRunning: true, apiRunning: true, daemonRunning: false, auctionRunning: true, battleSimRunning: true, tournamentRunning: true, }) ).toEqual({ shouldStart: true, shouldStop: false }); }); it('starts processes for preopen profiles', () => { expect( planProfileReconcile('PREOPEN', { frontendRunning: false, apiRunning: false, daemonRunning: false, auctionRunning: false, battleSimRunning: false, tournamentRunning: false, }) ).toEqual({ shouldStart: true, shouldStop: false }); }); it('does nothing when running profile is healthy', () => { expect( planProfileReconcile('RUNNING', { frontendRunning: true, apiRunning: true, daemonRunning: true, auctionRunning: true, battleSimRunning: true, tournamentRunning: true, }) ).toEqual({ shouldStart: false, shouldStop: false }); }); it('restarts a running profile when only the auction worker is missing', () => { expect( planProfileReconcile('RUNNING', { frontendRunning: true, apiRunning: true, daemonRunning: true, auctionRunning: false, battleSimRunning: true, tournamentRunning: true, }) ).toEqual({ shouldStart: true, shouldStop: false }); }); it('stops processes for non-running profiles', () => { expect( planProfileReconcile('STOPPED', { frontendRunning: false, apiRunning: false, daemonRunning: true, auctionRunning: false, battleSimRunning: false, tournamentRunning: false, }) ).toEqual({ shouldStart: false, shouldStop: true }); }); it('keeps reserved profiles off', () => { expect( planProfileReconcile('RESERVED', { frontendRunning: false, apiRunning: false, daemonRunning: false, auctionRunning: false, battleSimRunning: false, tournamentRunning: false, }) ).toEqual({ shouldStart: false, shouldStop: false }); }); }); describe('buildProcessDefinitions', () => { const processConfig = { workspaceRoot: '/srv/sammo/main', redisKeyPrefix: 'sammo:gateway', gameTokenSecret: 'test-secret', gatewayInternalApiUrl: 'http://127.0.0.1:13000', }; it('runs a built profile from its commit worktree', () => { const buildWorkspace = '/srv/sammo/worktrees/0123456789abcdef'; const definitions = buildProcessDefinitions(buildProfile(buildWorkspace), processConfig); expect(Object.values(definitions)).toHaveLength(6); expect(new Set(Object.values(definitions).map((definition) => definition.name)).size).toBe(6); expect(definitions.frontend).toMatchObject({ cwd: path.join(buildWorkspace, 'app', 'game-frontend'), script: path.join(buildWorkspace, 'app', 'game-frontend', 'node_modules', 'vite', 'bin', 'vite.js'), args: [ 'preview', '--host', '0.0.0.0', '--port', '15002', '--outDir', path.join(buildWorkspace, '.release-dist', 'che_2', 'game-frontend'), ], }); expect(definitions.api.cwd).toBe(path.join(buildWorkspace, 'app', 'game-api')); expect(definitions.api.script).toBe(path.join(buildWorkspace, 'app', 'game-api', 'dist', 'index.js')); expect(definitions.api.env).toMatchObject({ GAME_PROFILE_NAME: 'che:2', GAME_TRPC_PATH: '/che/api/trpc', GAME_API_EVENTS_PATH: '/che/api/events', GATEWAY_INTERNAL_API_URL: 'http://127.0.0.1:13000', GAME_UPLOAD_PATH: '/che/api/uploads', }); expect(definitions.daemon.cwd).toBe(path.join(buildWorkspace, 'app', 'game-engine')); expect(definitions.daemon.script).toBe(path.join(buildWorkspace, 'app', 'game-engine', 'dist', 'index.js')); expect(definitions.auction).toMatchObject({ cwd: path.join(buildWorkspace, 'app', 'game-api'), script: path.join(buildWorkspace, 'app', 'game-api', 'dist', 'index.js'), env: { GAME_API_ROLE: 'auction-worker' }, }); expect(definitions.battleSim).toMatchObject({ cwd: path.join(buildWorkspace, 'app', 'game-api'), script: path.join(buildWorkspace, 'app', 'game-api', 'dist', 'index.js'), env: { GAME_API_ROLE: 'battle-sim-worker' }, }); expect(definitions.tournament).toMatchObject({ cwd: path.join(buildWorkspace, 'app', 'game-api'), script: path.join(buildWorkspace, 'app', 'game-api', 'dist', 'index.js'), env: { GAME_API_ROLE: 'tournament-worker' }, }); }); it('keeps main as the runtime for profiles without a commit worktree', () => { const definitions = buildProcessDefinitions(buildProfile(), processConfig); expect(definitions.frontend.cwd).toBe(path.join(processConfig.workspaceRoot, 'app', 'game-frontend')); expect(definitions.api.cwd).toBe(path.join(processConfig.workspaceRoot, 'app', 'game-api')); expect(definitions.daemon.cwd).toBe(path.join(processConfig.workspaceRoot, 'app', 'game-engine')); expect(definitions.auction.cwd).toBe(path.join(processConfig.workspaceRoot, 'app', 'game-api')); expect(definitions.battleSim.cwd).toBe(path.join(processConfig.workspaceRoot, 'app', 'game-api')); expect(definitions.tournament.cwd).toBe(path.join(processConfig.workspaceRoot, 'app', 'game-api')); }); it('does not forward PM2 identity or parent runtime roles to profile processes', () => { const definitions = buildProcessDefinitions(buildProfile(), { ...processConfig, baseEnv: { DATABASE_URL: 'postgresql://integration.invalid/sammo', VITE_APP_BASE_PATH: '/gateway', GATEWAY_ROLE: 'orchestrator', args: 'daemon', NODE_APP_INSTANCE: '2', name: 'sammo:gateway-orchestrator', pm_id: '2', pm_exec_path: '/srv/controller.js', }, }); for (const definition of Object.values(definitions)) { expect(definition.env).toMatchObject({ DATABASE_URL: 'postgresql://integration.invalid/sammo' }); expect(definition.env).not.toHaveProperty('pm_id'); expect(definition.env).not.toHaveProperty('args'); expect(definition.env).not.toHaveProperty('pm_exec_path'); expect(definition.env).not.toHaveProperty('name'); expect(definition.env).not.toHaveProperty('NODE_APP_INSTANCE'); expect(definition.env).not.toHaveProperty('GATEWAY_ROLE'); } expect(definitions.frontend.env.VITE_APP_BASE_PATH).toBe('/che'); }); }); describe('sanitizeManagedProcessEnv', () => { it('keeps application configuration while removing PM2 metadata and role selectors', () => { expect( sanitizeManagedProcessEnv({ DATABASE_URL: 'postgresql://integration.invalid/sammo', PATH: '/usr/local/bin:/usr/bin', GATEWAY_ROLE: 'orchestrator', GAME_API_ROLE: 'server', args: 'daemon', NODE_APP_INSTANCE: '2', name: 'sammo:gateway-orchestrator', pm_id: '2', pm_cwd: '/srv/controller', axm_monitor: '{}', }) ).toEqual({ DATABASE_URL: 'postgresql://integration.invalid/sammo', PATH: '/usr/local/bin:/usr/bin', }); }); }); describe('buildWorkspaceCommands', () => { it('installs and builds runtime dependencies before the profile processes', () => { const workspaceRoot = '/srv/sammo/worktrees/0123456789abcdef'; const commands = buildWorkspaceCommands(workspaceRoot, true); expect(commands.map(({ args }) => args)).toEqual([ ['install', '--frozen-lockfile'], ['--filter', '@sammo-ts/common', 'build'], ['--filter', '@sammo-ts/infra', 'prisma:generate'], ['--filter', '@sammo-ts/infra', 'build'], ['--filter', '@sammo-ts/logic', 'build'], ['--filter', '@sammo-ts/game-api', 'build'], ['--filter', '@sammo-ts/game-engine', 'build'], ['--filter', '@sammo-ts/gateway-api', 'build'], ]); expect(commands.every(({ cwd }) => cwd === workspaceRoot)).toBe(true); }); it('deploys the game schema migration after building the selected workspace', () => { const workspaceRoot = '/srv/sammo/worktrees/0123456789abcdef'; const databaseUrl = 'postgresql://integration.invalid/sammo?schema=che'; const command = buildProfileMigrationCommand(workspaceRoot, databaseUrl, { NODE_ENV: 'production' }); expect(command).toEqual({ command: 'pnpm', args: ['--filter', '@sammo-ts/infra', 'prisma:migrate:deploy:game'], cwd: workspaceRoot, env: { NODE_ENV: 'production', DATABASE_URL: databaseUrl, }, }); }); });