시간 도메인과 정지 중 메시지·베팅 경계 정리

This commit is contained in:
2026-09-03 16:01:19 +00:00
parent 10cddbb565
commit abceee8315
108 changed files with 3504 additions and 1473 deletions
+29 -60
View File
@@ -12,7 +12,6 @@ import {
} from '@sammo-ts/logic';
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
import { normalizeTurnDaemonCommand } from '../src/turn/commandRegistry.js';
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
import { createTurnDaemonCommandHandler, hasVotePollDeadlinePassed } from '../src/turn/worldCommandHandler.js';
@@ -81,38 +80,12 @@ const buildDefaultUniquePoolSnapshot = (general: TurnGeneral): TurnWorldSnapshot
});
describe('voteReward command', () => {
it('keeps the wall-time fallback open at exact deadline equality', () => {
it('fails closed when a GAME_TIME poll lost its authoritative end tick', () => {
const deadline = new Date('0180-01-01T00:00:00.000Z');
expect(hasVotePollDeadlinePassed({ endAt: deadline, endTick: null, closedAt: null }, deadline, 0)).toBe(false);
expect(
hasVotePollDeadlinePassed(
{ endAt: deadline, endTick: null, closedAt: null },
new Date(deadline.getTime() + 1),
0
)
).toBe(true);
});
it('preserves the server-accepted game tick through durable command normalization', () => {
expect(
normalizeTurnDaemonCommand({
requestId: 'vote-accepted-tick',
sentAt: '2026-08-23T00:00:00.000Z',
command: {
type: 'voteReward',
userId: 'user-1',
voteId: 1,
generalId: 1,
selection: [0],
acceptedGameTick: 100,
},
})
).toMatchObject({
type: 'voteReward',
requestId: 'vote-accepted-tick',
acceptedGameTick: 100,
});
expect(hasVotePollDeadlinePassed({ endAt: deadline, endTick: null, closedAt: null }, 0)).toBe(true);
expect(hasVotePollDeadlinePassed({ endAt: deadline, endTick: 0n, closedAt: null }, 0)).toBe(false);
expect(hasVotePollDeadlinePassed({ endAt: deadline, endTick: 0n, closedAt: null }, 1)).toBe(true);
});
it('applies gold, unique item, logs, and idempotency', async () => {
@@ -290,9 +263,7 @@ describe('voteReward command', () => {
voteId: 1,
generalId: 1,
selection: [0],
// Ref accepts the request at exact equality. Engine processing may
// occur after the logical clock has advanced beyond the deadline.
acceptedGameTick: 0,
processingGameTick: 0,
};
const writerWindowStart = Date.now();
@@ -418,29 +389,26 @@ describe('voteReward command', () => {
{ schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] } }
);
const legacyLateHandler = createTurnDaemonCommandHandler({ world: legacyLateWorld });
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')
? [
{
options: ['찬성'],
multipleOptions: 1,
endAt: null,
endTick: 0n,
closedAt: null,
},
]
: [],
} as any,
});
expect(legacyLateResult).toMatchObject({
type: 'voteReward',
ok: false,
reason: '설문조사가 종료되었습니다.',
});
const { processingGameTick: _processingGameTick, ...missingBoundaryCommand } = command;
await expect(
legacyLateHandler.handle(missingBoundaryCommand, {
db: {
...actorBindingDb(),
$queryRaw: async (query: { strings: readonly string[] }) =>
query.strings.join(' ').includes('SELECT options')
? [
{
options: ['찬성'],
multipleOptions: 1,
endAt: null,
endTick: 0n,
closedAt: null,
},
]
: [],
} as any,
})
).rejects.toThrow('authoritative daemon processing game tick');
});
it.each([
@@ -501,8 +469,8 @@ describe('voteReward command', () => {
voteId,
generalId: 1,
selection: [0],
acceptedGameTick,
},
processingGameTick: acceptedGameTick,
} as any,
{ db: commandDb as any }
);
@@ -602,7 +570,8 @@ describe('voteReward command', () => {
voteId: 1,
generalId: 1,
selection: [0],
},
processingGameTick: 0,
} as any,
{ db: commandDb as any }
);