Files
HealthLog/tests/integration/source-ladder-new-sources.test.ts
T

127 lines
4.5 KiB
TypeScript

/**
* The sources that had no place on a ladder, against real Postgres through
* the SQL collapse every 90-day aggregate reads (`canonicalMeasurementsFrom`).
*
* A source a ladder does not name ranks 90, below everything it does name:
* a weight typed as a reply to a Telegram reminder lost its day to a WHOOP
* strap's estimate, and a Health Connect day lost to a hand-typed one. The
* hand-entered sources now rank with MANUAL, Health Connect with Apple
* Health, and a day between two sources that were on the ladder before
* resolves exactly as it did.
*/
import { beforeAll, describe, expect, it } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
import type {
MeasurementSource,
MeasurementType,
} from "@/generated/prisma/client";
import {
buildSourceRankCase,
canonicalMeasurementsFrom,
} from "@/lib/analytics/source-rank-sql";
import { boundInstantSql } from "@/lib/analytics/request-clock-sql";
const prisma = getPrismaClient();
const USER = "ladder-new-sources";
const DAY = 86_400_000;
const today = Math.floor(Date.now() / DAY) * DAY;
/** One reading per (type, source) on day `n` days back, at 08:00 / 09:00 UTC. */
function reading(
type: MeasurementType,
source: MeasurementSource,
n: number,
value: number,
hour = 8,
) {
return {
userId: USER,
type,
source,
value,
unit: type === "WEIGHT" ? "kg" : "count",
measuredAt: new Date(today - n * DAY + hour * 3_600_000),
};
}
async function canonicalSources(): Promise<Map<string, string[]>> {
const rank = buildSourceRankCase(null, '"type"', '"source"');
const rows = await prisma.$queryRawUnsafe<
Array<{ type: string; day: Date; source: string }>
>(
`SELECT DISTINCT m."type"::text AS type,
date_trunc('day', m."measured_at") AS day,
m."source"::text AS source
FROM ${canonicalMeasurementsFrom(rank, { from: boundInstantSql(2), interval: "90 days" })}
ORDER BY 1, 2, 3`,
USER,
new Date(),
);
const out = new Map<string, string[]>();
for (const r of rows) {
const key = `${r.type}@${r.day.toISOString().slice(0, 10)}`;
out.set(key, [...(out.get(key) ?? []), r.source]);
}
return out;
}
const dayKey = (n: number) =>
new Date(today - n * DAY).toISOString().slice(0, 10);
beforeAll(async () => {
await truncateAllTables(prisma);
await prisma.user.create({
data: { id: USER, username: USER, timezone: "UTC" },
});
await prisma.measurement.createMany({
data: [
// Day 1: a strap's weight estimate and a typed reply to a reminder.
reading("WEIGHT", "WHOOP", 1, 84),
reading("WEIGHT", "TELEGRAM", 1, 82.5, 9),
// Day 2: an assistant logging on the person's word beside a Fitbit
// scale-less estimate.
reading("WEIGHT", "FITBIT", 2, 85),
reading("WEIGHT", "MCP", 2, 82.4, 9),
// Day 3: a hand-typed weight and the person's own bridged scale.
reading("WEIGHT", "MANUAL", 3, 82.3),
reading("WEIGHT", "EXTERNAL", 3, 82.6, 9),
// Day 4: two sources the ladder always named: unchanged.
reading("WEIGHT", "APPLE_HEALTH", 4, 82.2),
reading("WEIGHT", "WITHINGS", 4, 82.1, 9),
// Day 1: Health Connect's daily steps beside a hand-typed count.
reading("ACTIVITY_STEPS", "HEALTH_CONNECT", 1, 9000),
reading("ACTIVITY_STEPS", "MANUAL", 1, 5000, 9),
// Day 2: Apple Health still outranks Health Connect.
reading("ACTIVITY_STEPS", "APPLE_HEALTH", 2, 9100),
reading("ACTIVITY_STEPS", "HEALTH_CONNECT", 2, 8900, 9),
],
});
}, 120_000);
describe("source ladders for the sources that had no place", () => {
it("ranks a reading the person entered through Telegram or MCP above a device estimate", async () => {
const picked = await canonicalSources();
expect(picked.get(`WEIGHT@${dayKey(1)}`)).toEqual(["TELEGRAM"]);
expect(picked.get(`WEIGHT@${dayKey(2)}`)).toEqual(["MCP"]);
});
it("keeps MANUAL above the person's own bridge", async () => {
const picked = await canonicalSources();
expect(picked.get(`WEIGHT@${dayKey(3)}`)).toEqual(["MANUAL"]);
});
it("ranks Health Connect right after Apple Health", async () => {
const picked = await canonicalSources();
expect(picked.get(`ACTIVITY_STEPS@${dayKey(1)}`)).toEqual([
"HEALTH_CONNECT",
]);
expect(picked.get(`ACTIVITY_STEPS@${dayKey(2)}`)).toEqual(["APPLE_HEALTH"]);
});
it("resolves a day between two long-ranked sources exactly as before", async () => {
const picked = await canonicalSources();
expect(picked.get(`WEIGHT@${dayKey(4)}`)).toEqual(["WITHINGS"]);
});
});