mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
fix(release): harden v1.31 arrival contracts
This commit is contained in:
1 parent
44add3bc1e
commit
4591899657
98 files changed
+5560
-1600
No files matched your search
@@ -0,0 +1,165 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { CoachScopeSource } from "@/lib/ai/coach/types";
|
||||
import {
|
||||
scopeSourceFromMetricKey,
|
||||
scopeSourceMetricLabelKey,
|
||||
} from "../coach-metric-scope";
|
||||
|
||||
const PROJECT_ROOT = resolve(import.meta.dirname, "../../../..");
|
||||
const MESSAGE_BUNDLES = ["en", "de"].map((locale) =>
|
||||
JSON.parse(
|
||||
readFileSync(join(PROJECT_ROOT, `messages/${locale}.json`), "utf8"),
|
||||
),
|
||||
);
|
||||
|
||||
function translationAt(bundle: unknown, key: string): unknown {
|
||||
return key.split(".").reduce<unknown>((current, segment) => {
|
||||
if (
|
||||
current == null ||
|
||||
typeof current !== "object" ||
|
||||
!(segment in current)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return (current as Record<string, unknown>)[segment];
|
||||
}, bundle);
|
||||
}
|
||||
|
||||
function sourceFilesBelow(directory: string): string[] {
|
||||
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = join(directory, entry.name);
|
||||
if (entry.isDirectory()) return sourceFilesBelow(path);
|
||||
return entry.name.endsWith(".tsx") ? [path] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function genericMetricCardInventory(): Set<string> {
|
||||
const insightRouteSources = sourceFilesBelow(
|
||||
join(PROJECT_ROOT, "src/app/insights"),
|
||||
).map((path) => readFileSync(path, "utf8"));
|
||||
const recoverySource = readFileSync(
|
||||
join(PROJECT_ROOT, "src/components/insights/recovery/recovery-section.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const statusMetrics = insightRouteSources.flatMap((source) =>
|
||||
Array.from(
|
||||
source.matchAll(/\bstatusMetric\s*=\s*"([A-Z0-9_]+)"/g),
|
||||
(m) => m[1],
|
||||
),
|
||||
);
|
||||
const directMetrics = insightRouteSources.flatMap((source) =>
|
||||
Array.from(
|
||||
source.matchAll(
|
||||
/<MetricStatusCard\b[\s\S]*?\bmetric\s*=\s*"([A-Z0-9_]+)"[\s\S]*?\/>/g,
|
||||
),
|
||||
(m) => m[1],
|
||||
),
|
||||
);
|
||||
const recoveryMetrics = Array.from(
|
||||
recoverySource.matchAll(/\bstatusMetric\s*:\s*"([A-Z0-9_]+)"/g),
|
||||
(m) => m[1],
|
||||
);
|
||||
|
||||
return new Set([...statusMetrics, ...directMetrics, ...recoveryMetrics]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every generic assessment mount is classified here. A non-null value means
|
||||
* the Coach snapshot has a source backed by the exact data on that card;
|
||||
* null records that no matching source exists and prevents a tempting but
|
||||
* incorrect near-match (for example wrist temperature → skin temperature).
|
||||
*/
|
||||
const GENERIC_METRIC_SCOPE_EXPECTATION: Readonly<
|
||||
Record<string, CoachScopeSource | null>
|
||||
> = {
|
||||
ACTIVE_ENERGY: "active_energy",
|
||||
AUDIO_EXPOSURE_EVENT: "audio_event",
|
||||
AUDIO_EXPOSURE_ENV: "audio_env",
|
||||
AUDIO_EXPOSURE_HEADPHONE: "audio_headphone",
|
||||
BLOOD_GLUCOSE: "glucose",
|
||||
BODY_TEMPERATURE: "body_temp",
|
||||
BONE_MASS: "bone_mass",
|
||||
FAT_FREE_MASS: "fat_free_mass",
|
||||
FAT_MASS: "fat_mass",
|
||||
FLIGHTS_CLIMBED: "flights",
|
||||
HEART_RATE_VARIABILITY: "hrv",
|
||||
LEAN_BODY_MASS: "lean_body_mass",
|
||||
MUSCLE_MASS: "muscle_mass",
|
||||
OXYGEN_SATURATION: "spo2",
|
||||
PULSE_WAVE_VELOCITY: "pulse_wave_velocity",
|
||||
RESPIRATORY_RATE: "respiratory_rate",
|
||||
RESTING_HEART_RATE: "resting_hr",
|
||||
SKIN_TEMPERATURE: "skin_temp",
|
||||
SLEEP_DURATION: "sleep",
|
||||
STEPS: "steps",
|
||||
TIME_IN_DAYLIGHT: "daylight",
|
||||
TOTAL_BODY_WATER: "total_body_water",
|
||||
VASCULAR_AGE: "vascular_age",
|
||||
VISCERAL_FAT: "visceral_fat",
|
||||
VO2_MAX: "vo2_max",
|
||||
WALKING_ASYMMETRY: "walking_asymmetry",
|
||||
WALKING_DOUBLE_SUPPORT: "walking_double_support",
|
||||
WALKING_HEART_RATE_AVERAGE: "walking_hr",
|
||||
WALKING_RUNNING_DISTANCE: "distance",
|
||||
WALKING_SPEED: "walking_speed",
|
||||
WALKING_STEADINESS: "walking_steadiness",
|
||||
WALKING_STEP_LENGTH: "walking_step_length",
|
||||
|
||||
ANS_CHARGE: null,
|
||||
AVERAGE_HEART_RATE: null,
|
||||
BREATHING_DISTURBANCES: null,
|
||||
CARDIO_LOAD: null,
|
||||
CARDIO_RECOVERY: null,
|
||||
DAY_STRAIN: null,
|
||||
ENERGY_EXPENDITURE_KJ: null,
|
||||
FALL_COUNT: null,
|
||||
GRIP_STRENGTH: null,
|
||||
MAX_HEART_RATE: null,
|
||||
PAIN_NRS: null,
|
||||
SIX_MINUTE_WALK_DISTANCE: null,
|
||||
STAIR_ASCENT_SPEED: null,
|
||||
STAIR_DESCENT_SPEED: null,
|
||||
WAIST_CIRCUMFERENCE: null,
|
||||
WAIST_TO_HEIGHT: null,
|
||||
WORKOUT_STRAIN: null,
|
||||
WRIST_TEMPERATURE: null,
|
||||
};
|
||||
|
||||
describe("generic MetricStatusCard Coach scope inventory", () => {
|
||||
it("classifies every mounted status metric and scopes every exact Coach source", () => {
|
||||
const mountedMetrics = genericMetricCardInventory();
|
||||
expect([...mountedMetrics].sort()).toEqual(
|
||||
Object.keys(GENERIC_METRIC_SCOPE_EXPECTATION).sort(),
|
||||
);
|
||||
|
||||
for (const [metric, expectedSource] of Object.entries(
|
||||
GENERIC_METRIC_SCOPE_EXPECTATION,
|
||||
)) {
|
||||
expect(scopeSourceFromMetricKey(metric), metric).toBe(expectedSource);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses an existing localized measurement label for every scoped mount", () => {
|
||||
const supportedSources = new Set(
|
||||
Object.values(GENERIC_METRIC_SCOPE_EXPECTATION).filter(
|
||||
(source): source is CoachScopeSource => source !== null,
|
||||
),
|
||||
);
|
||||
|
||||
for (const source of supportedSources) {
|
||||
const labelKey = scopeSourceMetricLabelKey(source);
|
||||
expect(labelKey, source).not.toBeNull();
|
||||
for (const messages of MESSAGE_BUNDLES) {
|
||||
expect(
|
||||
translationAt(messages, labelKey!),
|
||||
`${source}: ${labelKey}`,
|
||||
).toEqual(expect.any(String));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CoachScopeSource, CoachScopeWindow } from "@/lib/ai/coach/types";
|
||||
import { COACH_SOURCE_DOMAIN_LABEL } from "@/lib/ai/coach/tools/source-keys";
|
||||
import type { MetricStatusMetricId } from "@/lib/insights/metric-status-registry";
|
||||
|
||||
/**
|
||||
* v1.21.0 (C4 H1/H4) — metric → Coach scope + seed-question map.
|
||||
@@ -126,32 +127,114 @@ export function metricScopeFromExplainer(
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a recommendation / correlation `metricSource.type` — the
|
||||
* model's snapshot-key vocabulary ("bloodPressure", "weight", "pulse",
|
||||
* "mood", "medications.compliance30", "sleep", "steps", …) — to a single
|
||||
* `CoachScopeSource`, or null when the key has no snapshot block to
|
||||
* narrow to. Lets a card's "Ask the Coach" affordance pre-scope the
|
||||
* conversation to the metric the card is about.
|
||||
* Every generic metric-assessment id is classified here, including the ones
|
||||
* that intentionally cannot scope the Coach. A null entry means the snapshot
|
||||
* has no source backed by the exact data assessed on that card; near-matches
|
||||
* (for example wrist temperature → skin temperature) must stay unscoped.
|
||||
*/
|
||||
const STATUS_METRIC_SCOPE_SOURCE = {
|
||||
RESTING_HEART_RATE: "resting_hr",
|
||||
HEART_RATE_VARIABILITY: "hrv",
|
||||
OXYGEN_SATURATION: "spo2",
|
||||
RESPIRATORY_RATE: "respiratory_rate",
|
||||
BODY_TEMPERATURE: "body_temp",
|
||||
SKIN_TEMPERATURE: "skin_temp",
|
||||
BLOOD_GLUCOSE: "glucose",
|
||||
WALKING_HEART_RATE_AVERAGE: "walking_hr",
|
||||
PULSE_WAVE_VELOCITY: "pulse_wave_velocity",
|
||||
VASCULAR_AGE: "vascular_age",
|
||||
STEPS: "steps",
|
||||
ACTIVE_ENERGY: "active_energy",
|
||||
FLIGHTS_CLIMBED: "flights",
|
||||
WALKING_RUNNING_DISTANCE: "distance",
|
||||
TIME_IN_DAYLIGHT: "daylight",
|
||||
VO2_MAX: "vo2_max",
|
||||
TOTAL_BODY_WATER: "total_body_water",
|
||||
BONE_MASS: "bone_mass",
|
||||
FAT_FREE_MASS: "fat_free_mass",
|
||||
FAT_MASS: "fat_mass",
|
||||
MUSCLE_MASS: "muscle_mass",
|
||||
LEAN_BODY_MASS: "lean_body_mass",
|
||||
VISCERAL_FAT: "visceral_fat",
|
||||
WALKING_STEADINESS: "walking_steadiness",
|
||||
WALKING_ASYMMETRY: "walking_asymmetry",
|
||||
WALKING_DOUBLE_SUPPORT: "walking_double_support",
|
||||
WALKING_STEP_LENGTH: "walking_step_length",
|
||||
WALKING_SPEED: "walking_speed",
|
||||
AUDIO_EXPOSURE_ENV: "audio_env",
|
||||
AUDIO_EXPOSURE_HEADPHONE: "audio_headphone",
|
||||
AUDIO_EXPOSURE_EVENT: "audio_event",
|
||||
SLEEP_DURATION: "sleep",
|
||||
CARDIO_RECOVERY: null,
|
||||
WRIST_TEMPERATURE: null,
|
||||
FALL_COUNT: null,
|
||||
SIX_MINUTE_WALK_DISTANCE: null,
|
||||
STAIR_ASCENT_SPEED: null,
|
||||
STAIR_DESCENT_SPEED: null,
|
||||
BREATHING_DISTURBANCES: null,
|
||||
SLEEP_SCORE: null,
|
||||
ANS_CHARGE: null,
|
||||
DAY_STRAIN: null,
|
||||
WORKOUT_STRAIN: null,
|
||||
CARDIO_LOAD: null,
|
||||
AVERAGE_HEART_RATE: null,
|
||||
MAX_HEART_RATE: null,
|
||||
ENERGY_EXPENDITURE_KJ: null,
|
||||
GRIP_STRENGTH: null,
|
||||
PAIN_NRS: null,
|
||||
WAIST_CIRCUMFERENCE: null,
|
||||
WAIST_TO_HEIGHT: null,
|
||||
} as const satisfies Readonly<
|
||||
Record<MetricStatusMetricId, CoachScopeSource | null>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Recommendation cards use the insight snapshot's section-key vocabulary.
|
||||
* Keep those aliases declarative as well; status-card ids are resolved by the
|
||||
* exhaustive table above.
|
||||
*/
|
||||
const SNAPSHOT_METRIC_SCOPE_SOURCE: Readonly<Record<string, CoachScopeSource>> =
|
||||
{
|
||||
bloodpressure: "bp",
|
||||
blood_pressure: "bp",
|
||||
weight: "weight",
|
||||
pulse: "pulse",
|
||||
resting_hr: "resting_hr",
|
||||
restinghr: "resting_hr",
|
||||
hrv: "hrv",
|
||||
mood: "mood",
|
||||
sleep: "sleep",
|
||||
steps: "steps",
|
||||
activity: "steps",
|
||||
bloodglucose: "glucose",
|
||||
blood_glucose: "glucose",
|
||||
bmi: "bmi",
|
||||
medication: "compliance",
|
||||
"medications.compliance": "compliance",
|
||||
"medications.compliance7": "compliance",
|
||||
"medications.compliance30": "compliance",
|
||||
"medications.compliance90": "compliance",
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a generic assessment id or recommendation `metricSource.type` to
|
||||
* the exact Coach snapshot source it is based on. Unsupported assessment ids
|
||||
* resolve to null explicitly through `STATUS_METRIC_SCOPE_SOURCE`.
|
||||
*/
|
||||
export function scopeSourceFromMetricKey(
|
||||
metricKey: string | undefined,
|
||||
): CoachScopeSource | null {
|
||||
if (!metricKey) return null;
|
||||
const lower = metricKey.toLowerCase();
|
||||
if (lower.startsWith("medications.compliance") || lower === "medication") {
|
||||
return "compliance";
|
||||
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(STATUS_METRIC_SCOPE_SOURCE, metricKey)
|
||||
) {
|
||||
return STATUS_METRIC_SCOPE_SOURCE[
|
||||
metricKey as MetricStatusMetricId
|
||||
] as CoachScopeSource | null;
|
||||
}
|
||||
if (lower === "bloodpressure" || lower === "blood_pressure") return "bp";
|
||||
if (lower === "weight") return "weight";
|
||||
if (lower === "pulse") return "pulse";
|
||||
if (lower === "resting_hr" || lower === "restinghr") return "resting_hr";
|
||||
if (lower === "hrv") return "hrv";
|
||||
if (lower === "mood") return "mood";
|
||||
if (lower === "sleep" || lower.startsWith("sleep")) return "sleep";
|
||||
if (lower === "steps" || lower === "activity") return "steps";
|
||||
if (lower === "bloodglucose" || lower === "blood_glucose") return "glucose";
|
||||
if (lower === "bmi") return "bmi";
|
||||
return null;
|
||||
|
||||
return SNAPSHOT_METRIC_SCOPE_SOURCE[metricKey.toLowerCase()] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,11 +269,14 @@ const SCOPE_SOURCE_METRIC_LABEL_KEY: Partial<Record<CoachScopeSource, string>> =
|
||||
pulse: "measurements.typePulse",
|
||||
hrv: "measurements.typeHeartRateVariability",
|
||||
resting_hr: "measurements.typeRestingHeartRate",
|
||||
walking_hr: "measurements.typeWalkingHeartRateAverage",
|
||||
respiratory_rate: "measurements.typeRespiratoryRate",
|
||||
spo2: "measurements.typeOxygenSaturation",
|
||||
bmi: "measurements.typeBodyMassIndex",
|
||||
body_temp: "measurements.typeBodyTemperature",
|
||||
vo2_max: "measurements.typeVo2Max",
|
||||
pulse_wave_velocity: "measurements.typePulseWaveVelocity",
|
||||
vascular_age: "measurements.typeVascularAge",
|
||||
sleep: "measurements.typeSleep",
|
||||
body_fat: "measurements.typeBodyFat",
|
||||
fat_mass: "measurements.typeFatMass",
|
||||
@@ -198,8 +284,23 @@ const SCOPE_SOURCE_METRIC_LABEL_KEY: Partial<Record<CoachScopeSource, string>> =
|
||||
muscle_mass: "measurements.typeMuscleMass",
|
||||
lean_body_mass: "measurements.typeLeanBodyMass",
|
||||
bone_mass: "measurements.typeBoneMass",
|
||||
total_body_water: "measurements.typeTotalBodyWater",
|
||||
visceral_fat: "measurements.typeVisceralFat",
|
||||
active_energy: "measurements.typeActiveEnergyBurned",
|
||||
flights: "measurements.typeFlightsClimbed",
|
||||
steps: "measurements.typeSteps",
|
||||
distance: "measurements.typeWalkingRunningDistance",
|
||||
glucose: "measurements.typeBloodGlucose",
|
||||
walking_steadiness: "measurements.typeWalkingSteadiness",
|
||||
walking_asymmetry: "measurements.typeWalkingAsymmetry",
|
||||
walking_double_support: "measurements.typeWalkingDoubleSupport",
|
||||
walking_step_length: "measurements.typeWalkingStepLength",
|
||||
walking_speed: "measurements.typeWalkingSpeed",
|
||||
audio_env: "measurements.typeAudioExposureEnv",
|
||||
audio_headphone: "measurements.typeAudioExposureHeadphone",
|
||||
audio_event: "measurements.typeAudioExposureEvent",
|
||||
daylight: "measurements.typeTimeInDaylight",
|
||||
skin_temp: "measurements.typeSkinTemperature",
|
||||
};
|
||||
|
||||
export function scopeSourceMetricLabelKey(
|
||||
|
||||
@@ -276,6 +276,9 @@ export function CoachConversation({
|
||||
const [currentConversationId, setCurrentConversationId] = useState<
|
||||
string | null
|
||||
>(initialConversationId ?? null);
|
||||
const [activeWorkoutId, setActiveWorkoutId] = useState<string | null>(
|
||||
initialWorkoutId ?? null,
|
||||
);
|
||||
const [historyTrayOpen, setHistoryTrayOpen] = useState(false);
|
||||
const [sourcesTrayOpen, setSourcesTrayOpen] = useState(false);
|
||||
const [inputValue, setInputValue] = useResettableValue(prefill ?? "");
|
||||
@@ -641,7 +644,7 @@ export function CoachConversation({
|
||||
// Same first-turn gate as `scope` — see `initialWorkoutId`.
|
||||
const workoutId =
|
||||
currentConversationId === null
|
||||
? (initialWorkoutId ?? undefined)
|
||||
? (activeWorkoutId ?? undefined)
|
||||
: undefined;
|
||||
const wasFreshFenced = currentConversationId === null && fenced;
|
||||
const resolvedId = await send.send({
|
||||
@@ -725,7 +728,7 @@ export function CoachConversation({
|
||||
workoutId:
|
||||
fenced || currentConversationId !== null
|
||||
? undefined
|
||||
: (initialWorkoutId ?? undefined),
|
||||
: (activeWorkoutId ?? undefined),
|
||||
fenced,
|
||||
pendingAttachmentIds:
|
||||
currentConversationId === null ? pendingAttachmentIds : undefined,
|
||||
@@ -739,6 +742,7 @@ export function CoachConversation({
|
||||
// v1.29.x (S7) — a new chat is always a health thread; drop any staged
|
||||
// attachments so a fenced scope can never leak onto a fresh health thread.
|
||||
setPendingAttachmentIds([]);
|
||||
setActiveWorkoutId(null);
|
||||
dispatchGuided({ type: "RESET" });
|
||||
send.reset();
|
||||
}
|
||||
|
||||
@@ -115,23 +115,37 @@ describe("derived-score sheets — every sheet has an outbound coach edge", () =
|
||||
);
|
||||
});
|
||||
|
||||
it("anchors the sleep sheet on sleep and the strain sheet on workouts", () => {
|
||||
expect(
|
||||
(renderSheet("SLEEP_SCORE").coachScope as { metric: string }).metric,
|
||||
).toBe("sleep");
|
||||
expect(
|
||||
(renderSheet("STRAIN_SCORE").coachScope as { metric: string }).metric,
|
||||
).toBe("workouts");
|
||||
});
|
||||
|
||||
it("widens recovery-style sheets to the inputs that drive them", () => {
|
||||
// Recovery is a synthesis of HRV + resting HR + sleep; narrowing to one
|
||||
// of the three would hide the reason the score moved.
|
||||
const scope = renderSheet("RECOVERY_SCORE").coachScope as {
|
||||
metric: string;
|
||||
also?: string[];
|
||||
};
|
||||
expect(scope.metric).toBe("hrv");
|
||||
expect(scope.also).toEqual(expect.arrayContaining(["resting_hr", "sleep"]));
|
||||
});
|
||||
it.each([
|
||||
["SLEEP_SCORE", { metric: "sleep" }],
|
||||
[
|
||||
"READINESS",
|
||||
{
|
||||
metric: "hrv",
|
||||
also: ["resting_hr", "sleep", "respiratory_rate", "mood"],
|
||||
window: "last7days",
|
||||
},
|
||||
],
|
||||
[
|
||||
"RECOVERY_SCORE",
|
||||
{
|
||||
metric: "hrv",
|
||||
also: ["resting_hr", "sleep", "respiratory_rate", "mood"],
|
||||
window: "last7days",
|
||||
},
|
||||
],
|
||||
["STRESS_SCORE", { metric: "hrv", window: "last7days" }],
|
||||
[
|
||||
"STRAIN_SCORE",
|
||||
{
|
||||
metric: "workouts",
|
||||
also: ["active_energy", "resting_hr"],
|
||||
window: "last7days",
|
||||
},
|
||||
],
|
||||
] satisfies ReadonlyArray<[AnatomyMetricId, Record<string, unknown>]>)(
|
||||
"%s carries exactly its engine contributor domains",
|
||||
(metric, expectedScope) => {
|
||||
expect(renderSheet(metric).coachScope).toEqual(expectedScope);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -65,29 +65,34 @@ export interface CompositeScoreAnatomyProps {
|
||||
/**
|
||||
* v1.31.0 — the Coach scope each derived-score sheet hands off to.
|
||||
*
|
||||
* The score sheets were the one assessment surface with NO outbound edge: they
|
||||
* render the same `<InsightStatusCard>` as every metric page but passed
|
||||
* neither an opener nor a scope, so the card painted no action at all and the
|
||||
* sheet read one-way. A composite has no snapshot block of its own — it is a
|
||||
* synthesis — so each maps to the INPUTS that drive it, the same way the
|
||||
* recovery metric page already anchors on HRV + resting HR + sleep.
|
||||
* Every list below mirrors the score engine's real contributor domains:
|
||||
* Sleep Score reads only sleep-stage rows; Readiness blends RHR, HRV, sleep,
|
||||
* respiratory rate, and mood; Stress is derived only from HRV; and Strain uses
|
||||
* workout HR series plus active energy and the separately loaded resting HR.
|
||||
*
|
||||
* Recovery is source-dependent. A COMPUTED row is the Readiness blend
|
||||
* verbatim, while a canonical native WHOOP / Oura / Polar row is the vendor's
|
||||
* already-computed score and exposes no reconstructable input domain through
|
||||
* the launch DTO. Its static scope is therefore the conservative union of the
|
||||
* actual reconstructable contributors: the five Readiness domains, with no
|
||||
* guessed native-only sources.
|
||||
*/
|
||||
const METRIC_COACH_SCOPE: Record<AnatomyMetricId, CoachLaunchScope> = {
|
||||
SLEEP_SCORE: { metric: "sleep", also: ["hrv", "resting_hr"] },
|
||||
SLEEP_SCORE: { metric: "sleep" },
|
||||
READINESS: {
|
||||
metric: "hrv",
|
||||
also: ["resting_hr", "sleep"],
|
||||
also: ["resting_hr", "sleep", "respiratory_rate", "mood"],
|
||||
window: "last7days",
|
||||
},
|
||||
RECOVERY_SCORE: {
|
||||
metric: "hrv",
|
||||
also: ["resting_hr", "sleep"],
|
||||
also: ["resting_hr", "sleep", "respiratory_rate", "mood"],
|
||||
window: "last7days",
|
||||
},
|
||||
STRESS_SCORE: { metric: "hrv", also: ["resting_hr"], window: "last7days" },
|
||||
STRESS_SCORE: { metric: "hrv", window: "last7days" },
|
||||
STRAIN_SCORE: {
|
||||
metric: "workouts",
|
||||
also: ["active_energy", "pulse"],
|
||||
also: ["active_energy", "resting_hr"],
|
||||
window: "last7days",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -166,13 +166,13 @@ describe("dashboard layout save — every layout source is covered (source pins)
|
||||
expect(pageSrc).not.toMatch(/analytics[A-Za-z]*\??\.(data\.)?layout/);
|
||||
});
|
||||
|
||||
it("refetchOnMount stays false on the snapshot hook (the reason refetchType-all is required)", () => {
|
||||
it("keeps remounts disabled while focus refresh remains unconditional", () => {
|
||||
const hookSrc = readFileSync(
|
||||
join(process.cwd(), "src/lib/queries/use-dashboard-snapshot.ts"),
|
||||
"utf8",
|
||||
);
|
||||
expect(hookSrc).toContain("refetchOnMount: false");
|
||||
// v1.28.28 — focus refetch stays on (cross-tab freshness); do not revert.
|
||||
expect(hookSrc).toContain("refetchOnWindowFocus: true");
|
||||
// Arrival freshness is out-of-band, so staleTime must not suppress focus.
|
||||
expect(hookSrc).toContain('refetchOnWindowFocus: "always"');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user