fix: 특수 유저 커맨드의 Ref 호환 경계를 보강한다

수뇌 국가 설정과 NPC 정책을 actor-bound ENGINE mutation으로 옮기고, 추방·등용·점령·멸망·아이템 폐기의 특수 분기를 Ref와 맞춘다.

요청 ID를 사용자·프로필별로 격리하고 토너먼트 손상 projection을 fail-closed하며 실제 DB 및 Ref 차등 회귀를 보강한다.
This commit is contained in:
2026-08-24 12:10:57 +00:00
parent 5389f8ed94
commit 630bc29100
74 changed files with 3893 additions and 1141 deletions
+18 -3
View File
@@ -18,6 +18,7 @@ import { createTurnDaemonCommandHandler, hasVotePollDeadlinePassed } from '../sr
const buildGeneral = (id: number): TurnGeneral => ({
id,
userId: `user-${id}`,
name: `General_${id}`,
nationId: 1,
cityId: 1,
@@ -46,6 +47,12 @@ const buildGeneral = (id: number): TurnGeneral => ({
npcState: 0,
});
const actorBindingDb = (userId = 'user-1') => ({
inputEvent: {
findUnique: async () => ({ actorUserId: userId, target: 'ENGINE', eventType: 'voteReward' }),
},
});
describe('voteReward command', () => {
it('keeps the wall-time fallback open at exact deadline equality', () => {
const deadline = new Date('0180-01-01T00:00:00.000Z');
@@ -67,6 +74,7 @@ describe('voteReward command', () => {
sentAt: '2026-08-23T00:00:00.000Z',
command: {
type: 'voteReward',
userId: 'user-1',
voteId: 1,
generalId: 1,
selection: [0],
@@ -224,6 +232,7 @@ describe('voteReward command', () => {
let voteQueryCount = 0;
let voteInsertQuery: { strings: readonly string[]; values: readonly unknown[] } | undefined;
const commandDb = {
...actorBindingDb(),
auction: {
findMany: async () => [],
},
@@ -249,6 +258,8 @@ describe('voteReward command', () => {
const handler = createTurnDaemonCommandHandler({ world });
const command = {
type: 'voteReward' as const,
requestId: 'vote-reward-1',
userId: 'user-1',
voteId: 1,
generalId: 1,
selection: [0],
@@ -269,9 +280,7 @@ describe('voteReward command', () => {
expect(
voteInsertQuery?.values.filter(
(value) =>
value instanceof Date &&
value.getTime() >= writerWindowStart &&
value.getTime() <= writerWindowEnd
value instanceof Date && value.getTime() >= writerWindowStart && value.getTime() <= writerWindowEnd
)
).toHaveLength(1);
@@ -311,6 +320,7 @@ describe('voteReward command', () => {
const duplicateHandler = createTurnDaemonCommandHandler({ world: duplicateWorld });
const duplicateResult = await duplicateHandler.handle(command, {
db: {
...actorBindingDb(),
auction: { findMany: async () => [] },
$queryRaw: async (query: { strings: readonly string[] }) => {
const text = query.strings.join(' ');
@@ -346,6 +356,7 @@ describe('voteReward command', () => {
const mismatchHandler = createTurnDaemonCommandHandler({ world: mismatchWorld });
const mismatchResult = await mismatchHandler.handle(command, {
db: {
...actorBindingDb(),
auction: { findMany: async () => [] },
$queryRaw: async (query: { strings: readonly string[] }) => {
const text = query.strings.join(' ');
@@ -383,6 +394,7 @@ describe('voteReward command', () => {
const { acceptedGameTick: _acceptedGameTick, ...legacyLateCommand } = command;
const legacyLateResult = await legacyLateHandler.handle(legacyLateCommand, {
db: {
...actorBindingDb(),
$queryRaw: async (query: { strings: readonly string[] }) =>
query.strings.join(' ').includes('SELECT options')
? [
@@ -456,6 +468,7 @@ describe('voteReward command', () => {
});
const handler = createTurnDaemonCommandHandler({ world });
const commandDb = {
...actorBindingDb(),
auction: {
findMany: async () => [{ targetCode: 'che_무기_12_칠성검' }],
},
@@ -476,6 +489,8 @@ describe('voteReward command', () => {
const result = await handler.handle(
{
type: 'voteReward',
requestId: 'vote-reward-occupied',
userId: 'user-1',
voteId: 1,
generalId: 1,
selection: [0],