Files
ApiCore/ApiCore-Backend/src/common/outbox/outbox-claims.spec.ts
T
Nick SzaunigandClaude Opus 5.5 1971b2d9c4 feat: T4b/T4c wave 8 - weather history, phenology, honey chain, Telegram base
- Weather history from Open-Meteo behind an adapter: worker loop
  phenology-weather with backoff, Retry-After, User-Agent, chunked
  writes and forecast-to-archive correction
- Phenology: growing degree sum per DWD, phase from reports, GTS and
  calendar, calibration from previous years, daily job step,
  SeasonService reads the stored state
- Honey chain: prefill for extractions and batches (proposals only),
  plausibility and withdrawal warnings, Extraction lock, chain events
  and batch.created webhook from HoneyBatchRecorded
- Telegram base: schema, NotificationDelivery outbox, hardened bot
  client, startup check without crash, no-network-outside-loops guard
- ARCHITECTURE.md: sections 11.11-11.13, assumptions 42-46, env table

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-14 20:00:46 +02:00

166 lines
4.3 KiB
TypeScript

import { Prisma } from '../../../generated/prisma';
import { deferClaim, settleSkipped, settleSuccess } from './outbox-claims';
function fakeDb(count = 1) {
return { $executeRaw: jest.fn().mockResolvedValue(count) };
}
function sqlOf(db: ReturnType<typeof fakeDb>): Prisma.Sql {
return (db.$executeRaw.mock.calls as [Prisma.Sql][])[0][0];
}
describe('NotificationDelivery outbox helpers', () => {
const claimedAt = new Date('2026-10-15T10:00:00.000Z');
it('settleSuccess settles NotificationDelivery as DELIVERED into deliveredAt', async () => {
const db = fakeDb();
await expect(
settleSuccess(
db as never,
'NotificationDelivery',
'd1',
claimedAt,
'DELIVERED',
),
).resolves.toBe(true);
const query = sqlOf(db);
expect(query.sql).toContain('UPDATE "NotificationDelivery"');
expect(query.sql).toContain('"deliveredAt" = now()');
expect(query.sql).toContain('"NotificationDeliveryStatus"');
});
it('settleSuccess refuses DONE for NotificationDelivery', async () => {
await expect(
settleSuccess(
fakeDb() as never,
'NotificationDelivery',
'd1',
claimedAt,
'DONE',
),
).rejects.toThrow('NotificationDelivery settles as DELIVERED, not DONE');
});
it('settleSkipped is compare-and-set on id and claimedAt and keeps attempts', async () => {
const db = fakeDb();
await expect(
settleSkipped(
db as never,
'NotificationDelivery',
'd1',
claimedAt,
'chat gone',
),
).resolves.toBe(true);
const query = sqlOf(db);
expect(query.sql).toContain('UPDATE "NotificationDelivery"');
expect(query.sql).toContain(`CAST(? AS "NotificationDeliveryStatus")`);
expect(query.sql).toContain(
`WHERE "id" = ? AND "status" = 'PROCESSING' AND "claimedAt" = ?`,
);
expect(query.sql).not.toContain('"attempts"');
expect(query.values).toEqual(['SKIPPED', 'chat gone', 'd1', claimedAt]);
});
it('settleSkipped reports a lost claim', async () => {
await expect(
settleSkipped(
fakeDb(0) as never,
'NotificationDelivery',
'd1',
claimedAt,
'x',
),
).resolves.toBe(false);
});
it('deferClaim puts the entry back to PENDING after the delay by the database clock, attempts unchanged, compare-and-set', async () => {
const db = fakeDb();
await expect(
deferClaim(
db as never,
'NotificationDelivery',
'd1',
claimedAt,
15 * 60_000,
),
).resolves.toBe(true);
const query = sqlOf(db);
expect(query.sql).toContain('UPDATE "NotificationDelivery"');
expect(query.sql).toContain(`"status" = 'PENDING'`);
expect(query.sql).toContain(
`"availableAt" = now() + (? * interval '1 millisecond')`,
);
expect(query.sql).toContain('"claimedAt" = NULL');
expect(query.sql).not.toContain('"attempts"');
expect(query.sql).toContain(
`WHERE "id" = ? AND "status" = 'PROCESSING' AND "claimedAt" = ?`,
);
expect(query.values).toEqual([900_000, 'd1', claimedAt]);
});
it('deferClaim reports a lost claim and rejects a negative or non-integer delay', async () => {
await expect(
deferClaim(
fakeDb(0) as never,
'NotificationDelivery',
'd1',
claimedAt,
1,
),
).resolves.toBe(false);
await expect(
deferClaim(
fakeDb() as never,
'NotificationDelivery',
'd1',
claimedAt,
-1,
),
).rejects.toThrow(RangeError);
await expect(
deferClaim(
fakeDb() as never,
'NotificationDelivery',
'd1',
claimedAt,
1.5,
),
).rejects.toThrow(RangeError);
});
it('deferClaim caps the delay at 24 hours', async () => {
const db = fakeDb();
await deferClaim(
db as never,
'NotificationDelivery',
'd1',
claimedAt,
30 * 24 * 3_600_000,
);
expect(sqlOf(db).values[0]).toBe(24 * 3_600_000);
});
it('settleSkipped truncates the reason to 200 characters', async () => {
const db = fakeDb();
await settleSkipped(
db as never,
'NotificationDelivery',
'd1',
claimedAt,
'r'.repeat(500),
);
expect(sqlOf(db).values[1]).toBe('r'.repeat(200));
});
});