From a2f2a30ac25985fb0fcc590f6a0661e8ddf2ec9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 28 Jul 2026 12:03:02 +0200 Subject: [PATCH] fix(quality): close backup and localization guard gaps --- messages/de.json | 3 +- messages/en.json | 3 +- messages/es.json | 3 +- messages/fr.json | 3 +- messages/it.json | 3 +- messages/pl.json | 3 +- .../backup-plan-classification.test.ts | 31 ++++++++++++++---- .../insights-unit-literal-guard.test.ts | 1 - src/app/insights/respiratory-rate/page.tsx | 3 +- .../__tests__/intraday-profile-backup.test.ts | 9 +++++ src/lib/export/intraday-profile-backup.ts | Bin 8144 -> 8145 bytes 11 files changed, 46 insertions(+), 16 deletions(-) diff --git a/messages/de.json b/messages/de.json index 951aa164c..340969ccb 100644 --- a/messages/de.json +++ b/messages/de.json @@ -2865,7 +2865,8 @@ "flights": "Etagen", "falls": "Stürze", "years": "Jahre", - "breathingEvents": "Ereignisse" + "breathingEvents": "Ereignisse", + "respiratoryRate": "Atemzüge/min" }, "sleep": { "title": "Schlaf", diff --git a/messages/en.json b/messages/en.json index ead59de68..df7fa6515 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2865,7 +2865,8 @@ "flights": "flights", "falls": "falls", "years": "years", - "breathingEvents": "events" + "breathingEvents": "events", + "respiratoryRate": "breaths/min" }, "sleep": { "title": "Sleep", diff --git a/messages/es.json b/messages/es.json index 088243721..c4f599020 100644 --- a/messages/es.json +++ b/messages/es.json @@ -2865,7 +2865,8 @@ "flights": "pisos", "falls": "caídas", "years": "años", - "breathingEvents": "eventos" + "breathingEvents": "eventos", + "respiratoryRate": "respiraciones/min" }, "sleep": { "title": "Sueño", diff --git a/messages/fr.json b/messages/fr.json index ac626a5e9..5120603c8 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -2865,7 +2865,8 @@ "flights": "étages", "falls": "chutes", "years": "ans", - "breathingEvents": "événements" + "breathingEvents": "événements", + "respiratoryRate": "respirations/min" }, "sleep": { "title": "Sommeil", diff --git a/messages/it.json b/messages/it.json index 882ca79f3..e4f2e2566 100644 --- a/messages/it.json +++ b/messages/it.json @@ -2865,7 +2865,8 @@ "flights": "piani", "falls": "cadute", "years": "anni", - "breathingEvents": "eventi" + "breathingEvents": "eventi", + "respiratoryRate": "respiri/min" }, "sleep": { "title": "Sonno", diff --git a/messages/pl.json b/messages/pl.json index 1e1f83bcc..c696e7113 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -2865,7 +2865,8 @@ "flights": "piętra", "falls": "upadki", "years": "lata", - "breathingEvents": "zdarzenia" + "breathingEvents": "zdarzenia", + "respiratoryRate": "oddechów/min" }, "sleep": { "title": "Sen", diff --git a/src/__tests__/backup-plan-classification.test.ts b/src/__tests__/backup-plan-classification.test.ts index 676a3feb2..29433f3b6 100644 --- a/src/__tests__/backup-plan-classification.test.ts +++ b/src/__tests__/backup-plan-classification.test.ts @@ -187,12 +187,15 @@ function relationFieldNames(model: string): string[] { const READ_OPS = /(findMany|findUnique|findFirst|findUniqueOrThrow|groupBy)/; const WRITE_OPS = /(create|createMany|upsert|update|updateMany)/; +const READ_RELATION_OPS = /[{[t]/; +const WRITE_RELATION_OPS = /\{\s*create\b/; -/** Does any of `files` touch `model` with an operation matching `ops`? */ +/** Does any of `files` touch `model` through matching delegate and relation operations? */ function touches( files: readonly string[], model: string, - ops: RegExp, + delegateOps: RegExp, + relationOps: RegExp, ): boolean { const delegate = delegateName(model); const relations = relationFieldNames(model); @@ -202,16 +205,28 @@ function touches( for (const match of source.matchAll( new RegExp(`\\.${delegate}\\s*\\.\\s*(\\w+)`, "g"), )) { - if (ops.test(match[1])) return true; + if (delegateOps.test(match[1])) return true; } - // A nested relation key: `schedules: {`, `dayLogs: [`, `entries: true`. return relations.some((relation) => - new RegExp(`\\b${relation}\\s*:\\s*[{[t]`).test(source), + new RegExp(`\\b${relation}\\s*:\\s*(?:${relationOps.source})`).test( + source, + ), ); }); } describe("every backed-up model travels both ways, or is named as debt", () => { + it("does not count a nested relation read as a restore write", () => { + expect( + touches( + ["src/app/api/admin/backups/[id]/restore/route.ts"], + "User", + WRITE_OPS, + WRITE_RELATION_OPS, + ), + ).toBe(false); + }); + it("declares writer and restore files that exist", () => { for (const file of [...BACKUP_WRITER_FILES, ...BACKUP_RESTORE_FILES]) { expect(() => read(file), `${file} is declared but missing`).not.toThrow(); @@ -250,7 +265,8 @@ describe("every backed-up model travels both ways, or is named as debt", () => { it("finds a payload reader for every model claimed two-ended", () => { const missing = TWO_ENDED_MODELS.filter( - (model) => !touches(BACKUP_WRITER_FILES, model, READ_OPS), + (model) => + !touches(BACKUP_WRITER_FILES, model, READ_OPS, READ_RELATION_OPS), ); expect( missing, @@ -261,7 +277,8 @@ describe("every backed-up model travels both ways, or is named as debt", () => { it("finds a restore branch for every model claimed two-ended", () => { const missing = TWO_ENDED_MODELS.filter( - (model) => !touches(BACKUP_RESTORE_FILES, model, WRITE_OPS), + (model) => + !touches(BACKUP_RESTORE_FILES, model, WRITE_OPS, WRITE_RELATION_OPS), ); expect( missing, diff --git a/src/__tests__/insights-unit-literal-guard.test.ts b/src/__tests__/insights-unit-literal-guard.test.ts index d65de1b44..907dbd673 100644 --- a/src/__tests__/insights-unit-literal-guard.test.ts +++ b/src/__tests__/insights-unit-literal-guard.test.ts @@ -34,7 +34,6 @@ const LANGUAGE_NEUTRAL_UNITS = new Set([ "%", "/10", "bpm", - "breaths/min", "dBA", "kcal", "kg/m²", diff --git a/src/app/insights/respiratory-rate/page.tsx b/src/app/insights/respiratory-rate/page.tsx index 726f6a216..90b47e650 100644 --- a/src/app/insights/respiratory-rate/page.tsx +++ b/src/app/insights/respiratory-rate/page.tsx @@ -22,8 +22,7 @@ export default function InsightsAtemfrequenzPage() { i18nPrefix="insights.respiratoryRate" explainerMetric="respiratoryRate" color="var(--info)" - unit="breaths/min" - yAxisUnit="breaths/min" + unitKey="insights.units.respiratoryRate" emptyStateIcon={} emptyStateCtaType={null} coachPrefill="I haven't logged any respiratory rate yet — what does this metric tell me about my health, and how do I improve it?" diff --git a/src/lib/export/__tests__/intraday-profile-backup.test.ts b/src/lib/export/__tests__/intraday-profile-backup.test.ts index 946666766..b41a1915c 100644 --- a/src/lib/export/__tests__/intraday-profile-backup.test.ts +++ b/src/lib/export/__tests__/intraday-profile-backup.test.ts @@ -7,6 +7,8 @@ * which is the failure mode the whole backup guard exists to make loud. */ import { describe, expect, it, vi } from "vitest"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; // The slot-count module reaches for the global Prisma client at load; this // test only wants the constant off it. @@ -73,6 +75,13 @@ function tx(created: Array>) { } describe("intraday profile backup", () => { + it("keeps the backup source reviewable as text", () => { + const source = readFileSync( + resolve(process.cwd(), "src/lib/export/intraday-profile-backup.ts"), + ); + expect(source.includes(0)).toBe(false); + }); + it("carries the same slot count the reader indexes by", () => { // Re-declared rather than imported, so it is pinned here instead: a change // to one without the other would put rows in the table that every read diff --git a/src/lib/export/intraday-profile-backup.ts b/src/lib/export/intraday-profile-backup.ts index 233ea26309b0c092aee347d35e95e56bd8b7fb51..3ce7caf8f868832e640ca4db1a406fec71630713 100644 GIT binary patch delta 15 Xcmca$f6;!!ElH*rgUz=kzp?@VI}ir) delta 14 Wcmca;f5CpkElEa(&37cfvH}1zrUqsJ