mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
168 lines
4.6 KiB
TypeScript
168 lines
4.6 KiB
TypeScript
/**
|
|
* The activity calendar's reader against a real Postgres (D1036).
|
|
*
|
|
* - steps resolve to the source the person's ladder picks per day, so a day
|
|
* two devices counted shows one device's total, never the sum;
|
|
* - the day is the person's local day: a reading at 23:30 Berlin time lands
|
|
* on that day, not on the UTC day after;
|
|
* - a workout two devices recorded counts once, its minutes once;
|
|
* - the window is exact: nothing before `fromKey` or after `toKey`.
|
|
*/
|
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
import { readActivityDays } from "@/lib/activity/calendar-read";
|
|
import { invalidateUserTimezone } from "@/lib/tz/resolver";
|
|
import { userDayKey } from "@/lib/tz/format";
|
|
|
|
const prisma = getPrismaClient();
|
|
const TZ = "Europe/Berlin";
|
|
const USER = "activity-reader";
|
|
const DAY = 86_400_000;
|
|
const NOW = new Date();
|
|
|
|
function daysAgo(n: number, hourUtc = 10): Date {
|
|
const d = new Date(NOW.getTime() - n * DAY);
|
|
d.setUTCHours(hourUtc, 0, 0, 0);
|
|
return d;
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
await truncateAllTables(prisma);
|
|
await prisma.user.create({
|
|
data: {
|
|
id: USER,
|
|
username: USER,
|
|
timezone: TZ,
|
|
sourcePriorityJson: {
|
|
metricPriority: { steps: ["FITBIT", "APPLE_HEALTH"] },
|
|
},
|
|
},
|
|
});
|
|
invalidateUserTimezone(USER);
|
|
|
|
await prisma.measurement.createMany({
|
|
data: [
|
|
// Day 3: both devices; the ladder picks Fitbit's 8 000.
|
|
{
|
|
userId: USER,
|
|
type: "ACTIVITY_STEPS",
|
|
value: 9500,
|
|
unit: "steps",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: daysAgo(3),
|
|
},
|
|
{
|
|
userId: USER,
|
|
type: "ACTIVITY_STEPS",
|
|
value: 8000,
|
|
unit: "steps",
|
|
source: "FITBIT",
|
|
measuredAt: daysAgo(3),
|
|
},
|
|
// Day 2: Apple only, two slices that add up.
|
|
{
|
|
userId: USER,
|
|
type: "ACTIVITY_STEPS",
|
|
value: 3000,
|
|
unit: "steps",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: daysAgo(2, 8),
|
|
},
|
|
{
|
|
userId: USER,
|
|
type: "ACTIVITY_STEPS",
|
|
value: 4000,
|
|
unit: "steps",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: daysAgo(2, 15),
|
|
},
|
|
// 21:30 UTC is 23:30 in Berlin (summer) or 22:30 (winter): still day 5 locally.
|
|
{
|
|
userId: USER,
|
|
type: "ACTIVITY_STEPS",
|
|
value: 1200,
|
|
unit: "steps",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: daysAgo(5, 21),
|
|
},
|
|
{
|
|
userId: USER,
|
|
type: "EXERCISE_MINUTES",
|
|
value: 25,
|
|
unit: "min",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: daysAgo(3, 9),
|
|
},
|
|
{
|
|
userId: USER,
|
|
type: "EXERCISE_MINUTES",
|
|
value: 15,
|
|
unit: "min",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: daysAgo(3, 17),
|
|
},
|
|
// Outside the window asked for below.
|
|
{
|
|
userId: USER,
|
|
type: "ACTIVITY_STEPS",
|
|
value: 777,
|
|
unit: "steps",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: daysAgo(40),
|
|
},
|
|
],
|
|
});
|
|
|
|
const start = daysAgo(3, 7);
|
|
await prisma.workout.createMany({
|
|
data: [
|
|
{
|
|
userId: USER,
|
|
sportType: "running",
|
|
startedAt: start,
|
|
endedAt: new Date(start.getTime() + 40 * 60_000),
|
|
durationSec: 2400,
|
|
source: "APPLE_HEALTH",
|
|
externalId: "hk-1",
|
|
},
|
|
{
|
|
userId: USER,
|
|
sportType: "running",
|
|
startedAt: new Date(start.getTime() + 30_000),
|
|
endedAt: new Date(start.getTime() + 40 * 60_000),
|
|
durationSec: 2370,
|
|
source: "FITBIT",
|
|
externalId: "fb-1",
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
describe("readActivityDays", () => {
|
|
it("follows the ladder, the local day and the workout collapse", async () => {
|
|
const fromKey = userDayKey(daysAgo(10), TZ);
|
|
const toKey = userDayKey(NOW, TZ);
|
|
const { days } = await readActivityDays({
|
|
userId: USER,
|
|
tz: TZ,
|
|
fromKey,
|
|
toKey,
|
|
includeWorkouts: true,
|
|
});
|
|
const by = new Map(days.map((d) => [d.date, d]));
|
|
|
|
const d3 = by.get(userDayKey(daysAgo(3), TZ));
|
|
expect(d3?.steps).toBe(8000);
|
|
expect(d3?.exerciseMinutes).toBe(40);
|
|
expect(d3?.workoutCount).toBe(1);
|
|
expect(d3?.workoutsBySport).toEqual({ running: 1 });
|
|
expect(d3?.workoutMinutes).toBe(40);
|
|
|
|
expect(by.get(userDayKey(daysAgo(2), TZ))?.steps).toBe(7000);
|
|
expect(by.get(userDayKey(daysAgo(5, 21), TZ))?.steps).toBe(1200);
|
|
expect(days.every((d) => d.date >= fromKey && d.date <= toKey)).toBe(true);
|
|
expect(days.some((d) => d.steps === 777)).toBe(false);
|
|
});
|
|
});
|