131 lines
5.5 KiB
TypeScript
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) });
|
|
});
|
|
});
|