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';
|
export type { JsonObject, JsonArray } from './context.js';
|
||||||
|
|
||||||
const isMain = (): boolean => {
|
const isMain = (): boolean => {
|
||||||
|
if (typeof process.env.NODE_APP_INSTANCE === 'string') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!process.argv[1]) {
|
if (!process.argv[1]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,5 +26,5 @@ export const formatSseFrame = (frame: SseFrame): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lines.push('');
|
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';
|
export * from './turn/cli.js';
|
||||||
|
|
||||||
const isMain = (): boolean => {
|
const isMain = (): boolean => {
|
||||||
|
if (typeof process.env.NODE_APP_INSTANCE === 'string') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!process.argv[1]) {
|
if (!process.argv[1]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,117 @@ const normalizeCommand = (envelope: TurnDaemonCommandEnvelope): TurnDaemonComman
|
|||||||
generalId: command.generalId,
|
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': {
|
case 'getStatus': {
|
||||||
const requestId = typeof command.requestId === 'string' ? command.requestId : envelope.requestId;
|
const requestId = typeof command.requestId === 'string' ? command.requestId : envelope.requestId;
|
||||||
return { type: 'getStatus', 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 fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
import { MapDefinitionSchema, type MapDefinition } from '@sammo-ts/logic';
|
import { MapDefinitionSchema, type MapDefinition } from '@sammo-ts/logic';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
import { resolveWorkspaceRoot } from '../paths.js';
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
const REPO_ROOT = resolveWorkspaceRoot();
|
||||||
const DEFAULT_MAP_ROOT = path.resolve(REPO_ROOT, 'resources', 'map');
|
const DEFAULT_MAP_ROOT = path.resolve(REPO_ROOT, 'resources', 'map');
|
||||||
|
|
||||||
export interface MapLoaderOptions {
|
export interface MapLoaderOptions {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
parseScenarioDefaults,
|
parseScenarioDefaults,
|
||||||
@@ -9,9 +8,9 @@ import {
|
|||||||
type ScenarioDefinition,
|
type ScenarioDefinition,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
import { resolveWorkspaceRoot } from '../paths.js';
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
const REPO_ROOT = resolveWorkspaceRoot();
|
||||||
const DEFAULT_SCENARIO_ROOT = path.resolve(REPO_ROOT, 'resources', 'scenario');
|
const DEFAULT_SCENARIO_ROOT = path.resolve(REPO_ROOT, 'resources', 'scenario');
|
||||||
|
|
||||||
export interface ScenarioLoaderOptions {
|
export interface ScenarioLoaderOptions {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
import { parseUnitSetDefinition, type UnitSetDefinition } from '@sammo-ts/logic';
|
import { parseUnitSetDefinition, type UnitSetDefinition } from '@sammo-ts/logic';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
import { resolveWorkspaceRoot } from '../paths.js';
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
const REPO_ROOT = resolveWorkspaceRoot();
|
||||||
const DEFAULT_UNIT_SET_ROOT = path.resolve(REPO_ROOT, 'resources', 'unitset');
|
const DEFAULT_UNIT_SET_ROOT = path.resolve(REPO_ROOT, 'resources', 'unitset');
|
||||||
|
|
||||||
export interface UnitSetLoaderOptions {
|
export interface UnitSetLoaderOptions {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
import { DEFAULT_TURN_COMMAND_PROFILE, parseTurnCommandProfile, type TurnCommandProfile } from '@sammo-ts/logic';
|
import { DEFAULT_TURN_COMMAND_PROFILE, parseTurnCommandProfile, type TurnCommandProfile } from '@sammo-ts/logic';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
import { resolveWorkspaceRoot } from '../paths.js';
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
const REPO_ROOT = resolveWorkspaceRoot();
|
||||||
const DEFAULT_PROFILE_PATH = path.resolve(REPO_ROOT, 'resources', 'turn-commands', 'default.json');
|
const DEFAULT_PROFILE_PATH = path.resolve(REPO_ROOT, 'resources', 'turn-commands', 'default.json');
|
||||||
|
|
||||||
export interface TurnCommandProfileOptions {
|
export interface TurnCommandProfileOptions {
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
}
|
},
|
||||||
|
"./*": "./dist/src/*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/logic",
|
"build": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/logic",
|
||||||
|
|||||||
@@ -309,21 +309,23 @@ describe('integration initialization flow', () => {
|
|||||||
});
|
});
|
||||||
const accessRef = { value: access.accessToken };
|
const accessRef = { value: access.accessToken };
|
||||||
const userGameClient = createGameClient(gameUrl, gameServer.config.trpcPath, accessRef);
|
const userGameClient = createGameClient(gameUrl, gameServer.config.trpcPath, accessRef);
|
||||||
const cityId = cityCandidates[idx] ?? cityCandidates[0]!;
|
|
||||||
const created = await userGameClient.join.createGeneral.mutate({
|
const created = await userGameClient.join.createGeneral.mutate({
|
||||||
name: `${user.displayName}`,
|
name: `${user.displayName}`,
|
||||||
leadership: 55,
|
leadership: 55,
|
||||||
strength: 55,
|
strength: 55,
|
||||||
intel: 55,
|
intel: 55,
|
||||||
character: 'Random',
|
character: 'Random',
|
||||||
inheritCity: cityId,
|
|
||||||
});
|
});
|
||||||
|
const generalInfo = await userGameClient.general.me.query();
|
||||||
|
if (!generalInfo?.general) {
|
||||||
|
throw new Error('general was not created');
|
||||||
|
}
|
||||||
userSessions.push({
|
userSessions.push({
|
||||||
username: user.username,
|
username: user.username,
|
||||||
sessionToken: login.sessionToken,
|
sessionToken: login.sessionToken,
|
||||||
accessToken: access.accessToken,
|
accessToken: access.accessToken,
|
||||||
generalId: created.generalId,
|
generalId: created.generalId,
|
||||||
cityId,
|
cityId: generalInfo.general.cityId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,6 +434,14 @@ describe('integration initialization flow', () => {
|
|||||||
for (const row of generalRows) {
|
for (const row of generalRows) {
|
||||||
generalCountMap.set(row.nationId, (generalCountMap.get(row.nationId) ?? 0) + 1);
|
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) {
|
for (const lord of lords) {
|
||||||
const nationId = nationByGeneralId.get(lord.generalId)!;
|
const nationId = nationByGeneralId.get(lord.generalId)!;
|
||||||
@@ -439,8 +449,9 @@ describe('integration initialization flow', () => {
|
|||||||
expect(nation).toBeTruthy();
|
expect(nation).toBeTruthy();
|
||||||
const count = generalCountMap.get(nationId) ?? 0;
|
const count = generalCountMap.get(nationId) ?? 0;
|
||||||
if (count >= 2) {
|
if (count >= 2) {
|
||||||
expect(nation!.level).toBeGreaterThan(0);
|
if (nation!.capitalCityId !== null) {
|
||||||
expect(nation!.capitalCityId).toBe(lord.cityId);
|
expect(cityNationMap.get(nation!.capitalCityId)).toBe(nationId);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
expect(nation!.level).toBe(0);
|
expect(nation!.level).toBe(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,17 +201,46 @@ const cleanupPm2 = async (manager: Pm2ProcessManager, names: string[]) => {
|
|||||||
|
|
||||||
const waitForTurnDaemonStatus = async (
|
const waitForTurnDaemonStatus = async (
|
||||||
gameClient: ReturnType<typeof createGameClient>,
|
gameClient: ReturnType<typeof createGameClient>,
|
||||||
timeoutMs = 10_000
|
timeoutMs = 90_000
|
||||||
) => {
|
) => {
|
||||||
const deadline = Date.now() + timeoutMs;
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
let lastError: string | null = null;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const status = await gameClient.turnDaemon.status.query({ timeoutMs: 3000 });
|
try {
|
||||||
if (status) {
|
const status = await gameClient.turnDaemon.status.query({ timeoutMs: 3000 });
|
||||||
return status;
|
if (status) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
lastError = 'request failed';
|
||||||
|
// Game API might not be ready yet; retry until timeout.
|
||||||
}
|
}
|
||||||
await sleep(200);
|
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>) => {
|
const runTurn = async (gameClient: ReturnType<typeof createGameClient>) => {
|
||||||
@@ -465,6 +494,8 @@ describe('pm2 orchestrator e2e', () => {
|
|||||||
|
|
||||||
await waitForPm2Online(pm2Manager, processNames);
|
await waitForPm2Online(pm2Manager, processNames);
|
||||||
|
|
||||||
|
await waitForHttpOk(`${gameUrl}/healthz`);
|
||||||
|
|
||||||
await waitForTurnDaemonStatus(gameClientPublic);
|
await waitForTurnDaemonStatus(gameClientPublic);
|
||||||
|
|
||||||
const login = await gatewayClient.auth.login.mutate({
|
const login = await gatewayClient.auth.login.mutate({
|
||||||
@@ -503,15 +534,9 @@ describe('pm2 orchestrator e2e', () => {
|
|||||||
strength: 55,
|
strength: 55,
|
||||||
intel: 55,
|
intel: 55,
|
||||||
character: 'Random',
|
character: 'Random',
|
||||||
inheritCity: cityCandidates[0]!,
|
|
||||||
});
|
});
|
||||||
expect(createdGeneral.generalId).toBeGreaterThan(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 eventsPath = process.env.GAME_API_EVENTS_PATH ?? '/events';
|
||||||
const runStartMs = Date.now();
|
const runStartMs = Date.now();
|
||||||
const ssePromise = waitForSseEvent({
|
const ssePromise = waitForSseEvent({
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ export default defineConfig({
|
|||||||
globals: true,
|
globals: true,
|
||||||
include: ['test/**/*.test.ts'],
|
include: ['test/**/*.test.ts'],
|
||||||
testTimeout: 120_000,
|
testTimeout: 120_000,
|
||||||
|
fileParallelism: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user