mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
The resolver reads the profile's sex for the body-fat and pulse bands, and nothing asserted it. A resolver that stopped reading the field, or read it and defaulted to one side, passed the whole suite. Pin both recorded sides and the neutral band that OTHER and an unanswered profile share.
227 lines
8.0 KiB
TypeScript
227 lines
8.0 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
||
import {
|
||
getEffectiveRange,
|
||
getAllEffectiveRanges,
|
||
METRIC_BOUNDS,
|
||
type UserProfileForRange,
|
||
type ThresholdOverridesJson,
|
||
} from "../effective-range";
|
||
|
||
const baseProfile: UserProfileForRange = {
|
||
heightCm: 180,
|
||
dateOfBirth: new Date("1985-01-01"),
|
||
gender: "MALE",
|
||
};
|
||
|
||
describe("getEffectiveRange", () => {
|
||
it("returns computed default when no override is set", () => {
|
||
const result = getEffectiveRange("WEIGHT", baseProfile, null);
|
||
expect(result.isOverride).toBe(false);
|
||
expect(result.range).not.toBeNull();
|
||
// BMI 18.5..24.9 at 1.80m ≈ 59.94..80.68
|
||
expect(result.range!.greenMin).toBeCloseTo(59.94, 1);
|
||
expect(result.range!.greenMax).toBeCloseTo(80.68, 1);
|
||
});
|
||
|
||
it("returns null range when a prerequisite profile field is missing", () => {
|
||
const result = getEffectiveRange(
|
||
"WEIGHT",
|
||
{ heightCm: null, dateOfBirth: null, gender: null },
|
||
null,
|
||
);
|
||
expect(result.range).toBeNull();
|
||
expect(result.default).toBeNull();
|
||
});
|
||
|
||
it("uses override when present", () => {
|
||
const overrides: ThresholdOverridesJson = {
|
||
WEIGHT: { min: 70, max: 85 },
|
||
};
|
||
const result = getEffectiveRange("WEIGHT", baseProfile, overrides);
|
||
expect(result.isOverride).toBe(true);
|
||
expect(result.range!.greenMin).toBe(70);
|
||
expect(result.range!.greenMax).toBe(85);
|
||
// Orange wings should be non-degenerate
|
||
expect(result.range!.orangeMin).toBeLessThan(70);
|
||
expect(result.range!.orangeMax).toBeGreaterThan(85);
|
||
});
|
||
|
||
it("returns the pristine default alongside the override", () => {
|
||
const overrides: ThresholdOverridesJson = {
|
||
WEIGHT: { min: 70, max: 85 },
|
||
};
|
||
const result = getEffectiveRange("WEIGHT", baseProfile, overrides);
|
||
expect(result.default).not.toBeNull();
|
||
expect(result.default!.greenMin).toBeCloseTo(59.94, 1);
|
||
});
|
||
|
||
it("has bounds for every supported metric", () => {
|
||
for (const metric of Object.keys(METRIC_BOUNDS) as Array<
|
||
keyof typeof METRIC_BOUNDS
|
||
>) {
|
||
const result = getEffectiveRange(metric, baseProfile, null);
|
||
expect(result.bounds).toEqual(METRIC_BOUNDS[metric]);
|
||
}
|
||
});
|
||
|
||
it("has sensible blood glucose defaults in fasting context", () => {
|
||
const result = getEffectiveRange(
|
||
"BLOOD_GLUCOSE_FASTING",
|
||
baseProfile,
|
||
null,
|
||
);
|
||
expect(result.range!.greenMin).toBe(70);
|
||
expect(result.range!.greenMax).toBe(99);
|
||
expect(result.range!.orangeMax).toBe(125); // pre-diabetes upper
|
||
});
|
||
|
||
it("has different defaults for postprandial vs fasting", () => {
|
||
const fasting = getEffectiveRange(
|
||
"BLOOD_GLUCOSE_FASTING",
|
||
baseProfile,
|
||
null,
|
||
);
|
||
const post = getEffectiveRange(
|
||
"BLOOD_GLUCOSE_POSTPRANDIAL",
|
||
baseProfile,
|
||
null,
|
||
);
|
||
expect(post.range!.greenMax).toBeGreaterThan(fasting.range!.greenMax);
|
||
});
|
||
});
|
||
|
||
// The resolver narrows the stored three-value sex to the two a sex-split
|
||
// reference table has rows for, and OTHER / "no answer" must land on the
|
||
// neutral band rather than on either side. Nothing pinned that before, so a
|
||
// resolver that quietly stopped reading the profile's sex — or read it and
|
||
// picked a default side — went unnoticed.
|
||
describe("sex-split defaults", () => {
|
||
const withGender = (gender: string | null) =>
|
||
getEffectiveRange("BODY_FAT", { ...baseProfile, gender }, null).range!;
|
||
|
||
it("gives each recorded side its own body-fat band", () => {
|
||
expect(withGender("MALE").greenMin).toBe(14);
|
||
expect(withGender("MALE").greenMax).toBe(24);
|
||
expect(withGender("FEMALE").greenMin).toBe(21);
|
||
expect(withGender("FEMALE").greenMax).toBe(31);
|
||
});
|
||
|
||
it("gives OTHER and an unanswered profile the same neutral band", () => {
|
||
const neutral = { greenMin: 17, greenMax: 27 };
|
||
expect(withGender("OTHER")).toMatchObject(neutral);
|
||
expect(withGender(null)).toMatchObject(neutral);
|
||
});
|
||
});
|
||
|
||
describe("getAllEffectiveRanges", () => {
|
||
it("returns an entry for every metric", () => {
|
||
const ranges = getAllEffectiveRanges(baseProfile, null);
|
||
const keys = Object.keys(ranges);
|
||
expect(keys).toContain("WEIGHT");
|
||
expect(keys).toContain("BLOOD_GLUCOSE_FASTING");
|
||
expect(keys).toContain("TOTAL_BODY_WATER");
|
||
expect(keys).toContain("BONE_MASS");
|
||
expect(keys).toContain("OXYGEN_SATURATION");
|
||
expect(keys.length).toBeGreaterThanOrEqual(14);
|
||
});
|
||
});
|
||
|
||
// Audit-2026-05-07 / phase P0 / closes audit C-15: TOTAL_BODY_WATER and
|
||
// BONE_MASS lacked threshold definitions. Severity logic returned `nominal`
|
||
// for any value, so users would see "all healthy" regardless of input.
|
||
describe("body composition thresholds", () => {
|
||
it("TOTAL_BODY_WATER has a sensible non-null green band", () => {
|
||
const result = getEffectiveRange("TOTAL_BODY_WATER", baseProfile, null);
|
||
expect(result.range).not.toBeNull();
|
||
expect(result.range!.greenMin).toBeGreaterThan(0);
|
||
expect(result.range!.greenMax).toBeGreaterThan(result.range!.greenMin);
|
||
expect(result.range!.orangeMin).toBeLessThan(result.range!.greenMin);
|
||
expect(result.range!.orangeMax).toBeGreaterThan(result.range!.greenMax);
|
||
});
|
||
|
||
it("BONE_MASS has a sensible non-null green band", () => {
|
||
const result = getEffectiveRange("BONE_MASS", baseProfile, null);
|
||
expect(result.range).not.toBeNull();
|
||
expect(result.range!.greenMin).toBeGreaterThan(0);
|
||
expect(result.range!.greenMax).toBeGreaterThan(result.range!.greenMin);
|
||
});
|
||
|
||
it("respects user override for body water", () => {
|
||
const overrides: ThresholdOverridesJson = {
|
||
TOTAL_BODY_WATER: { min: 30, max: 45 },
|
||
};
|
||
const result = getEffectiveRange(
|
||
"TOTAL_BODY_WATER",
|
||
baseProfile,
|
||
overrides,
|
||
);
|
||
expect(result.isOverride).toBe(true);
|
||
expect(result.range!.greenMin).toBe(30);
|
||
expect(result.range!.greenMax).toBe(45);
|
||
});
|
||
|
||
it("METRIC_BOUNDS for body composition match VALUE_RANGES in validation", () => {
|
||
expect(METRIC_BOUNDS.TOTAL_BODY_WATER).toEqual({
|
||
min: 5,
|
||
max: 100,
|
||
unit: "kg",
|
||
});
|
||
expect(METRIC_BOUNDS.BONE_MASS).toEqual({ min: 0.5, max: 8, unit: "kg" });
|
||
});
|
||
});
|
||
|
||
// Audit-2026-05-07 / v1.3.3: SpO2 (pulse oximetry) added as a single-value
|
||
// metric with lower-only severity (saturation cannot exceed 100% by definition).
|
||
// Defaults follow BTS Guideline 2017 (target 94–98%) and ATS clinical practice
|
||
// (≥95% at rest is healthy). Below 92% triggers orange band; below 88% in
|
||
// real-world clinical literature is the ER threshold.
|
||
describe("OXYGEN_SATURATION thresholds", () => {
|
||
it("has a non-null green band centred on the BTS healthy range", () => {
|
||
const result = getEffectiveRange("OXYGEN_SATURATION", baseProfile, null);
|
||
expect(result.range).not.toBeNull();
|
||
expect(result.range!.greenMin).toBe(95);
|
||
expect(result.range!.greenMax).toBe(100);
|
||
});
|
||
|
||
it("collapses upper orange wing to greenMax (no upper concern)", () => {
|
||
const result = getEffectiveRange("OXYGEN_SATURATION", baseProfile, null);
|
||
expect(result.range!.orangeMax).toBe(result.range!.greenMax);
|
||
});
|
||
|
||
it("respects user override (e.g. COPD baseline 88-92)", () => {
|
||
const overrides: ThresholdOverridesJson = {
|
||
OXYGEN_SATURATION: { min: 88, max: 92 },
|
||
};
|
||
const result = getEffectiveRange(
|
||
"OXYGEN_SATURATION",
|
||
baseProfile,
|
||
overrides,
|
||
);
|
||
expect(result.isOverride).toBe(true);
|
||
expect(result.range!.greenMin).toBe(88);
|
||
expect(result.range!.greenMax).toBe(92);
|
||
});
|
||
|
||
it("clamps override orangeMax to physiological 100% (no impossible saturations)", () => {
|
||
// Without the bounds clamp this used to emit orangeMax = 100.75.
|
||
const overrides: ThresholdOverridesJson = {
|
||
OXYGEN_SATURATION: { min: 95, max: 100 },
|
||
};
|
||
const result = getEffectiveRange(
|
||
"OXYGEN_SATURATION",
|
||
baseProfile,
|
||
overrides,
|
||
);
|
||
expect(result.range!.orangeMax).toBeLessThanOrEqual(100);
|
||
expect(result.range!.orangeMin).toBeGreaterThanOrEqual(50);
|
||
});
|
||
|
||
it("METRIC_BOUNDS plausibility floor is 50% (incompatible-with-life below)", () => {
|
||
expect(METRIC_BOUNDS.OXYGEN_SATURATION).toEqual({
|
||
min: 50,
|
||
max: 100,
|
||
unit: "%",
|
||
});
|
||
});
|
||
});
|