fix: Ref 게임 로직과 시나리오 풀 호환을 보정

월 경계, 전투 기술 상한, 연감과 베팅·설문·경매 정산 순서를 Ref 계약에 맞춘다.\n\n시나리오 일반 풀을 ENGINE mutation과 logical tick 기반으로 직렬화하고 914·915 catalog 및 조건부 100기 pool 실행 경계를 추가한다.\n\n경매 worker는 세대별 durable event만 만들고 ENGINE이 row lock 후 상태 전이와 정산을 단일 transaction으로 소유한다.
This commit is contained in:
2026-08-23 16:26:14 +00:00
parent bf6b7be7b0
commit 85591c68ad
114 changed files with 13327 additions and 901 deletions
@@ -21,7 +21,7 @@ import type { GamePrisma } from '@sammo-ts/infra';
import type { MapDefinition, ScenarioConfig, ScenarioMeta, TurnSchedule } from '@sammo-ts/logic';
import { buildAuctionTimerKeys } from '../src/auction/keys.js';
import { processDueAuctionId, runAuctionWorker } from '../src/auction/worker.js';
import { buildAuctionFinalizeRequestId, processDueAuctionId, runAuctionWorker } from '../src/auction/worker.js';
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
const liveDescribe = databaseUrl && process.env.REDIS_URL ? describe : describe.skip;
@@ -80,8 +80,11 @@ liveDescribe('auction worker durable recovery', () => {
return auction;
};
const requestIdFor = (auction: { id: number; closeAt: Date }): string =>
`auction:finalize:${auction.id}:${auction.closeAt.getTime()}`;
const requestIdFor = (auction: { id: number; closeAt: Date; closeTick?: bigint | null }): string =>
buildAuctionFinalizeRequestId(auction.id, {
closeAt: auction.closeAt,
closeTick: auction.closeTick ?? null,
});
const memoryRedis = () => ({
zRangeByScore: vi.fn(async () => []),
@@ -91,7 +94,7 @@ liveDescribe('auction worker durable recovery', () => {
zRemRangeByScore: vi.fn(async () => 0),
});
it('atomically moves OPEN to FINALIZING and creates one deterministic input event', async () => {
it('leaves OPEN and creates one deterministic input event', async () => {
const auction = await createAuction('OPEN');
const redis = memoryRedis();
@@ -104,7 +107,7 @@ liveDescribe('auction worker durable recovery', () => {
id: String(auction.id),
nowMs: Date.now(),
})
).resolves.toBe('FINALIZING');
).resolves.toBe('PENDING');
await expect(
processDueAuctionId({
db: connector.prisma,
@@ -114,13 +117,13 @@ liveDescribe('auction worker durable recovery', () => {
id: String(auction.id),
nowMs: Date.now(),
})
).resolves.toBe('FINALIZING');
).resolves.toBe('PENDING');
const [storedAuction, events] = await Promise.all([
connector.prisma.auction.findUniqueOrThrow({ where: { id: auction.id } }),
connector.prisma.inputEvent.findMany({ where: { requestId: requestIdFor(auction) } }),
]);
expect(storedAuction.status).toBe('FINALIZING');
expect(storedAuction.status).toBe('OPEN');
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({
target: 'ENGINE',
@@ -130,6 +133,7 @@ liveDescribe('auction worker durable recovery', () => {
type: 'auctionFinalize',
requestId: requestIdFor(auction),
auctionId: auction.id,
expectedCloseAt: auction.closeAt.toISOString(),
},
});
});
@@ -191,7 +195,7 @@ liveDescribe('auction worker durable recovery', () => {
id: String(auction.id),
nowMs: Date.now(),
})
).resolves.toBe('FINALIZING');
).resolves.toBe('PENDING');
await expect(
processDueAuctionId({
db: connector.prisma,
@@ -201,7 +205,7 @@ liveDescribe('auction worker durable recovery', () => {
id: String(auction.id),
nowMs: Date.now(),
})
).resolves.toBe('FINALIZING');
).resolves.toBe('PENDING');
await expect(
connector.prisma.inputEvent.findMany({
where: { requestId: { startsWith: requestId } },
@@ -258,7 +262,7 @@ liveDescribe('auction worker durable recovery', () => {
id: String(auction.id),
nowMs: Date.now(),
})
).resolves.toBe('FINALIZING');
).resolves.toBe('PENDING');
await expect(
connector.prisma.inputEvent.findUnique({ where: { requestId: requestIdFor(auction) } })
@@ -531,7 +535,11 @@ liveDescribe('auction worker durable recovery', () => {
where: { id: extensionAuction.id },
data: { closeAt: secondCloseAt, closeTick: BigInt(secondCloseTick) },
});
const secondExtensionRequestId = requestIdFor({ id: extensionAuction.id, closeAt: secondCloseAt });
const secondExtensionRequestId = requestIdFor({
id: extensionAuction.id,
closeAt: secondCloseAt,
closeTick: BigInt(secondCloseTick),
});
await processDueAuctionId({
db: connector.prisma,
redis: memoryRedis(),