fix: 은퇴 명예 기록과 유산 수명주기 정합성을 복원한다

은퇴·사망·통일의 저장 순서와 명예의 전당 및 명장일람 판정을 Ref 흐름에 맞춘다.

유산 행동을 인증된 daemon transaction으로 통합하고 중복 지급·고유 아이템·로그·오류 경계를 회귀 테스트한다.
This commit is contained in:
2026-08-24 03:38:12 +00:00
parent 95cf68dffd
commit 4fc20de8d4
33 changed files with 4160 additions and 1166 deletions
+45 -5
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
import { RANK_DATA_TYPES } from '@sammo-ts/common';
@@ -108,6 +108,7 @@ const buildContext = (options?: {
profileId?: string;
generals?: RankingGeneralRow[];
rankRows?: Array<{ generalId: number; type: string; value: number }>;
gameHistoryFindMany?: (args: unknown) => Promise<Array<{ season: number; scenario: number; scenarioName: string }>>;
}): GameApiContext => {
const selectedGeneralRows = options?.generals ?? generalRows;
const selectedProfile = options?.profileId
@@ -198,10 +199,12 @@ const buildContext = (options?: {
findMany: async () => [{ targetCode: 'che_명마_15_적토마' }],
},
gameHistory: {
findMany: async () => [
{ season: 3, scenario: 22, scenarioName: '가상모드22' },
{ season: 3, scenario: 22, scenarioName: '가상모드22' },
],
findMany:
options?.gameHistoryFindMany ??
(async () => [
{ season: 3, scenario: 22, scenarioName: '가상모드22' },
{ season: 3, scenario: 22, scenarioName: '가상모드22' },
]),
},
hallOfFame: {
findMany: async (args: { where: { type: string } }) =>
@@ -303,6 +306,28 @@ describe('ranking.getBestGeneral', () => {
});
});
it('excludes 100th-season event mastery from current Best General values', async () => {
const generals = [
{
...generalRows[0]!,
meta: {
...generalRows[0]!.meta,
dex1: 120,
event100_allstar: { granted: { dex1: 70 } },
} as unknown as RankingGeneralRow['meta'],
},
];
const result = await appRouter.createCaller(buildContext({ generals })).ranking.getBestGeneral({
view: 'user',
});
expect(result.sections.find((section) => section.title === '보 병 숙 련 도')?.entries[0]).toMatchObject({
id: 1,
value: 50,
printValue: '50',
});
});
it('returns positions one through ten for every populated ranking section', async () => {
const generals = Array.from({ length: 12 }, (_, index) => {
const id = index + 1;
@@ -386,6 +411,21 @@ describe('ranking hall of fame', () => {
]);
});
it('includes the active OPEN game while excluding ABANDONED options', async () => {
const findMany = vi.fn(async () => [
{ season: 4, scenario: 23, scenarioName: '현재 시나리오' },
{ season: 3, scenario: 22, scenarioName: '완료 시나리오' },
]);
const options = await appRouter
.createCaller(buildContext({ authenticated: false, gameHistoryFindMany: findMany }))
.ranking.getHallOfFameOptions();
expect(findMany).toHaveBeenCalledWith(
expect.objectContaining({ where: { status: { in: ['OPEN', 'COMPLETED'] } } })
);
expect(options.map((entry) => entry.season)).toEqual([4, 3]);
});
it('scopes previous-server options and rankings to the request profile', async () => {
const cheCaller = appRouter.createCaller(buildContext({ authenticated: false }));
await expect(cheCaller.ranking.getHallOfFameOptions({ source: 'legacy' })).resolves.toEqual([