fix(import): honour the energy unit an archived workout carries

`totalEnergyBurned` rides the same account-display-unit contract as the
distance beside it: a Health app set to kilojoules writes `kJ`, and the
column is kilocalories. Convert it through the shared table.

Refs #944
This commit is contained in:
Marc-André Bombeck committed 2026-09-09 22:47:05 +02:00
1 parent bdfff04975
commit e031e2211c
2 files changed
+44 -2

No files matched your search

@@ -1297,6 +1297,38 @@ describe("streamParseExportXml — the record's own unit (issue #944)", () => {
expect(prisma._measurements[0].value).toBeCloseTo(2484, 6);
});
it("stores a kilojoule workout burn in kilocalories", async () => {
const tmp = mkdtempSync(join(tmpdir(), "healthlog-parser-energy-test-"));
const xmlPath = join(tmp, "export.xml");
writeFileSync(
xmlPath,
`<?xml version="1.0" encoding="UTF-8"?>
<HealthData locale="en_AU">
<Workout workoutActivityType="HKWorkoutActivityTypeRunning"
duration="42.0"
durationUnit="min"
totalDistance="6.5"
totalDistanceUnit="km"
totalEnergyBurned="1724"
totalEnergyBurnedUnit="kJ"
startDate="2026-05-14 18:00:00 +0200"
endDate="2026-05-14 18:42:00 +0200"
sourceName="Apple Watch"/>
</HealthData>`,
);
const prisma = makeFakePrisma();
await streamParseExportXml({
xmlPath,
userId: "user-energy",
userTimezone: "Europe/Berlin",
prisma: prisma as unknown as PrismaClient,
});
expect(prisma._workouts).toHaveLength(1);
expect(prisma._workouts[0].totalDistanceM).toBeCloseTo(6500, 6);
expect(prisma._workouts[0].totalEnergyKcal).toBeCloseTo(412.0458891, 6);
});
it("stores a mile-scale walking distance in metres", async () => {
const { prisma } = await importCumulativeFixture(
cumulativeExportXml(
@@ -42,7 +42,10 @@ import {
} from "@/lib/measurements/drain-per-sample-cumulative";
import { reconcileExternalMeasurement } from "@/lib/measurements/reconcile-external-measurement";
import { resolveHkWorkoutSportType } from "@/lib/measurements/hk-workout-activity-type-map";
import { hkDistanceToMetres } from "@/lib/measurements/hk-units";
import {
convertHkValue,
hkDistanceToMetres,
} from "@/lib/measurements/hk-units";
import { emitInsertedMeasurementArrivals } from "@/lib/arrivals/measurement-emit";
import { maybeEnqueueMorningRefresh } from "@/lib/daily/morning-refresh-trigger";
import { emitDataArrival } from "@/lib/arrivals/emit-shared";
@@ -948,6 +951,7 @@ export async function streamParseExportXml(
const totalDistance = Number.parseFloat(attrs.totalDistance ?? "");
const totalEnergy = Number.parseFloat(attrs.totalEnergyBurned ?? "");
const distanceUnit = attrs.totalDistanceUnit ?? "";
const energyUnit = attrs.totalEnergyBurnedUnit ?? "";
// Apple ships the ACCOUNT's own length unit on HKWorkout.totalDistance
// (km for a metric account, mi for an imperial one); metres is the
// canonical DB unit. issue #944 — the same conversion the `<Record>`
@@ -969,7 +973,13 @@ export async function streamParseExportXml(
startedAt: startDate,
endedAt: endDate,
durationSec,
totalEnergyKcal: Number.isFinite(totalEnergy) ? totalEnergy : null,
totalEnergyKcal: Number.isFinite(totalEnergy)
? // issue #944 — the energy attribute carries the account's own
// unit too: a Health app set to kilojoules writes `kJ` here, and
// the column is kilocalories. Same shared conversion; an unknown
// unit leaves the number alone.
(convertHkValue(totalEnergy, energyUnit, "kcal") ?? totalEnergy)
: null,
totalDistanceM: distanceM,
externalId,
externalSourceVersion: attrs.sourceVersion ?? null,