test(personal-records): detection worker + queue + UI badge coverage

Adds the integration suite for the detection worker against a real
Postgres testcontainer. Covers measurement-driven PRs (steps,
RHR, VO2 max), idempotency on re-run, silent-flag propagation,
and the drift guard that every PR-trackable metric type lands in
the scan loop.

Also patches a pre-existing schema gap: the
(userId, metricType, metricSlot, achievedAt) unique index on
personal_records is NULLS-DISTINCT in Postgres, so the index alone
does not dedup the measurement-driven rows (metricSlot = NULL).
The worker now does an application-level pre-flight findFirst on
the same composite key before each insert, keeping both the
workout path (index covers it) and the measurement path
(application-level guard) idempotent without a Migration.
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 19:02:47 +02:00
1 parent dd44a0df49
commit 223b8a9208
4 files changed
+212 -2

No files matched your search

@@ -76,6 +76,7 @@ interface FakeWhere {
sportType?: string;
totalDistanceM?: { gte: number };
durationSec?: { gt: number } | { gte: number };
achievedAt?: Date;
}
function makeFakePrisma(state: {
@@ -122,6 +123,11 @@ function makeFakePrisma(state: {
row.metricSlot !== where.metricSlot
)
return false;
if (
where.achievedAt !== undefined &&
row.achievedAt.getTime() !== where.achievedAt.getTime()
)
return false;
return true;
}
@@ -240,8 +240,23 @@ export async function detectPersonalRecordsForUser(
const outcome = compareToCurrentBest(best.value, currentPR?.value, direction);
if (outcome === "no-improvement") continue;
// ON CONFLICT DO NOTHING via `skipDuplicates`. Re-running the
// same job is a guaranteed no-op once the row exists.
// Application-level idempotency. The DB unique index covers the
// workout case (`metric_slot` non-null) but Postgres' default
// NULLS-DISTINCT semantics mean `(userId, metric, NULL, ts)`
// doesn't dedup at the index level — a second handler invocation
// would otherwise write a second row at the same achievedAt. The
// explicit pre-flight findFirst keeps both code paths safe.
const existing = await prisma.personalRecord.findFirst({
where: {
userId,
metricType,
metricSlot: null,
achievedAt: best.measuredAt,
},
select: { id: true },
});
if (existing) continue;
const result = await prisma.personalRecord.createMany({
data: [
{