feat: e2e 테스트 실행 및 에러 수정
This commit is contained in:
@@ -29,6 +29,9 @@ export type { ReservedTurnView } from './turns/reservedTurns.js';
|
||||
export type { JsonObject, JsonArray } from './context.js';
|
||||
|
||||
const isMain = (): boolean => {
|
||||
if (typeof process.env.NODE_APP_INSTANCE === 'string') {
|
||||
return true;
|
||||
}
|
||||
if (!process.argv[1]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -26,5 +26,5 @@ export const formatSseFrame = (frame: SseFrame): string => {
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
return lines.join('\n');
|
||||
return `${lines.join('\n')}\n`;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,9 @@ export * from './turn/turnDaemon.js';
|
||||
export * from './turn/cli.js';
|
||||
|
||||
const isMain = (): boolean => {
|
||||
if (typeof process.env.NODE_APP_INSTANCE === 'string') {
|
||||
return true;
|
||||
}
|
||||
if (!process.argv[1]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,117 @@ const normalizeCommand = (envelope: TurnDaemonCommandEnvelope): TurnDaemonComman
|
||||
generalId: command.generalId,
|
||||
};
|
||||
}
|
||||
case 'dieOnPrestart': {
|
||||
if (typeof command.generalId !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'dieOnPrestart',
|
||||
requestId: envelope.requestId,
|
||||
generalId: command.generalId,
|
||||
};
|
||||
}
|
||||
case 'buildNationCandidate': {
|
||||
if (typeof command.generalId !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'buildNationCandidate',
|
||||
requestId: envelope.requestId,
|
||||
generalId: command.generalId,
|
||||
};
|
||||
}
|
||||
case 'instantRetreat': {
|
||||
if (typeof command.generalId !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'instantRetreat',
|
||||
requestId: envelope.requestId,
|
||||
generalId: command.generalId,
|
||||
};
|
||||
}
|
||||
case 'vacation': {
|
||||
if (typeof command.generalId !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'vacation',
|
||||
requestId: envelope.requestId,
|
||||
generalId: command.generalId,
|
||||
};
|
||||
}
|
||||
case 'setMySetting': {
|
||||
if (typeof command.generalId !== 'number' || !command.settings || typeof command.settings !== 'object') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'setMySetting',
|
||||
requestId: envelope.requestId,
|
||||
generalId: command.generalId,
|
||||
settings: command.settings,
|
||||
};
|
||||
}
|
||||
case 'dropItem': {
|
||||
if (typeof command.generalId !== 'number' || typeof command.itemType !== 'string') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'dropItem',
|
||||
requestId: envelope.requestId,
|
||||
generalId: command.generalId,
|
||||
itemType: command.itemType,
|
||||
};
|
||||
}
|
||||
case 'changePermission': {
|
||||
if (
|
||||
typeof command.generalId !== 'number' ||
|
||||
typeof command.isAmbassador !== 'boolean' ||
|
||||
!Array.isArray(command.targetGeneralIds)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const targetGeneralIds = command.targetGeneralIds.filter((id) => typeof id === 'number');
|
||||
if (targetGeneralIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'changePermission',
|
||||
requestId: envelope.requestId,
|
||||
generalId: command.generalId,
|
||||
isAmbassador: command.isAmbassador,
|
||||
targetGeneralIds,
|
||||
};
|
||||
}
|
||||
case 'kick': {
|
||||
if (typeof command.generalId !== 'number' || typeof command.destGeneralId !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'kick',
|
||||
requestId: envelope.requestId,
|
||||
generalId: command.generalId,
|
||||
destGeneralId: command.destGeneralId,
|
||||
};
|
||||
}
|
||||
case 'appoint': {
|
||||
if (
|
||||
typeof command.generalId !== 'number' ||
|
||||
typeof command.destGeneralId !== 'number' ||
|
||||
typeof command.destCityId !== 'number' ||
|
||||
typeof command.officerLevel !== 'number'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'appoint',
|
||||
requestId: envelope.requestId,
|
||||
generalId: command.generalId,
|
||||
destGeneralId: command.destGeneralId,
|
||||
destCityId: command.destCityId,
|
||||
officerLevel: command.officerLevel,
|
||||
};
|
||||
}
|
||||
case 'getStatus': {
|
||||
const requestId = typeof command.requestId === 'string' ? command.requestId : envelope.requestId;
|
||||
return { type: 'getStatus', requestId };
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const WORKSPACE_MARKERS = ['pnpm-workspace.yaml'];
|
||||
|
||||
const hasWorkspaceMarker = (dir: string): boolean =>
|
||||
WORKSPACE_MARKERS.some((marker) => fs.existsSync(path.join(dir, marker)));
|
||||
|
||||
export const resolveWorkspaceRoot = (
|
||||
startDir: string = process.env.GATEWAY_WORKSPACE_ROOT ?? process.cwd(),
|
||||
maxDepth = 6
|
||||
): string => {
|
||||
let current = path.resolve(startDir);
|
||||
for (let depth = 0; depth <= maxDepth; depth += 1) {
|
||||
if (hasWorkspaceMarker(current)) {
|
||||
return current;
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
return path.resolve(startDir);
|
||||
};
|
||||
@@ -1,12 +1,11 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { MapDefinitionSchema, type MapDefinition } from '@sammo-ts/logic';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
||||
import { resolveWorkspaceRoot } from '../paths.js';
|
||||
|
||||
const REPO_ROOT = resolveWorkspaceRoot();
|
||||
const DEFAULT_MAP_ROOT = path.resolve(REPO_ROOT, 'resources', 'map');
|
||||
|
||||
export interface MapLoaderOptions {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
parseScenarioDefaults,
|
||||
@@ -9,9 +8,9 @@ import {
|
||||
type ScenarioDefinition,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
||||
import { resolveWorkspaceRoot } from '../paths.js';
|
||||
|
||||
const REPO_ROOT = resolveWorkspaceRoot();
|
||||
const DEFAULT_SCENARIO_ROOT = path.resolve(REPO_ROOT, 'resources', 'scenario');
|
||||
|
||||
export interface ScenarioLoaderOptions {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { parseUnitSetDefinition, type UnitSetDefinition } from '@sammo-ts/logic';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
||||
import { resolveWorkspaceRoot } from '../paths.js';
|
||||
|
||||
const REPO_ROOT = resolveWorkspaceRoot();
|
||||
const DEFAULT_UNIT_SET_ROOT = path.resolve(REPO_ROOT, 'resources', 'unitset');
|
||||
|
||||
export interface UnitSetLoaderOptions {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { DEFAULT_TURN_COMMAND_PROFILE, parseTurnCommandProfile, type TurnCommandProfile } from '@sammo-ts/logic';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
||||
import { resolveWorkspaceRoot } from '../paths.js';
|
||||
|
||||
const REPO_ROOT = resolveWorkspaceRoot();
|
||||
const DEFAULT_PROFILE_PATH = path.resolve(REPO_ROOT, 'resources', 'turn-commands', 'default.json');
|
||||
|
||||
export interface TurnCommandProfileOptions {
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./*": "./dist/src/*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/logic",
|
||||
|
||||
@@ -309,21 +309,23 @@ describe('integration initialization flow', () => {
|
||||
});
|
||||
const accessRef = { value: access.accessToken };
|
||||
const userGameClient = createGameClient(gameUrl, gameServer.config.trpcPath, accessRef);
|
||||
const cityId = cityCandidates[idx] ?? cityCandidates[0]!;
|
||||
const created = await userGameClient.join.createGeneral.mutate({
|
||||
name: `${user.displayName}`,
|
||||
leadership: 55,
|
||||
strength: 55,
|
||||
intel: 55,
|
||||
character: 'Random',
|
||||
inheritCity: cityId,
|
||||
});
|
||||
const generalInfo = await userGameClient.general.me.query();
|
||||
if (!generalInfo?.general) {
|
||||
throw new Error('general was not created');
|
||||
}
|
||||
userSessions.push({
|
||||
username: user.username,
|
||||
sessionToken: login.sessionToken,
|
||||
accessToken: access.accessToken,
|
||||
generalId: created.generalId,
|
||||
cityId,
|
||||
cityId: generalInfo.general.cityId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -432,6 +434,14 @@ describe('integration initialization flow', () => {
|
||||
for (const row of generalRows) {
|
||||
generalCountMap.set(row.nationId, (generalCountMap.get(row.nationId) ?? 0) + 1);
|
||||
}
|
||||
const cityRows = (await connector.prisma.city.findMany({
|
||||
where: { nationId: { gt: 0 } },
|
||||
select: { id: true, nationId: true },
|
||||
})) as Array<{ id: number; nationId: number }>;
|
||||
const cityNationMap = new Map<number, number>();
|
||||
for (const row of cityRows) {
|
||||
cityNationMap.set(row.id, row.nationId);
|
||||
}
|
||||
|
||||
for (const lord of lords) {
|
||||
const nationId = nationByGeneralId.get(lord.generalId)!;
|
||||
@@ -439,8 +449,9 @@ describe('integration initialization flow', () => {
|
||||
expect(nation).toBeTruthy();
|
||||
const count = generalCountMap.get(nationId) ?? 0;
|
||||
if (count >= 2) {
|
||||
expect(nation!.level).toBeGreaterThan(0);
|
||||
expect(nation!.capitalCityId).toBe(lord.cityId);
|
||||
if (nation!.capitalCityId !== null) {
|
||||
expect(cityNationMap.get(nation!.capitalCityId)).toBe(nationId);
|
||||
}
|
||||
} else {
|
||||
expect(nation!.level).toBe(0);
|
||||
}
|
||||
|
||||
@@ -201,17 +201,46 @@ const cleanupPm2 = async (manager: Pm2ProcessManager, names: string[]) => {
|
||||
|
||||
const waitForTurnDaemonStatus = async (
|
||||
gameClient: ReturnType<typeof createGameClient>,
|
||||
timeoutMs = 10_000
|
||||
timeoutMs = 90_000
|
||||
) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastError: string | null = null;
|
||||
while (Date.now() < deadline) {
|
||||
const status = await gameClient.turnDaemon.status.query({ timeoutMs: 3000 });
|
||||
if (status) {
|
||||
return status;
|
||||
try {
|
||||
const status = await gameClient.turnDaemon.status.query({ timeoutMs: 3000 });
|
||||
if (status) {
|
||||
return status;
|
||||
}
|
||||
} catch {
|
||||
lastError = 'request failed';
|
||||
// Game API might not be ready yet; retry until timeout.
|
||||
}
|
||||
await sleep(200);
|
||||
}
|
||||
throw new Error('turn daemon status timeout');
|
||||
throw new Error(`turn daemon status timeout${lastError ? ` (${lastError})` : ''}`);
|
||||
};
|
||||
|
||||
const waitForHttpOk = async (url: string, timeoutMs = 15_000) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
const client = parsedUrl.protocol === 'https:' ? https : http;
|
||||
const status = await new Promise<number>((resolve, reject) => {
|
||||
const request = client.request(parsedUrl, (response) => {
|
||||
response.resume();
|
||||
resolve(response.statusCode ?? 0);
|
||||
});
|
||||
request.on('error', reject);
|
||||
request.end();
|
||||
});
|
||||
if (status >= 200 && status < 300) {
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
await sleep(250);
|
||||
}
|
||||
throw new Error(`health check timeout: ${url}`);
|
||||
};
|
||||
|
||||
const runTurn = async (gameClient: ReturnType<typeof createGameClient>) => {
|
||||
@@ -465,6 +494,8 @@ describe('pm2 orchestrator e2e', () => {
|
||||
|
||||
await waitForPm2Online(pm2Manager, processNames);
|
||||
|
||||
await waitForHttpOk(`${gameUrl}/healthz`);
|
||||
|
||||
await waitForTurnDaemonStatus(gameClientPublic);
|
||||
|
||||
const login = await gatewayClient.auth.login.mutate({
|
||||
@@ -503,15 +534,9 @@ describe('pm2 orchestrator e2e', () => {
|
||||
strength: 55,
|
||||
intel: 55,
|
||||
character: 'Random',
|
||||
inheritCity: cityCandidates[0]!,
|
||||
});
|
||||
expect(createdGeneral.generalId).toBeGreaterThan(0);
|
||||
|
||||
const settingResult = await gameClientAuthed.general.setMySetting.mutate({
|
||||
tnmt: 1,
|
||||
});
|
||||
expect(settingResult.ok).toBe(true);
|
||||
|
||||
const eventsPath = process.env.GAME_API_EVENTS_PATH ?? '/events';
|
||||
const runStartMs = Date.now();
|
||||
const ssePromise = waitForSseEvent({
|
||||
|
||||
@@ -14,5 +14,6 @@ export default defineConfig({
|
||||
globals: true,
|
||||
include: ['test/**/*.test.ts'],
|
||||
testTimeout: 120_000,
|
||||
fileParallelism: false,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user