feat: synchronize account icons across game profiles
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
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) });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user