test: audit and strengthen legacy test suite
This commit is contained in:
@@ -226,23 +226,36 @@ const buildPayload = (action: BattleSimJobPayload['action']): BattleSimJobPayloa
|
||||
});
|
||||
|
||||
describe('battle sim processor', () => {
|
||||
it('simulates battles and returns summary', () => {
|
||||
it('returns the fixed-seed battle summary instead of only a successful shape', () => {
|
||||
const payload = buildPayload('battle');
|
||||
const result = processBattleSimJob(payload);
|
||||
|
||||
expect(result.result).toBe(true);
|
||||
expect(result.reason).toBe('success');
|
||||
expect(result.lastWarLog).toBeTruthy();
|
||||
expect(result.phase).toBeTypeOf('number');
|
||||
expect(result.attackerRice).toBeTypeOf('number');
|
||||
expect(result).toMatchObject({
|
||||
result: true,
|
||||
reason: 'success',
|
||||
datetime: '2026-01-01 00:00:00',
|
||||
avgWar: 1,
|
||||
phase: 2,
|
||||
killed: 625,
|
||||
maxKilled: 625,
|
||||
minKilled: 625,
|
||||
dead: 1000,
|
||||
maxDead: 1000,
|
||||
minDead: 1000,
|
||||
attackerRice: 65,
|
||||
defenderRice: 83,
|
||||
attackerSkills: { 부상: 1 },
|
||||
defendersSkills: [{ 회피시도: 1, 회피: 1 }],
|
||||
});
|
||||
expect(result.lastWarLog?.generalActionLog).toContain('퇴각했습니다.');
|
||||
});
|
||||
|
||||
it('returns defender order for reorder action', () => {
|
||||
it('returns the fixed defender ID order for reorder action', () => {
|
||||
const payload = buildPayload('reorder');
|
||||
const result = processBattleSimJob(payload);
|
||||
|
||||
expect(result.result).toBe(true);
|
||||
expect(result.order?.length).toBe(1);
|
||||
expect(result.order).toEqual([2]);
|
||||
});
|
||||
|
||||
it('executes crew trigger handlers in simulator battles', () => {
|
||||
|
||||
@@ -5,12 +5,13 @@ import { createTournamentAutoStartHandler } from '@sammo-ts/common';
|
||||
|
||||
import { TournamentStore } from '../src/tournament/store.js';
|
||||
import { buildTournamentKeys } from '../src/tournament/keys.js';
|
||||
import type { TournamentBetEntry, TournamentMatchEntry, TournamentParticipantEntry, TournamentState } from '../src/tournament/types.js';
|
||||
import {
|
||||
applyBattle,
|
||||
applyPreBattleStage,
|
||||
settleTournamentOutcome,
|
||||
} from '../src/tournament/worker.js';
|
||||
import type {
|
||||
TournamentBetEntry,
|
||||
TournamentMatchEntry,
|
||||
TournamentParticipantEntry,
|
||||
TournamentState,
|
||||
} from '../src/tournament/types.js';
|
||||
import { applyBattle, applyPreBattleStage, settleTournamentOutcome } from '../src/tournament/worker.js';
|
||||
import type { TurnDaemonTransport } from '../src/daemon/transport.js';
|
||||
|
||||
class MemoryRedis {
|
||||
@@ -208,7 +209,7 @@ const runTournamentToCompletion = async (options: {
|
||||
const delayTick = async (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
describe('tournament worker (in-memory)', () => {
|
||||
it('64명(상/중/하 스탯)으로 결승까지 진행된다', async () => {
|
||||
it('64명 고정 seed 대진에서 15번 참가자가 결승을 이긴다', async () => {
|
||||
const redis = new MemoryRedis();
|
||||
const store = new TournamentStore(redis, buildTournamentKeys('test'));
|
||||
const participants = createParticipants(16, 16, 32);
|
||||
@@ -219,10 +220,17 @@ describe('tournament worker (in-memory)', () => {
|
||||
const prisma = createPrismaMock({ baseSeed: 'seed' });
|
||||
const finalState = await runTournamentToCompletion({ store, prisma, baseSeed: 'seed' });
|
||||
const matches = await store.getMatches();
|
||||
const finalMatches = matches.filter((match) => match.stage === 10);
|
||||
|
||||
expect(finalState.stage).toBe(0);
|
||||
expect(finalState.winnerId).toBeDefined();
|
||||
expect(matches.some((match) => match.stage === 10 && match.winnerId)).toBe(true);
|
||||
expect(finalState.winnerId).toBe(15);
|
||||
expect(finalMatches).toHaveLength(1);
|
||||
expect(finalMatches[0]).toMatchObject({
|
||||
attackerId: 15,
|
||||
defenderId: 1,
|
||||
winnerId: 15,
|
||||
lastEnergy: { attacker: -90, defender: -92 },
|
||||
});
|
||||
});
|
||||
|
||||
it('우승 결과에 따라 베팅 정산 명령이 생성된다', async () => {
|
||||
|
||||
@@ -100,7 +100,7 @@ const createMockPrisma = (initialGeneralRows: any[] = []) => {
|
||||
const addMinutes = (time: Date, minutes: number): Date => new Date(time.getTime() + minutes * 60_000);
|
||||
|
||||
describe('NPC 일반 내정 턴', () => {
|
||||
it('예약 턴이 없어도 내정 수치가 증가한다', async () => {
|
||||
it('고정 seed에서 예약 턴 없이 치안 명령을 실행하고 다음 턴으로 이동한다', async () => {
|
||||
const generals: TurnGeneral[] = [
|
||||
{
|
||||
id: 1,
|
||||
@@ -285,15 +285,10 @@ describe('NPC 일반 내정 턴', () => {
|
||||
});
|
||||
|
||||
const afterCity = world.getCityById(1)!;
|
||||
const increased =
|
||||
afterCity.population > beforeStats.population ||
|
||||
afterCity.agriculture > beforeStats.agriculture ||
|
||||
afterCity.commerce > beforeStats.commerce ||
|
||||
afterCity.security > beforeStats.security ||
|
||||
afterCity.defence > beforeStats.defence ||
|
||||
afterCity.wall > beforeStats.wall;
|
||||
|
||||
expect(increased).toBe(true);
|
||||
expect(world.getGeneralById(1)!.turnTime.getTime()).toBeGreaterThan(mockDate.getTime());
|
||||
expect(afterCity).toMatchObject({
|
||||
...beforeStats,
|
||||
security: 1050,
|
||||
});
|
||||
expect(world.getGeneralById(1)!.turnTime.getTime()).toBe(addMinutes(mockDate, 10).getTime());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,24 +67,14 @@ describe('NPC 대형 시뮬레이션', () => {
|
||||
for (let i = 0; i < npcPerType; i += 1) {
|
||||
const cityId = smallMediumCityIds[i % smallMediumCityIds.length];
|
||||
generals.push(
|
||||
createNpcGeneral(
|
||||
generals.length + 1,
|
||||
cityId,
|
||||
{ leadership: 75, strength: 75, intelligence: 10 },
|
||||
2
|
||||
)
|
||||
createNpcGeneral(generals.length + 1, cityId, { leadership: 75, strength: 75, intelligence: 10 }, 2)
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < npcPerType; i += 1) {
|
||||
const cityId = smallMediumCityIds[i % smallMediumCityIds.length];
|
||||
generals.push(
|
||||
createNpcGeneral(
|
||||
generals.length + 1,
|
||||
cityId,
|
||||
{ leadership: 75, strength: 10, intelligence: 75 },
|
||||
2
|
||||
)
|
||||
createNpcGeneral(generals.length + 1, cityId, { leadership: 75, strength: 10, intelligence: 75 }, 2)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -275,7 +265,7 @@ describe('NPC 대형 시뮬레이션', () => {
|
||||
: ` ${trace.requestedAction} -> ${trace.actionKey}`;
|
||||
console.log(
|
||||
`- ${trace.year}-${String(trace.month).padStart(2, '0')} N${trace.nationId} G${trace.generalId} g${round(trace.gold)}/r${round(trace.rice)} c${trace.crew}/t${trace.train}/a${trace.atmos} ` +
|
||||
`${trace.ok ? 'OK' : 'FAIL'} ${action}${extra}`
|
||||
`${trace.ok ? 'OK' : 'FAIL'} ${action}${extra}`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -302,9 +292,7 @@ describe('NPC 대형 시뮬레이션', () => {
|
||||
};
|
||||
|
||||
const assertSmallMediumCitiesFounded = () => {
|
||||
const targetCities = world
|
||||
.listCities()
|
||||
.filter((city) => [4, 5].includes(city.level));
|
||||
const targetCities = world.listCities().filter((city) => [4, 5].includes(city.level));
|
||||
for (const city of targetCities) {
|
||||
expect(city.nationId).toBeGreaterThan(0);
|
||||
}
|
||||
@@ -384,54 +372,13 @@ describe('NPC 대형 시뮬레이션', () => {
|
||||
expect(afterTotal).toBeGreaterThan(beforeTotal);
|
||||
};
|
||||
|
||||
const assertDomesticGrowthBy = (year: number, month: number) => {
|
||||
const assertDomesticGrowth = () => {
|
||||
const citiesNow = world.listCities().filter((city) => city.nationId > 0);
|
||||
for (const city of citiesNow) {
|
||||
const baseline = initialCityStats.get(city.id);
|
||||
if (!baseline) {
|
||||
continue;
|
||||
}
|
||||
const delta = {
|
||||
population: city.population - baseline.population,
|
||||
agriculture: city.agriculture - baseline.agriculture,
|
||||
commerce: city.commerce - baseline.commerce,
|
||||
security: city.security - baseline.security,
|
||||
defence: city.defence - baseline.defence,
|
||||
wall: city.wall - baseline.wall,
|
||||
};
|
||||
console.log('[DEBUG] domestic delta', {
|
||||
year,
|
||||
month,
|
||||
cityId: city.id,
|
||||
nationId: city.nationId,
|
||||
delta,
|
||||
});
|
||||
|
||||
const failures: string[] = [];
|
||||
if (city.population <= baseline.population) failures.push('population');
|
||||
if (city.agriculture <= baseline.agriculture) failures.push('agriculture');
|
||||
if (city.commerce <= baseline.commerce) failures.push('commerce');
|
||||
if (city.security <= baseline.security) failures.push('security');
|
||||
if (city.defence <= baseline.defence) failures.push('defence');
|
||||
if (city.wall <= baseline.wall) failures.push('wall');
|
||||
if (failures.length > 0) {
|
||||
console.log('[DEBUG] domestic not grown', {
|
||||
year,
|
||||
month,
|
||||
cityId: city.id,
|
||||
nationId: city.nationId,
|
||||
failures,
|
||||
baseline,
|
||||
current: {
|
||||
population: city.population,
|
||||
agriculture: city.agriculture,
|
||||
commerce: city.commerce,
|
||||
security: city.security,
|
||||
defence: city.defence,
|
||||
wall: city.wall,
|
||||
},
|
||||
});
|
||||
}
|
||||
// 182년 시점에는 징병이 병행되어 인구가 순감할 수 있으므로, 대규모 붕괴만 방지한다.
|
||||
expect(city.population).toBeGreaterThan(baseline.population - 15000);
|
||||
expect(city.agriculture).toBeGreaterThan(baseline.agriculture);
|
||||
@@ -446,15 +393,18 @@ describe('NPC 대형 시뮬레이션', () => {
|
||||
['179-09', () => assertUprisingCount(1)],
|
||||
['179-10', () => assertUprisingCount(2)],
|
||||
['179-11', () => assertFoundedCount(1)],
|
||||
['179-12', () => {
|
||||
assertTaxRateUnder(15);
|
||||
maybeSnapshotGold();
|
||||
}],
|
||||
[
|
||||
'179-12',
|
||||
() => {
|
||||
assertTaxRateUnder(15);
|
||||
maybeSnapshotGold();
|
||||
},
|
||||
],
|
||||
['180-01', () => assertGoldIncome()],
|
||||
['180-07', () => assertSmallMediumCitiesFounded()],
|
||||
['180-11', () => assertCityTrust(90)],
|
||||
['181-01', () => assertNationGeneralCount(10)],
|
||||
['182-01', () => assertDomesticGrowthBy(182, 1)],
|
||||
['182-01', () => assertDomesticGrowth()],
|
||||
['182-10', () => assertNationRecruitCount(5)],
|
||||
['183-01', () => assertWarReadiness(10, 70, 70)],
|
||||
['183-02', () => assertDispatchRecorded(183, 1, 1)],
|
||||
|
||||
@@ -156,7 +156,10 @@ describe('voteReward command', () => {
|
||||
|
||||
const itemRegistry = createItemModuleRegistry(await loadItemModules([...ITEM_KEYS]));
|
||||
const config = resolveUniqueConfig(snapshot.scenarioConfig.const as Record<string, unknown>);
|
||||
const occupied = countOccupiedUniqueItems(generals.map((general) => general.role.items), itemRegistry);
|
||||
const occupied = countOccupiedUniqueItems(
|
||||
generals.map((general) => general.role.items),
|
||||
itemRegistry
|
||||
);
|
||||
const rng = new RandUtil(LiteHashDRBG.build(buildVoteUniqueSeed('seed', 1, 1)));
|
||||
const itemKey = rollUniqueLottery({
|
||||
rng,
|
||||
@@ -199,7 +202,12 @@ describe('voteReward command', () => {
|
||||
expect(updated?.gold).toBe(1500);
|
||||
expect(updated?.role.items.weapon).toBe('che_무기_12_칠성검');
|
||||
const meta = updated?.meta as Record<string, unknown>;
|
||||
expect(meta?.voteRewards).toBeTruthy();
|
||||
expect(meta.voteRewards).toMatchObject({
|
||||
1: {
|
||||
awarded: true,
|
||||
itemKey: 'che_무기_12_칠성검',
|
||||
},
|
||||
});
|
||||
|
||||
const diff = world.consumeDirtyState();
|
||||
const logTexts = diff.logs.map((entry) => entry.text);
|
||||
@@ -211,6 +219,7 @@ describe('voteReward command', () => {
|
||||
throw new Error('voteReward second result missing');
|
||||
}
|
||||
expect(second.alreadyApplied).toBe(true);
|
||||
expect(second.itemKey).toBe('che_무기_12_칠성검');
|
||||
const afterSecond = world.getGeneralById(1);
|
||||
expect(afterSecond?.gold).toBe(1500);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user