diff --git a/e2e/settings-score.spec.ts b/e2e/settings-score.spec.ts index 9909ead1f..399bd9f2e 100644 --- a/e2e/settings-score.spec.ts +++ b/e2e/settings-score.spec.ts @@ -2,6 +2,8 @@ import { expect, test } from "./setup/test"; import { STORAGE_STATE_PATH } from "./setup/global-setup"; import { settleBeforeMeasure } from "./utils/settle"; +import { SCORE_VERSION } from "@/lib/analytics/score/types"; +import { healthScoreAlgorithmItemKey } from "@/lib/daily/priority-item-key"; /** * Settings → Health Score: the surface that decides which pillars count. @@ -65,11 +67,14 @@ function scoreReport() { ], delta: null, deltaReason: "no_current_score", - scoreVersion: 2, + scoreVersion: SCORE_VERSION, weightGoal: insufficient("no_goal"), // Raised and never dismissed, which is the state every account is in: // nothing has ever rendered this notice. - algorithmNotice: { itemKey: "health_score_algorithm:2", dismissed: false }, + algorithmNotice: { + itemKey: healthScoreAlgorithmItemKey(SCORE_VERSION), + dismissed: false, + }, }; } @@ -223,7 +228,7 @@ test.describe("Settings → Health Score", () => { await notice.locator('[data-slot="score-change-notice-dismiss"]').click(); await expect(notice).toHaveCount(0); - expect(dismissed).toBe("health_score_algorithm:2"); + expect(dismissed).toBe(healthScoreAlgorithmItemKey(SCORE_VERSION)); }); test("refuses a selection too narrow to produce a score, in plain language", async ({ diff --git a/e2e/utils/mock-populated-insights.ts b/e2e/utils/mock-populated-insights.ts index 266efdc2d..f54147906 100644 --- a/e2e/utils/mock-populated-insights.ts +++ b/e2e/utils/mock-populated-insights.ts @@ -4,6 +4,7 @@ import { SECTION_MOBILITY, SECTION_VITALS, } from "@/components/insights/derived/use-dashboard-derived"; +import { SCORE_VERSION } from "@/lib/analytics/score/types"; /** * A POPULATED Insights overview fixture. @@ -170,7 +171,7 @@ const HEALTH_SCORE = { composition: ["BLOOD_PRESSURE", "ACTIVITY", "SLEEP", "ADIPOSITY"], configured: false, noiseFloor: 3, - scoreVersion: 2, + scoreVersion: SCORE_VERSION, }, coverage: coverage(), confidence: confidence(), @@ -187,7 +188,7 @@ const HEALTH_SCORE = { ], delta: 2, deltaReason: null, - scoreVersion: 2, + scoreVersion: SCORE_VERSION, weightGoal: { status: "ok" as const, value: { diff --git a/e2e/v1341-ui-evidence.spec.ts b/e2e/v1341-ui-evidence.spec.ts index 6eea31bf5..6448d3472 100644 --- a/e2e/v1341-ui-evidence.spec.ts +++ b/e2e/v1341-ui-evidence.spec.ts @@ -2,6 +2,7 @@ import { mkdir } from "node:fs/promises"; import { join } from "node:path"; import type { Page, TestInfo } from "@playwright/test"; import type { DailyDigest } from "@/lib/daily/digest"; +import { SCORE_VERSION } from "@/lib/analytics/score/types"; import { expect, test } from "./setup/test"; import { STORAGE_STATE_PATH } from "./setup/global-setup"; @@ -23,7 +24,7 @@ const NARRATIVE_DIGEST: DailyDigest = { band: "green", delta: -2, deltaReason: null, - scoreVersion: 2, + scoreVersion: SCORE_VERSION, composition: ["BLOOD_PRESSURE", "ACTIVITY", "SLEEP"], }, topSignal: LONG_HEADLINE_BRIEFING.signalsOfDay?.[0] ?? null, diff --git a/src/app/api/admin/backups/[id]/restore/__tests__/round-trip.test.ts b/src/app/api/admin/backups/[id]/restore/__tests__/round-trip.test.ts index 1305d3123..88bc30426 100644 --- a/src/app/api/admin/backups/[id]/restore/__tests__/round-trip.test.ts +++ b/src/app/api/admin/backups/[id]/restore/__tests__/round-trip.test.ts @@ -22,6 +22,8 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { NextRequest } from "next/server"; import { Buffer } from "node:buffer"; +import { SCORE_VERSION } from "@/lib/analytics/score/types"; + process.env.ENCRYPTION_KEY = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; @@ -269,7 +271,7 @@ function sourceClient() { timezone: "Europe/Berlin", composite: 76, band: "yellow", - scoreVersion: 2, + scoreVersion: SCORE_VERSION, composition: ["BLOOD_PRESSURE", "SLEEP", "ADIPOSITY"], pillarScores: { BLOOD_PRESSURE: 86, SLEEP: 100, ADIPOSITY: 42 }, inputFingerprint: "a".repeat(64), @@ -629,7 +631,7 @@ describe("backup round trip — export, wire schema, restore", () => { timezone: "Europe/Berlin", composite: 76, band: "yellow", - scoreVersion: 2, + scoreVersion: SCORE_VERSION, inputFingerprint: "a".repeat(64), }); // The composition and the per-pillar scores are what keep the stored day diff --git a/src/app/api/daily/digest/__tests__/route.test.ts b/src/app/api/daily/digest/__tests__/route.test.ts index 28ca64f05..4ae9ddb1a 100644 --- a/src/app/api/daily/digest/__tests__/route.test.ts +++ b/src/app/api/daily/digest/__tests__/route.test.ts @@ -3,6 +3,7 @@ import { NextRequest } from "next/server"; import { apiError } from "@/lib/api-response"; import type { DailyDigest } from "@/lib/daily/digest"; +import { SCORE_VERSION } from "@/lib/analytics/score/types"; /** * `GET /api/daily/digest` — the P3 read seam. @@ -63,7 +64,7 @@ const DIGEST: DailyDigest = { band: "good", delta: 3, deltaReason: null, - scoreVersion: 2, + scoreVersion: SCORE_VERSION, composition: ["BLOOD_PRESSURE", "ACTIVITY", "SLEEP"], }, topSignal: null, diff --git a/src/components/daily/__tests__/today-hero.test.tsx b/src/components/daily/__tests__/today-hero.test.tsx index 00ca9ccc4..5be773a7e 100644 --- a/src/components/daily/__tests__/today-hero.test.tsx +++ b/src/components/daily/__tests__/today-hero.test.tsx @@ -6,6 +6,7 @@ import { I18nProvider } from "@/lib/i18n/context"; import { TodayHero } from "../today-hero"; import type { DailyDigest } from "@/lib/daily/digest"; import type { PriorityItem } from "@/lib/daily/priority-item"; +import { SCORE_VERSION } from "@/lib/analytics/score/types"; // The hero now wires the coach check-in card's keep / let-go taps through // `useCoachCheckinAction`, so it needs a QueryClient in the tree. @@ -57,7 +58,7 @@ function digest(over: Partial = {}): DailyDigest { band: "green", delta: 3, deltaReason: null, - scoreVersion: 2, + scoreVersion: SCORE_VERSION, composition: ["BLOOD_PRESSURE", "ACTIVITY", "SLEEP"], }, topSignal: { @@ -114,7 +115,7 @@ describe("", () => { band: "green", delta: 20, deltaReason: "algorithm_changed", - scoreVersion: 2, + scoreVersion: SCORE_VERSION, composition: ["BLOOD_PRESSURE", "ACTIVITY", "SLEEP"], }, })} diff --git a/src/lib/analytics/score/__tests__/record.test.ts b/src/lib/analytics/score/__tests__/record.test.ts index 2d92f5a91..b0b05c480 100644 --- a/src/lib/analytics/score/__tests__/record.test.ts +++ b/src/lib/analytics/score/__tests__/record.test.ts @@ -144,8 +144,12 @@ describe("stored bands", () => { }); describe("input fingerprint", () => { + // The fixture only needs two DIFFERENT version markers to prove the + // fingerprint is sensitive to a version move — pinned relative to + // SCORE_VERSION so the pair stays "previous vs current" rather than + // going stale (or colliding) the next time the constant bumps. const base = { - scoreVersion: 1, + scoreVersion: SCORE_VERSION - 1, composition: ["BLOOD_PRESSURE", "SLEEP", "ADIPOSITY"] as ScorePillarId[], pillars: HEALTHY, }; @@ -157,9 +161,9 @@ describe("input fingerprint", () => { }); it("moves when the algorithm version moves", () => { - expect(healthScoreInputFingerprint({ ...base, scoreVersion: 2 })).not.toBe( - healthScoreInputFingerprint(base), - ); + expect( + healthScoreInputFingerprint({ ...base, scoreVersion: SCORE_VERSION }), + ).not.toBe(healthScoreInputFingerprint(base)); }); it("moves when the composition narrows", () => { diff --git a/src/lib/dashboard/__tests__/snapshot.test.ts b/src/lib/dashboard/__tests__/snapshot.test.ts index 8077c2b43..fb6dacf08 100644 --- a/src/lib/dashboard/__tests__/snapshot.test.ts +++ b/src/lib/dashboard/__tests__/snapshot.test.ts @@ -77,7 +77,7 @@ vi.mock("@/lib/analytics/score/reader", () => ({ bandSetter: "BLOOD_PRESSURE" as const, composition: ["BLOOD_PRESSURE", "ACTIVITY", "SLEEP"] as const, noiseFloor: 2, - scoreVersion: 2 as const, + scoreVersion: SCORE_VERSION, }, coverage, confidence: { score: 100, band: "high" as const }, @@ -94,7 +94,7 @@ vi.mock("@/lib/analytics/score/reader", () => ({ pillars: [], delta: legacy?.delta ?? null, deltaReason: null, - scoreVersion: 2 as const, + scoreVersion: SCORE_VERSION, weightGoal: { status: "insufficient" as const, coverage, @@ -154,6 +154,7 @@ import { } from "../snapshot"; import type { ModuleKey } from "@/lib/modules/gate"; import { MODULE_KEYS } from "@/lib/modules/registry"; +import { SCORE_VERSION } from "@/lib/analytics/score/types"; const emptySummary = { count: 0, @@ -370,7 +371,7 @@ describe("buildDashboardSnapshot — per-type thick-phase gate", () => { confidence: { score: 100, band: "high" }, composition: ["BLOOD_PRESSURE", "ACTIVITY", "SLEEP"], deltaReason: null, - scoreVersion: 2, + scoreVersion: SCORE_VERSION, bandSetter: "BLOOD_PRESSURE", // v1.21.2 (A5 / A6) — additive narrative fields, null absent a // disagreeing readiness set / a returned metric (the fake derived @@ -516,7 +517,7 @@ describe("buildDashboardSnapshot — healthScore (warm phase only)", () => { confidence: { score: 100, band: "high" }, composition: ["BLOOD_PRESSURE", "ACTIVITY", "SLEEP"], deltaReason: null, - scoreVersion: 2, + scoreVersion: SCORE_VERSION, bandSetter: "BLOOD_PRESSURE", // v1.21.2 (A5 / A6) — additive narrative fields, null here. tension: null,