Files
core2026/packages/infra/test/readModelOutboxDispatcher.test.ts
T

98 lines
3.8 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import type { GamePrismaClient } from '../src/gamePrisma.js';
import {
claimReadModelOutboxBatch,
dispatchReadModelOutboxBatch,
pruneDeliveredReadModelOutbox,
} from '../src/readModelOutboxDispatcher.js';
const validPayload = {
version: 1,
changes: [['general.content', 7, '3']],
};
const createDb = (rows: readonly object[]) => {
const queryRaw = vi.fn().mockResolvedValue(rows);
const updateMany = vi.fn().mockResolvedValue({ count: 1 });
return {
db: { $queryRaw: queryRaw, readModelOutbox: { updateMany } } as unknown as GamePrismaClient,
queryRaw,
updateMany,
};
};
describe('read-model outbox dispatcher', () => {
it('claims a bounded lease with one statement and preserves delivery identity', async () => {
const fixture = createDb([{ id: 41n, payload: validPayload, attempts: 2 }]);
await expect(
claimReadModelOutboxBatch(fixture.db, {
owner: 'worker-a',
limit: 25,
leaseMs: 15_000,
now: new Date('2026-08-16T00:00:00.000Z'),
})
).resolves.toEqual([{ id: 41n, payload: validPayload, attempts: 2 }]);
expect(fixture.queryRaw).toHaveBeenCalledTimes(1);
});
it('publishes and acknowledges a valid payload', async () => {
const fixture = createDb([{ id: 1n, payload: validPayload, attempts: 1 }]);
const publish = vi.fn().mockResolvedValue(undefined);
const result = await dispatchReadModelOutboxBatch(fixture.db, publish, {
owner: 'worker-a',
now: () => new Date('2026-08-16T00:00:00.000Z'),
});
expect(result).toEqual({ claimed: 1, delivered: 1, failed: 0 });
expect(publish).toHaveBeenCalledWith(validPayload, 1n);
expect(fixture.updateMany).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: 1n, lockOwner: 'worker-a', deliveredAt: null },
data: expect.objectContaining({ deliveredAt: new Date('2026-08-16T00:00:00.000Z') }),
})
);
});
it('releases failed and malformed rows with bounded retry state', async () => {
const fixture = createDb([
{ id: 1n, payload: validPayload, attempts: 3 },
{ id: 2n, payload: { version: 99 }, attempts: 1 },
]);
const publish = vi.fn().mockRejectedValue(new Error('redis unavailable'));
const result = await dispatchReadModelOutboxBatch(fixture.db, publish, {
owner: 'worker-a',
retryBaseMs: 1_000,
retryMaxMs: 10_000,
now: () => new Date('2026-08-16T00:00:00.000Z'),
});
expect(result).toEqual({ claimed: 2, delivered: 0, failed: 2 });
expect(publish).toHaveBeenCalledTimes(1);
expect(fixture.updateMany).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
where: { id: 1n, lockOwner: 'worker-a', deliveredAt: null },
data: expect.objectContaining({ availableAt: new Date('2026-08-16T00:00:04.000Z') }),
})
);
expect(fixture.updateMany).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
where: { id: 2n, lockOwner: 'worker-a', deliveredAt: null },
data: expect.objectContaining({ availableAt: new Date('2026-08-16T00:00:01.000Z') }),
})
);
});
it('prunes only a bounded delivered batch', async () => {
const fixture = createDb([{ id: 1n }, { id: 2n }]);
await expect(
pruneDeliveredReadModelOutbox(fixture.db, {
deliveredBefore: new Date('2026-08-15T00:00:00.000Z'),
limit: 100,
})
).resolves.toBe(2);
});
});