fix(jobs): hand the reaction line its new datum in the reader's units

The reaction line read the arrived weight with its stored unit (kg) and
a workout distance in metres, whatever the reader had chosen, so the
sentence written about the new reading could quote kilograms to a
reader on pounds. The evidence now converts the reading and the
distance into the reader's units before the prompt is built.

Refs #1067
This commit is contained in:
Marc-André Bombeck committed 2026-10-01 13:23:17 +02:00
1 parent 7cb1b5ba05
commit 43be8ebd43
2 files changed
+107 -8

No files matched your search

@@ -0,0 +1,80 @@
/**
* The datum a reaction line reacts to is handed to the model in the reader's
* units, so the line cannot quote kilograms to a reader on pounds.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
const measurementFindMany = vi.fn();
const workoutFindFirst = vi.fn();
vi.mock("@/lib/db", () => ({
prisma: {
measurement: {
findMany: (...a: unknown[]) => measurementFindMany(...a),
},
workout: { findFirst: (...a: unknown[]) => workoutFindFirst(...a) },
},
}));
import { loadArrivalEvidence } from "../reaction-line";
const row = { occurredAt: new Date("2026-09-30T07:00:00Z"), refId: "w1" };
const imperial = {
timezone: "UTC",
sourcePriorityJson: null,
unitPreference: "imperial",
glucoseUnit: null,
};
beforeEach(() => {
measurementFindMany.mockReset();
workoutFindFirst.mockReset();
});
describe("loadArrivalEvidence — the reader's units", () => {
it("states a new weight in pounds for an imperial reader", async () => {
measurementFindMany.mockResolvedValue([{ type: "WEIGHT", value: 80 }]);
const evidence = await loadArrivalEvidence(
{ userId: "u1", kind: "weight", localDate: "2026-09-30", revision: "r" },
row,
imperial,
);
expect(evidence).toBe("- Newly arrived reading: WEIGHT 176.4 lb.");
});
it("states blood pressure in mmHg for every reader", async () => {
measurementFindMany.mockResolvedValue([
{ type: "BLOOD_PRESSURE_DIA", value: 80 },
{ type: "BLOOD_PRESSURE_SYS", value: 125 },
]);
const evidence = await loadArrivalEvidence(
{
userId: "u1",
kind: "blood_pressure",
localDate: "2026-09-30",
revision: "r",
},
row,
imperial,
);
expect(evidence).toContain("BLOOD_PRESSURE_SYS 125 mmHg.");
});
it("states a workout distance in miles, not metres", async () => {
measurementFindMany.mockResolvedValue([]);
workoutFindFirst.mockResolvedValue({
sportType: "running",
durationSec: 1800,
totalDistanceM: 5000,
totalEnergyKcal: 300,
avgHeartRate: 150,
});
const evidence = await loadArrivalEvidence(
{ userId: "u1", kind: "workout", localDate: "2026-09-30", revision: "r" },
row,
imperial,
);
expect(evidence).toContain("distance 3.11 mi;");
expect(evidence).not.toContain("metres");
});
});
+27 -8
View File
@@ -25,6 +25,12 @@
* The deterministic digest adds only the same computed context rendered next
* to the reaction.
*/
import {
applyDisplayTransform,
getQuantityTransform,
getReadingTransform,
resolveUnitPreferences,
} from "@/lib/measurements/display-transform";
import type { Job } from "pg-boss";
import { prisma } from "@/lib/db";
@@ -140,11 +146,19 @@ export function sanitiseReactionLine(
* intentionally a narrow, labels-and-numbers-only projection: no notes or
* other free text can enter the provider prompt through this path.
*/
async function loadArrivalEvidence(
export async function loadArrivalEvidence(
job: ReactionLineJob,
row: { occurredAt: Date; refId: string | null },
user: { timezone: string; sourcePriorityJson: unknown },
user: {
timezone: string;
sourcePriorityJson: unknown;
unitPreference: string | null;
glucoseUnit: string | null;
},
): Promise<string> {
// The line is written in the reader's units, so the new datum it reacts to
// is handed over in them.
const units = resolveUnitPreferences(user);
if (job.kind === "weight" || job.kind === "blood_pressure") {
const types =
job.kind === "weight"
@@ -157,15 +171,15 @@ async function loadArrivalEvidence(
measuredAt: row.occurredAt,
deletedAt: null,
},
select: { type: true, value: true, unit: true },
select: { type: true, value: true },
orderBy: { type: "asc" },
});
if (readings.length > 0) {
return readings
.map(
(reading) =>
`- Newly arrived reading: ${reading.type} ${reading.value} ${reading.unit}.`,
)
.map((reading) => {
const transform = getReadingTransform(reading.type, units);
return `- Newly arrived reading: ${reading.type} ${applyDisplayTransform(reading.value, transform)} ${transform.displayUnit}.`;
})
.join("\n");
}
}
@@ -241,7 +255,12 @@ async function loadArrivalEvidence(
},
});
if (workout) {
return `- Newly arrived workout: ${workout.sportType}; duration ${workout.durationSec} seconds; distance ${workout.totalDistanceM ?? "not reported"} metres; energy ${workout.totalEnergyKcal ?? "not reported"} kcal; average heart rate ${workout.avgHeartRate ?? "not reported"} bpm.`;
const distance = getQuantityTransform("distance", units.system);
const distanceText =
workout.totalDistanceM == null
? "not reported"
: `${applyDisplayTransform(workout.totalDistanceM, distance)} ${distance.displayUnit}`;
return `- Newly arrived workout: ${workout.sportType}; duration ${workout.durationSec} seconds; distance ${distanceText}; energy ${workout.totalEnergyKcal ?? "not reported"} kcal; average heart rate ${workout.avgHeartRate ?? "not reported"} bpm.`;
}
}