mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Two trigger fixes so the new achievements actually fire end-to-end. The doctor-PDF hidden Easter-egg used to watch a non-existent `doctor-report.export` audit action. The real route emits two: `doctor-report.generate` (the JSON helper) and `doctor-report.pdf.generate` (the PDF route). Either now counts. The locale-flip hidden Easter-egg used to watch `settings.locale.update`, which no endpoint emits — the locale field shares the catch-all `profile.update` audit row with every other profile field. Profile-update now emits a separate `settings.locale.update` row when (and only when) the locale value actually changes (compares the new value to the prior persisted locale, suppresses the row when they match), with the from/to values in the details payload for traceability. Already-existing TanStack Query invalidation map covers the new metrics: `measurementDependentKeys`, `moodDependentKeys`, and `medicationDependentKeys` already include `queryKeys.gamificationAchievements()`, so any measurement / mood / intake mutation triggers a refetch and the route persists the new unlock to `UserAchievement` (existing v1.4.15 mechanism). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
181 lines
5.2 KiB
TypeScript
181 lines
5.2 KiB
TypeScript
/**
|
|
* Integration test for the v1.4.18 achievement expansion.
|
|
*
|
|
* Drives a fresh Postgres user through the moments when expansion
|
|
* predicates fire, verifying:
|
|
* - mood entries persisted as `MoodEntry` rows are picked up by
|
|
* `getMoodMetrics` (the row shape uses YYYY-MM-DD strings, not
|
|
* Date objects, which is the most common drift surface)
|
|
* - measurement counts are split by `MeasurementType` correctly
|
|
* - earnability flags reflect what the user actually has
|
|
*
|
|
* We don't drive the full HTTP route here because the existing
|
|
* `medication_schedules.days_of_week` migration drift makes the route
|
|
* fail in the integration container; that's a pre-existing issue
|
|
* tracked separately. Instead we round-trip the prisma rows through
|
|
* `buildExpansionMetricValues` and `getEarnabilityFlags`, which is
|
|
* the same code-path the route exercises after the fetch.
|
|
*/
|
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
buildExpansionMetricValues,
|
|
getEarnabilityFlags,
|
|
} from "@/lib/gamification/expansion-metrics";
|
|
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(getPrismaClient());
|
|
});
|
|
|
|
describe("achievements expansion — fresh user, real prisma rows", () => {
|
|
it("recognises a single mood entry as `hasMood: true`", async () => {
|
|
const prisma = getPrismaClient();
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
username: "mood-tracker",
|
|
email: "mood@example.test",
|
|
createdAt: new Date("2026-01-01T00:00:00Z"),
|
|
},
|
|
});
|
|
await prisma.moodEntry.create({
|
|
data: {
|
|
userId: user.id,
|
|
date: "2026-04-15",
|
|
mood: "GUT",
|
|
score: 4,
|
|
moodLoggedAt: new Date("2026-04-15T08:00:00Z"),
|
|
},
|
|
});
|
|
|
|
const moodEntries = await prisma.moodEntry.findMany({
|
|
where: { userId: user.id },
|
|
select: { date: true, score: true, moodLoggedAt: true },
|
|
});
|
|
|
|
const result = buildExpansionMetricValues({
|
|
measurements: [],
|
|
moodEntries,
|
|
intakeEvents: [],
|
|
auditEvents: [],
|
|
});
|
|
|
|
expect(result.moodEntryCount).toBe(1);
|
|
|
|
const flags = getEarnabilityFlags({
|
|
hasMedication: false,
|
|
moodEntryCount: result.moodEntryCount,
|
|
measurementCounts: {
|
|
weightCount: result.weightMeasurementCount,
|
|
bpCount: result.bpMeasurementCount,
|
|
pulseCount: result.pulseMeasurementCount,
|
|
},
|
|
});
|
|
expect(flags.hasMood).toBe(true);
|
|
expect(flags.hasWeight).toBe(false);
|
|
});
|
|
|
|
it("counts measurements by type from real DB rows", async () => {
|
|
const prisma = getPrismaClient();
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
username: "many-readings",
|
|
email: "readings@example.test",
|
|
createdAt: new Date("2026-01-01T00:00:00Z"),
|
|
},
|
|
});
|
|
await prisma.measurement.createMany({
|
|
data: [
|
|
{
|
|
userId: user.id,
|
|
type: "WEIGHT",
|
|
value: 80,
|
|
unit: "kg",
|
|
measuredAt: new Date("2026-04-01T08:00:00Z"),
|
|
},
|
|
{
|
|
userId: user.id,
|
|
type: "WEIGHT",
|
|
value: 80.5,
|
|
unit: "kg",
|
|
measuredAt: new Date("2026-04-02T08:00:00Z"),
|
|
},
|
|
{
|
|
userId: user.id,
|
|
type: "BLOOD_PRESSURE_SYS",
|
|
value: 120,
|
|
unit: "mmHg",
|
|
measuredAt: new Date("2026-04-01T08:00:00Z"),
|
|
},
|
|
{
|
|
userId: user.id,
|
|
type: "BLOOD_PRESSURE_DIA",
|
|
value: 80,
|
|
unit: "mmHg",
|
|
measuredAt: new Date("2026-04-01T08:00:00Z"),
|
|
},
|
|
{
|
|
userId: user.id,
|
|
type: "PULSE",
|
|
value: 65,
|
|
unit: "bpm",
|
|
measuredAt: new Date("2026-04-03T08:00:00Z"),
|
|
},
|
|
],
|
|
});
|
|
|
|
const measurements = await prisma.measurement.findMany({
|
|
where: {
|
|
userId: user.id,
|
|
type: {
|
|
in: ["WEIGHT", "BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", "PULSE"],
|
|
},
|
|
},
|
|
select: { type: true, measuredAt: true },
|
|
});
|
|
|
|
const result = buildExpansionMetricValues({
|
|
measurements,
|
|
moodEntries: [],
|
|
intakeEvents: [],
|
|
auditEvents: [],
|
|
});
|
|
|
|
expect(result.weightMeasurementCount).toBe(2);
|
|
expect(result.bpMeasurementCount).toBe(1); // SYS canonical, DIA not double-counted
|
|
expect(result.pulseMeasurementCount).toBe(1);
|
|
});
|
|
|
|
it("fires the doctor-PDF hidden trigger from a real audit-log row", async () => {
|
|
const prisma = getPrismaClient();
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
username: "exporter",
|
|
email: "exporter@example.test",
|
|
createdAt: new Date("2026-01-01T00:00:00Z"),
|
|
},
|
|
});
|
|
await prisma.auditLog.create({
|
|
data: {
|
|
userId: user.id,
|
|
action: "doctor-report.pdf.generate",
|
|
},
|
|
});
|
|
|
|
const auditEvents = await prisma.auditLog.findMany({
|
|
where: { userId: user.id, action: "doctor-report.pdf.generate" },
|
|
select: { action: true, createdAt: true },
|
|
});
|
|
|
|
const result = buildExpansionMetricValues({
|
|
measurements: [],
|
|
moodEntries: [],
|
|
intakeEvents: [],
|
|
auditEvents,
|
|
});
|
|
|
|
expect(result.doctorPdfCount).toBe(1);
|
|
});
|
|
});
|