mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
Merge environment data into release/v1.42.0
# Conflicts: # src/__tests__/i18n-reverse-coverage.test.ts
This commit is contained in:
commit
7bcb6f71d9
73 files changed
+6061
-385
No files matched your search
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* v1.42 (#615) — pair guard binding the two environment models in
|
||||
* `prisma/schema.prisma` to the backup that carries them
|
||||
* (`src/lib/export/environment-backup.ts`), on the pattern of
|
||||
* `measurement-backup-completeness.test.ts`.
|
||||
*
|
||||
* Twenty-two air-quality columns and three sealed locations arrived in one
|
||||
* release, and nothing failed when a column was missing from the backup: the
|
||||
* export kept building, the restore kept parsing (the section schema is
|
||||
* `.passthrough()`), and the column came back NULL on every restored day. So
|
||||
* both ends are compared as literals:
|
||||
*
|
||||
* - every scalar column of `EnvironmentContext` and
|
||||
* `EnvironmentTravelLocation` is in the named backup select (or excluded
|
||||
* here with a reason);
|
||||
* - every selected `EnvironmentContext` column is written by the export
|
||||
* (`row.<column>`, or the location helper for the location columns);
|
||||
* - every selected column is written back by the restore (`<column>:` in
|
||||
* `restoreEnvironmentData`, or listed in `AIR_QUALITY_COLUMNS`, which the
|
||||
* restore maps over).
|
||||
*
|
||||
* Mutation check: delete `pm25Mean: true` from the context select and the
|
||||
* first case names it; delete `"pm25Mean",` from `AIR_QUALITY_COLUMNS` and the
|
||||
* restore case names it.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const schema = readFileSync(join(ROOT, "prisma", "schema.prisma"), "utf8");
|
||||
const source = readFileSync(
|
||||
join(ROOT, "src", "lib", "export", "environment-backup.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
/** Columns the backup is right to leave out, with the reason. */
|
||||
const EXCLUDED: Record<string, string> = {
|
||||
id: "nothing in the file or the database addresses one of these rows; a day is identified by its date",
|
||||
userId:
|
||||
"user-scoped backup; the owner is re-derived from the restoring account",
|
||||
};
|
||||
|
||||
/** Columns written through the location helper rather than `row.<column>`. */
|
||||
const LOCATION_COLUMNS = new Set([
|
||||
"lat",
|
||||
"lon",
|
||||
"locationLabel",
|
||||
"label",
|
||||
"locationEncrypted",
|
||||
]);
|
||||
|
||||
function block(text: string, start: string, end = "\n}"): string {
|
||||
const from = text.indexOf(start);
|
||||
expect(from, `${start} not found`).toBeGreaterThan(-1);
|
||||
const to = text.indexOf(end, from);
|
||||
expect(to).toBeGreaterThan(from);
|
||||
return text.slice(from, to);
|
||||
}
|
||||
|
||||
function scalarColumns(model: string): string[] {
|
||||
const models = new Set(
|
||||
[...schema.matchAll(/^model\s+([A-Za-z_]\w*)\s*\{/gm)].map((m) => m[1]),
|
||||
);
|
||||
const out: string[] = [];
|
||||
for (const raw of block(schema, `model ${model} {`).split("\n").slice(1)) {
|
||||
const line = raw.trim();
|
||||
if (!line || line.startsWith("//") || line.startsWith("@@")) continue;
|
||||
const m = /^([a-zA-Z_]\w*)\s+([A-Za-z_]\w*)(\[\])?/.exec(line);
|
||||
if (!m || m[3] || models.has(m[2]) || line.includes("@relation")) continue;
|
||||
out.push(m[1]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function selectKeys(name: string): string[] {
|
||||
return [
|
||||
...block(source, `const ${name} = {`).matchAll(/^ {2}(\w+): true,$/gm),
|
||||
].map((m) => m[1]);
|
||||
}
|
||||
|
||||
const restoreBody = block(
|
||||
source,
|
||||
"export async function restoreEnvironmentData(",
|
||||
"\n}\n",
|
||||
);
|
||||
const airQualityList = block(source, "const AIR_QUALITY_COLUMNS = [", "]");
|
||||
|
||||
describe.each([
|
||||
["EnvironmentContext", "ENVIRONMENT_CONTEXT_BACKUP_SELECT"],
|
||||
["EnvironmentTravelLocation", "ENVIRONMENT_TRAVEL_LOCATION_BACKUP_SELECT"],
|
||||
])("%s backup completeness", (model, selectName) => {
|
||||
const columns = scalarColumns(model);
|
||||
const selected = selectKeys(selectName);
|
||||
|
||||
it("parses both ends into a plausible shape", () => {
|
||||
expect(columns.length).toBeGreaterThan(8);
|
||||
expect(columns).toContain("locationEncrypted");
|
||||
expect(selected.length).toBeGreaterThan(5);
|
||||
});
|
||||
|
||||
it("the backup select covers every column", () => {
|
||||
const missing = columns.filter(
|
||||
(c) => !selected.includes(c) && !(c in EXCLUDED),
|
||||
);
|
||||
expect(
|
||||
missing,
|
||||
`${selectName} omits these ${model} columns; a restore brings them back NULL`,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("the restore writes every selected column back", () => {
|
||||
const unwritten = selected.filter(
|
||||
(c) =>
|
||||
!new RegExp(`\\b${c}:`).test(restoreBody) &&
|
||||
!airQualityList.includes(`"${c}"`) &&
|
||||
// The day key and the source are written under their own names.
|
||||
!["date", "startDate", "endDate"].includes(c),
|
||||
);
|
||||
expect(unwritten, "selected but never restored").toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("EnvironmentContext export", () => {
|
||||
it("writes every selected column into the entry", () => {
|
||||
const selected = selectKeys("ENVIRONMENT_CONTEXT_BACKUP_SELECT");
|
||||
const unwritten = selected.filter(
|
||||
(c) =>
|
||||
!LOCATION_COLUMNS.has(c) && !new RegExp(`row\\.${c}\\b`).test(source),
|
||||
);
|
||||
expect(unwritten, "selected but never exported").toEqual([]);
|
||||
});
|
||||
|
||||
it("covers all twenty-two air-quality columns and the sealed location", () => {
|
||||
const selected = selectKeys("ENVIRONMENT_CONTEXT_BACKUP_SELECT");
|
||||
for (const column of [
|
||||
"apparentMax",
|
||||
"pm25Mean",
|
||||
"o3Max8h",
|
||||
"pollenRagweedMax",
|
||||
"aqDomain",
|
||||
"aqHours",
|
||||
"aqFetchedAt",
|
||||
"locationEncrypted",
|
||||
]) {
|
||||
expect(selected, column).toContain(column);
|
||||
}
|
||||
});
|
||||
|
||||
it("carries no stale exclusion", () => {
|
||||
const all = new Set([
|
||||
...scalarColumns("EnvironmentContext"),
|
||||
...scalarColumns("EnvironmentTravelLocation"),
|
||||
]);
|
||||
for (const [column, reason] of Object.entries(EXCLUDED)) {
|
||||
expect(all.has(column), column).toBe(true);
|
||||
expect(reason.length).toBeGreaterThan(20);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -81,14 +81,6 @@ const RELEASE_PENDING_PREFIXES: Record<string, string> = {
|
||||
"auth.claim": "v1.42 managed-profile claim page (#959)",
|
||||
"recordSharing.managed.handover":
|
||||
"v1.42 managed-profile handover dialog (#959)",
|
||||
"settings.sections.environment.airQuality":
|
||||
"v1.42 environment settings, air quality (#615)",
|
||||
"environment.fields.apparentMax": "v1.42 air-quality field label (#615)",
|
||||
"environment.fields.pm10": "v1.42 air-quality field label (#615)",
|
||||
"environment.fields.no2": "v1.42 air-quality field label (#615)",
|
||||
"environment.fields.europeanAqi": "v1.42 air-quality field label (#615)",
|
||||
"environment.fields.uvIndexMax": "v1.42 air-quality field label (#615)",
|
||||
"environment.fields.dustMax": "v1.42 air-quality field label (#615)",
|
||||
"settings.appleHealth.freshness":
|
||||
"v1.42 per-type HealthKit freshness list (#1173)",
|
||||
"insights.workouts.manual.editTitle": "v1.42 manual workout edit (#1162)",
|
||||
|
||||
Reference in new issue
Block a user