Files
core2026/app/game-api/test/accountIconResetReconciler.test.ts
T

131 lines
5.5 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import type { DatabaseClient } from '../src/context.js';
import { InMemoryTurnDaemonTransport } from '../src/daemon/inMemoryTransport.js';
import { AccountIconResetReconciler } from '../src/services/accountIconResetReconciler.js';
const reset = (userId: string) => ({
userId,
resetRevision: '2026-07-31T09:00:00.001Z',
current: {
revision: '2026-07-31T09:00:00.002Z',
picture: 'newer.png',
imageServer: 1,
},
});
describe('AccountIconResetReconciler', () => {
it('replays stale resets, skips newer watermarks, and bootstraps matching post-reset tuples', async () => {
const db = {
general: {
findMany: vi.fn(async () => [
{
id: 1,
userId: 'needs-reset',
picture: 'old.png',
imageServer: 1,
meta: { accountIconUpdatedAt: '2026-07-31T08:59:59.999Z' },
},
{
id: 2,
userId: 'already-newer',
picture: 'newer.png',
imageServer: 1,
meta: { accountIconUpdatedAt: '2026-07-31T09:00:00.002Z' },
},
{
id: 3,
userId: 'legacy-watermark',
picture: 'newer.png',
imageServer: 1,
meta: {},
},
]),
},
inputEvent: {
findMany: vi.fn(async () => []),
},
} as unknown as DatabaseClient;
const source = {
listResets: vi.fn(async () => [reset('needs-reset'), reset('already-newer'), reset('legacy-watermark')]),
};
const transport = new InMemoryTurnDaemonTransport();
const reconciler = new AccountIconResetReconciler(db, source, transport, 30_000);
await reconciler.reconcileOnce();
expect(source.listResets).toHaveBeenCalledWith(['needs-reset', 'already-newer', 'legacy-watermark']);
expect(transport.commands.map(({ command }) => command)).toEqual([
{
type: 'adjustGeneralIcon',
requestId: 'general:adjustIcon:needs-reset:2026-07-31T09:00:00.001Z',
userId: 'needs-reset',
picture: 'default.jpg',
imageServer: 0,
iconRevision: '2026-07-31T09:00:00.001Z',
},
{
type: 'adjustGeneralIcon',
requestId: 'general:adjustIcon:legacy-watermark:2026-07-31T09:00:00.002Z',
userId: 'legacy-watermark',
picture: 'newer.png',
imageServer: 1,
iconRevision: '2026-07-31T09:00:00.002Z',
},
]);
expect(reconciler.getHealth()).toMatchObject({ lastSuccessAt: expect.any(String), lastError: null });
});
it('requeues terminal events and isolates a persistent user failure from later users', async () => {
const db = {
general: {
findMany: vi.fn(async () =>
['terminal', 'broken', 'later'].map((userId, index) => ({
id: index + 1,
userId,
picture: 'old.png',
imageServer: 1,
meta: {},
}))
),
},
inputEvent: {
findMany: vi.fn(async ({ where }: { where: { OR: Array<{ requestId: unknown }> } }) => {
const first = where.OR[0]?.requestId;
if (first === 'general:adjustIcon:broken:2026-07-31T09:00:00.001Z') {
throw new Error('broken event lookup');
}
if (first === 'general:adjustIcon:terminal:2026-07-31T09:00:00.001Z') {
return [
{
requestId: first,
status: 'FAILED',
eventType: 'adjustGeneralIcon',
payload: {
type: 'adjustGeneralIcon',
requestId: first,
userId: 'terminal',
picture: 'default.jpg',
imageServer: 0,
iconRevision: '2026-07-31T09:00:00.001Z',
},
},
];
}
return [];
}),
},
} as unknown as DatabaseClient;
const source = { listResets: vi.fn(async () => ['terminal', 'broken', 'later'].map(reset)) };
const transport = new InMemoryTurnDaemonTransport();
const reconciler = new AccountIconResetReconciler(db, source, transport, 30_000);
await expect(reconciler.reconcileOnce()).rejects.toThrow('1 account icon reset reconciliation');
expect(transport.commands.map(({ command }) => command.requestId)).toEqual([
'general:adjustIcon:terminal:2026-07-31T09:00:00.001Z:retry:1',
'general:adjustIcon:later:2026-07-31T09:00:00.001Z',
]);
expect(reconciler.getHealth()).toMatchObject({ lastErrorAt: expect.any(String) });
});
});