test: harden exhaustive compatibility suites
This commit is contained in:
@@ -76,10 +76,15 @@ export const runReferenceTurnCommandTrace = (workspaceRoot: string, fixturePath:
|
||||
if (resolvedFixture !== fixtureRoot && !resolvedFixture.startsWith(`${fixtureRoot}${path.sep}`)) {
|
||||
throw new Error(`Reference turn fixture must be under ${fixtureRoot}`);
|
||||
}
|
||||
const stdout = execFileSync('./scripts/run-turn-differential-case.sh', [resolvedFixture], {
|
||||
const runner = process.env.TURN_DIFFERENTIAL_CASE_SCRIPT ?? './scripts/run-turn-differential-case.sh';
|
||||
const stdout = execFileSync(runner, [resolvedFixture], {
|
||||
cwd: stackDirectory,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
TURN_DIFFERENTIAL_STACK_DIR: stackDirectory,
|
||||
},
|
||||
});
|
||||
return withProjectedTraceMeta(JSON.parse(stdout) as CanonicalTurnCommandTrace);
|
||||
};
|
||||
@@ -89,7 +94,7 @@ export const runReferenceTurnCommandTraceRequest = (
|
||||
request: Record<string, unknown>
|
||||
): CanonicalTurnCommandTrace => {
|
||||
const stackDirectory = path.join(workspaceRoot, 'docker_compose_files/reference');
|
||||
const runner = process.env.TURN_DIFFERENTIAL_RUNNER_SCRIPT ?? './scripts/run-turn-differential-case.sh';
|
||||
const runner = process.env.TURN_DIFFERENTIAL_CASE_SCRIPT ?? './scripts/run-turn-differential-case.sh';
|
||||
const stdout = execFileSync(runner, ['-'], {
|
||||
cwd: stackDirectory,
|
||||
input: JSON.stringify(request),
|
||||
|
||||
@@ -276,6 +276,12 @@ describe('auction integration flow', () => {
|
||||
apiPort: Number(process.env.GAME_API_PORT ?? 14000),
|
||||
status: 'RUNNING',
|
||||
});
|
||||
await gatewayClient.admin.profiles.updateMeta.mutate({
|
||||
profileName: 'che:908',
|
||||
patch: {
|
||||
localAccountGeneralCreationGraceDays: 7,
|
||||
},
|
||||
});
|
||||
|
||||
await gatewayClient.admin.profiles.installNow.mutate({
|
||||
profileName: 'che:908',
|
||||
|
||||
@@ -200,7 +200,8 @@ const assertTraceParity = (
|
||||
}
|
||||
};
|
||||
|
||||
const workspaceRoot = findWorkspaceRoot(process.cwd());
|
||||
const configuredWorkspaceRoot = process.env.TURN_DIFFERENTIAL_WORKSPACE_ROOT;
|
||||
const workspaceRoot = configuredWorkspaceRoot ?? findWorkspaceRoot(process.cwd());
|
||||
const describeWithReference = workspaceRoot ? describe : describe.skip;
|
||||
|
||||
describeWithReference('ref ↔ core2026 battle differential', () => {
|
||||
|
||||
@@ -240,6 +240,12 @@ describe('integration initialization flow', () => {
|
||||
apiPort: Number(process.env.GAME_API_PORT ?? 14000),
|
||||
status: 'RUNNING',
|
||||
});
|
||||
await gatewayClient.admin.profiles.updateMeta.mutate({
|
||||
profileName: 'che:2',
|
||||
patch: {
|
||||
localAccountGeneralCreationGraceDays: 7,
|
||||
},
|
||||
});
|
||||
|
||||
await gatewayClient.admin.profiles.installNow.mutate({
|
||||
profileName: 'che:2',
|
||||
@@ -358,7 +364,7 @@ describe('integration initialization flow', () => {
|
||||
action: 'che_건국',
|
||||
args: {
|
||||
nationName: `TestNation${idx + 1}`,
|
||||
nationType: 'che_def',
|
||||
nationType: 'che_명가',
|
||||
colorType: idx,
|
||||
},
|
||||
expectedRevision: queueRevision,
|
||||
@@ -462,10 +468,9 @@ describe('integration initialization flow', () => {
|
||||
const nation = nationRows.find((row) => row.id === nationId);
|
||||
expect(nation).toBeTruthy();
|
||||
const count = generalCountMap.get(nationId) ?? 0;
|
||||
if (count >= 2) {
|
||||
if (nation!.capitalCityId !== null) {
|
||||
expect(cityNationMap.get(nation!.capitalCityId)).toBe(nationId);
|
||||
}
|
||||
expect(count).toBeGreaterThanOrEqual(1);
|
||||
if ((nation!.capitalCityId ?? 0) > 0) {
|
||||
expect(cityNationMap.get(nation!.capitalCityId!)).toBe(nationId);
|
||||
} else {
|
||||
expect(nation!.level).toBe(0);
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ integration('legacy instant diplomacy responses', () => {
|
||||
expect(addedLogs(trace).map((log) => [log.generalId, log.category])).toEqual([
|
||||
[1, 'history'],
|
||||
[1, 'action'],
|
||||
[0, 'history'],
|
||||
[0, 'summary'],
|
||||
[2, 'history'],
|
||||
[2, 'action'],
|
||||
]);
|
||||
@@ -130,7 +130,7 @@ integration('legacy instant diplomacy responses', () => {
|
||||
expect(addedLogs(trace).map((log) => [log.generalId, log.category])).toEqual([
|
||||
[1, 'history'],
|
||||
[1, 'action'],
|
||||
[0, 'history'],
|
||||
[0, 'summary'],
|
||||
[2, 'history'],
|
||||
[2, 'action'],
|
||||
]);
|
||||
|
||||
@@ -2,6 +2,7 @@ import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import os from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFile } from 'node:child_process';
|
||||
|
||||
@@ -173,8 +174,19 @@ const createGameClient = (baseUrl: string, trpcPath: string, accessTokenRef: { v
|
||||
],
|
||||
});
|
||||
|
||||
const buildProcessName = (profileName: string, role: 'api' | 'daemon' | 'tournament'): string =>
|
||||
`sammo:${profileName}:${role === 'api' ? 'game-api' : role === 'daemon' ? 'turn-daemon' : 'tournament-worker'}`;
|
||||
const buildProcessName = (
|
||||
profileName: string,
|
||||
role: 'api' | 'daemon' | 'auction' | 'battle' | 'tournament'
|
||||
): string => {
|
||||
const roleName = {
|
||||
api: 'game-api',
|
||||
daemon: 'turn-daemon',
|
||||
auction: 'auction-worker',
|
||||
battle: 'battle-sim-worker',
|
||||
tournament: 'tournament-worker',
|
||||
}[role];
|
||||
return `sammo:${profileName}:${roleName}`;
|
||||
};
|
||||
|
||||
const waitForPm2Online = async (manager: Pm2ProcessManager, names: string[], timeoutMs = 30_000) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
@@ -204,6 +216,55 @@ const cleanupPm2 = async (manager: Pm2ProcessManager, names: string[]) => {
|
||||
}
|
||||
};
|
||||
|
||||
const processIsAlive = (pid: number): boolean => {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const stopDedicatedPm2Daemon = async (pm2Home: string): Promise<void> => {
|
||||
let daemonPid: number | null = null;
|
||||
try {
|
||||
const rawPid = await fs.readFile(path.join(pm2Home, 'pm2.pid'), 'utf8');
|
||||
const parsedPid = Number(rawPid.trim());
|
||||
if (Number.isSafeInteger(parsedPid) && parsedPid > 1) {
|
||||
daemonPid = parsedPid;
|
||||
}
|
||||
} catch {
|
||||
// PM2 might already have removed its pid file.
|
||||
}
|
||||
|
||||
await execCommand('pnpm', ['exec', 'pm2', 'kill'], {
|
||||
...process.env,
|
||||
PM2_HOME: pm2Home,
|
||||
}).catch(() => undefined);
|
||||
|
||||
if (daemonPid === null || !processIsAlive(daemonPid)) {
|
||||
return;
|
||||
}
|
||||
for (let attempt = 0; attempt < 50 && processIsAlive(daemonPid); attempt += 1) {
|
||||
await sleep(100);
|
||||
}
|
||||
if (!processIsAlive(daemonPid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commandLine = (await fs.readFile(`/proc/${daemonPid}/cmdline`, 'utf8')).replaceAll('\0', ' ');
|
||||
if (!commandLine.includes('PM2') || !commandLine.includes(pm2Home)) {
|
||||
throw new Error(`Refusing to stop PID ${daemonPid}: it is not the dedicated PM2 daemon for ${pm2Home}`);
|
||||
}
|
||||
process.kill(daemonPid, 'SIGTERM');
|
||||
for (let attempt = 0; attempt < 50 && processIsAlive(daemonPid); attempt += 1) {
|
||||
await sleep(100);
|
||||
}
|
||||
if (processIsAlive(daemonPid)) {
|
||||
throw new Error(`Dedicated PM2 daemon did not stop: PID ${daemonPid}, PM2_HOME ${pm2Home}`);
|
||||
}
|
||||
};
|
||||
|
||||
const waitForTurnDaemonStatus = async (gameClient: ReturnType<typeof createGameClient>, timeoutMs = 90_000) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastError: string | null = null;
|
||||
@@ -399,11 +460,16 @@ describe('pm2 orchestrator e2e', () => {
|
||||
let profileName = 'che:2';
|
||||
let apiPort = 14000;
|
||||
let processNames: string[] = [];
|
||||
let pm2Home: string | null = null;
|
||||
let previousPm2Home: string | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
await loadEnv();
|
||||
process.chdir(workspaceRoot);
|
||||
await ensureBuildArtifacts();
|
||||
previousPm2Home = process.env.PM2_HOME;
|
||||
pm2Home = await fs.mkdtemp(path.join(os.tmpdir(), 'sammo-pm2-e2e-'));
|
||||
process.env.PM2_HOME = pm2Home;
|
||||
|
||||
profile = process.env.PROFILE ?? 'che';
|
||||
scenario = process.env.SCENARIO ?? '2';
|
||||
@@ -416,6 +482,8 @@ describe('pm2 orchestrator e2e', () => {
|
||||
processNames = [
|
||||
buildProcessName(profileName, 'api'),
|
||||
buildProcessName(profileName, 'daemon'),
|
||||
buildProcessName(profileName, 'auction'),
|
||||
buildProcessName(profileName, 'battle'),
|
||||
buildProcessName(profileName, 'tournament'),
|
||||
];
|
||||
|
||||
@@ -433,11 +501,23 @@ describe('pm2 orchestrator e2e', () => {
|
||||
}, 60_000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (pm2Manager) {
|
||||
await cleanupPm2(pm2Manager, processNames);
|
||||
}
|
||||
if (gatewayServer) {
|
||||
await gatewayServer.app.close();
|
||||
try {
|
||||
if (pm2Manager) {
|
||||
await cleanupPm2(pm2Manager, processNames);
|
||||
}
|
||||
if (gatewayServer) {
|
||||
await gatewayServer.app.close();
|
||||
}
|
||||
if (pm2Home) {
|
||||
await stopDedicatedPm2Daemon(pm2Home);
|
||||
await fs.rm(pm2Home, { recursive: true, force: true });
|
||||
}
|
||||
} finally {
|
||||
if (previousPm2Home === undefined) {
|
||||
delete process.env.PM2_HOME;
|
||||
} else {
|
||||
process.env.PM2_HOME = previousPm2Home;
|
||||
}
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
@@ -456,7 +536,7 @@ describe('pm2 orchestrator e2e', () => {
|
||||
const adminGameAccessRef: { value?: string } = {};
|
||||
const accessTokenRef: { value?: string } = {};
|
||||
const gatewayClient = createGatewayClient(gatewayUrl, gatewayServer.config.trpcPath, adminSessionRef);
|
||||
const gameTrpcPath = process.env.GAME_TRPC_PATH ?? process.env.TRPC_PATH ?? '/trpc';
|
||||
const gameTrpcPath = `/${profile}/api/trpc`;
|
||||
const gameClientPublic = createGameClient(gameUrl, gameTrpcPath, { value: undefined });
|
||||
const gameClientAdmin = createGameClient(gameUrl, gameTrpcPath, adminGameAccessRef);
|
||||
const gameClientAuthed = createGameClient(gameUrl, gameTrpcPath, accessTokenRef);
|
||||
@@ -482,6 +562,12 @@ describe('pm2 orchestrator e2e', () => {
|
||||
apiPort,
|
||||
status: 'RUNNING',
|
||||
});
|
||||
await gatewayClient.admin.profiles.updateMeta.mutate({
|
||||
profileName,
|
||||
patch: {
|
||||
localAccountGeneralCreationGraceDays: 7,
|
||||
},
|
||||
});
|
||||
|
||||
await gatewayClient.admin.profiles.installNow.mutate({
|
||||
profileName,
|
||||
@@ -556,7 +642,7 @@ describe('pm2 orchestrator e2e', () => {
|
||||
});
|
||||
expect(createdGeneral.generalId).toBeGreaterThan(0);
|
||||
|
||||
const eventsPath = process.env.GAME_API_EVENTS_PATH ?? '/events';
|
||||
const eventsPath = `/${profile}/api/events`;
|
||||
const runStartMs = Date.now();
|
||||
const ssePromise = waitForSseEvent({
|
||||
url: `${gameUrl}${eventsPath}`,
|
||||
|
||||
@@ -204,6 +204,12 @@ describe('actual tournament lifecycle', () => {
|
||||
apiPort: Number(process.env.GAME_API_PORT ?? 14000),
|
||||
status: 'RUNNING',
|
||||
});
|
||||
await gatewayClient.admin.profiles.updateMeta.mutate({
|
||||
profileName: 'che:908',
|
||||
patch: {
|
||||
localAccountGeneralCreationGraceDays: 7,
|
||||
},
|
||||
});
|
||||
await gatewayClient.admin.profiles.installNow.mutate({
|
||||
profileName: 'che:908',
|
||||
install: {
|
||||
|
||||
@@ -26,7 +26,8 @@ integration('legacy command trace runner', () => {
|
||||
expect.objectContaining({ fromNationId: 1, toNationId: 2, state: 1, term: 24 }),
|
||||
expect.objectContaining({ fromNationId: 2, toNationId: 1, state: 1, term: 24 }),
|
||||
]);
|
||||
expect(trace.after.messages).toHaveLength(2);
|
||||
const addedMessages = trace.after.messages.slice(trace.before.messages.length);
|
||||
expect(addedMessages).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('runs live sortie through conquest and nation collapse', () => {
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
readReferenceDatabaseSnapshot,
|
||||
} from '../src/turn-differential/referenceSnapshot.js';
|
||||
|
||||
const workspaceRoot = findTurnDifferentialWorkspaceRoot(process.cwd());
|
||||
const configuredWorkspaceRoot = process.env.TURN_DIFFERENTIAL_WORKSPACE_ROOT;
|
||||
const workspaceRoot = configuredWorkspaceRoot ?? findTurnDifferentialWorkspaceRoot(process.cwd());
|
||||
const integration = describe.skipIf(!workspaceRoot || process.env.TURN_DIFFERENTIAL_REFERENCE !== '1');
|
||||
|
||||
integration('legacy turn state snapshot adapter', () => {
|
||||
|
||||
Reference in New Issue
Block a user