fix: 부분 시계 메시지 만료의 재등장을 막는다
This commit is contained in:
@@ -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 () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user