Files
ApiCore/ApiCore-Backend/test/outbox-claims.e2e-spec.ts
T
Nick.RosenloecherandClaude Opus 5.5 6edae3efec
Backend E2E / e2e (push) Failing after 7m26s
Backend Image / build-and-push (push) Failing after 4m20s
Frontend Image / build-and-push (push) Successful in 15m44s
test: Backend-e2e in CI stabil (eigene Jahre, Sperren, ein Server je App)
- daily-evaluation nutzt die Jahre 2011/2012 statt 2001, damit der Job nicht
  die Daten von autopilot und reminders liest und verändert.
- throttler: app.listen(0) einmal je App statt eines Servers je Anfrage
  (ECONNRESET bei 40 parallelen Anfragen).
- outbox-claims: 120 s Zeitlimit für 2000 sequentielle Anweisungen.
- migration-harness: eine `migrate deploy` zur Zeit über eine eigene
  Advisory-Sperre (P1002 bei parallelen Migrations-Suites).
- authority-notices und authority-notices-herd laufen über
  exclusive-suite nacheinander; Herd-Wiederholung räumt eigene Entwürfe.

Refs #32

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

66 lines
2.2 KiB
TypeScript

import { Test, TestingModule } from '@nestjs/testing';
import { Prisma } from '../generated/prisma';
import { claimBatch } from '../src/common/outbox/outbox-claims';
import { PrismaModule } from '../src/prisma/prisma.module';
import { PrismaService } from '../src/prisma/prisma.service';
const ROUNDS = 1000;
// 2 x ROUNDS sequential statements: seconds locally, but on a loaded CI
// database the default 20 s is too tight. The rounds themselves stay (they are
// what catches the rounding race).
const ROUNDS_TIMEOUT_MS = 120_000;
/**
* "availableAt" is TIMESTAMP(3): a value written from SQL now() (stale
* release, retry backoff, requeue) is ROUNDED to milliseconds, so it can lie
* up to half a millisecond after the next statement's now(). A claim right
* behind such a write must still find the row.
*/
describe('claimBatch and millisecond rounding (e2e)', () => {
const correlationId = `claim-rounding-${Date.now()}-${Math.round(Math.random() * 1e6)}`;
let moduleRef: TestingModule;
let prisma: PrismaService;
beforeAll(async () => {
moduleRef = await Test.createTestingModule({
imports: [PrismaModule],
}).compile();
await moduleRef.init();
prisma = moduleRef.get(PrismaService);
});
afterAll(async () => {
await prisma.domainEvent.deleteMany({ where: { correlationId } });
await moduleRef.close();
});
it(
'claims an entry whose availableAt was just set to now()',
async () => {
const event = await prisma.domainEvent.create({
data: {
type: 'TaskDue',
aggregateType: 'Task',
aggregateId: correlationId,
payload: {},
correlationId,
},
});
let missed = 0;
for (let round = 0; round < ROUNDS; round++) {
await prisma.$executeRaw`UPDATE "DomainEvent" SET "status" = 'PENDING', "claimedAt" = NULL, "availableAt" = now() WHERE "id" = ${event.id}`;
const claimed = await claimBatch(prisma, 'DomainEvent', {
limit: 1,
filter: Prisma.sql`"correlationId" = ${correlationId}`,
orderBy: 'occurredAt',
});
if (claimed.length !== 1) {
missed++;
}
}
expect(missed).toBe(0);
},
ROUNDS_TIMEOUT_MS,
);
});