fix: 부분 시계 메시지 만료의 재등장을 막는다

This commit is contained in:
2026-08-24 20:26:00 +00:00
parent 8872ce89b8
commit 77dc53b424
3 changed files with 33 additions and 4 deletions
+6 -1
View File
@@ -206,7 +206,12 @@ export const invalidateMessages = async (db: DatabaseClient, ids: number[]): Pro
where: { id: { in: uniqueIds } }, where: { id: { in: uniqueIds } },
data: { data: {
validUntil: gameTime.now, validUntil: gameTime.now,
...(gameTime.tick === null ? {} : { validUntilTick: BigInt(gameTime.tick) }), // A partially migrated profile can still carry a legacy logical
// sentinel even while no authoritative clock exists. Replace it
// with an already-expired logical tick when expiring by wall time;
// NULL would fall back to the wall timestamp after clock recovery
// and could make the handled message visible again.
validUntilTick: gameTime.tick === null ? 0n : BigInt(gameTime.tick),
}, },
}); });
}; };
@@ -15,7 +15,7 @@ import { InMemoryFlushStore } from '../src/auth/flushStore.js';
import { InMemoryBattleSimTransport } from '../src/battleSim/inMemoryTransport.js'; import { InMemoryBattleSimTransport } from '../src/battleSim/inMemoryTransport.js';
import type { GameApiContext } from '../src/context.js'; import type { GameApiContext } from '../src/context.js';
import { InMemoryTurnDaemonTransport } from '../src/daemon/inMemoryTransport.js'; import { InMemoryTurnDaemonTransport } from '../src/daemon/inMemoryTransport.js';
import { fetchMessagesFromMailbox } from '../src/messages/store.js'; import { fetchMessagesFromMailbox, invalidateMessages } from '../src/messages/store.js';
import { appRouter } from '../src/router.js'; import { appRouter } from '../src/router.js';
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL; const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
@@ -379,7 +379,7 @@ integration('diplomacy document message persistence', () => {
await expectInputEvent(chainedRequestId, 'sendLetter', fixtureUserId); await expectInputEvent(chainedRequestId, 'sendLetter', fixtureUserId);
}); });
it('keeps permanent messages readable while a profile has no logical clock', async () => { it('keeps permanent messages readable without a clock and does not resurrect them after invalidation', async () => {
const created = await appRouter const created = await appRouter
.createCaller(buildContext('legacy-clock-fallback', fixtureAuth)) .createCaller(buildContext('legacy-clock-fallback', fixtureAuth))
.diplomacy.sendLetter({ .diplomacy.sendLetter({
@@ -412,6 +412,20 @@ integration('diplomacy document message persistence', () => {
text: expect.stringContaining(`#${created.id}`), text: expect.stringContaining(`#${created.id}`),
}) })
); );
await invalidateMessages(db, [receiver.id]);
await expect(
db.message.findUniqueOrThrow({ where: { id: receiver.id }, select: { validUntilTick: true } })
).resolves.toEqual({ validUntilTick: 0n });
await expect(
fetchMessagesFromMailbox({
db,
mailbox: receiverMailbox,
msgType: 'diplomacy',
limit: 15,
fromSeq: 0,
})
).resolves.not.toContainEqual(expect.objectContaining({ id: receiver.id }));
} finally { } finally {
await db.worldState.update({ await db.worldState.update({
where: { id: fixtureWorldStateId }, where: { id: fixtureWorldStateId },
@@ -422,6 +436,16 @@ integration('diplomacy document message persistence', () => {
}, },
}); });
} }
await expect(
fetchMessagesFromMailbox({
db,
mailbox: receiverMailbox,
msgType: 'diplomacy',
limit: 15,
fromSeq: 0,
})
).resolves.not.toContainEqual(expect.objectContaining({ id: receiver.id }));
}); });
it('stores diplomacy and national copies for both approval and rejection responses', async () => { it('stores diplomacy and national copies for both approval and rejection responses', async () => {
+1 -1
View File
@@ -987,7 +987,7 @@ describe('messages router missing-flow compatibility', () => {
); );
expect(setup.messageUpdateMany).toHaveBeenCalledWith({ expect(setup.messageUpdateMany).toHaveBeenCalledWith({
where: { id: { in: [31] } }, where: { id: { in: [31] } },
data: { validUntil: expect.any(Date) }, data: { validUntil: expect.any(Date), validUntilTick: 0n },
}); });
expect(setup.queryRaw).toHaveBeenCalledTimes(9); expect(setup.queryRaw).toHaveBeenCalledTimes(9);
}); });