Merge branch 'v143/dawarich' into release/v1.43.0

# Conflicts:
#	src/__tests__/contract-stub-inventory.test.ts
This commit is contained in:
Marc-André Bombeck committed 2026-10-16 02:51:02 +02:00
commit da860aeb3e
101 files changed
+5354 -216

No files matched your search

+1 -14
View File
@@ -33,23 +33,10 @@ const CONTRACT_STUBS: Readonly<Record<string, readonly string[]>> = {
"anamnesis.life_circumstances.confirm",
],
"anamnesis/life-circumstances/route.ts": ["anamnesis.life_circumstances.get"],
"dawarich/connect/route.ts": ["dawarich.connect"],
"dawarich/disconnect/route.ts": ["dawarich.disconnect"],
"dawarich/places/route.ts": ["dawarich.places.get"],
"dawarich/settings/route.ts": ["dawarich.settings.patch"],
"dawarich/status/route.ts": ["dawarich.status.get"],
"dawarich/sync/route.ts": ["dawarich.sync"],
"dawarich/test/route.ts": ["dawarich.test"],
"environment/travel/suggestions/[id]/dismiss/route.ts": [
"environment.travel_suggestion.dismiss",
],
"environment/travel/suggestions/route.ts": [
"environment.travel_suggestions.get",
],
};
/** The size of the register when it was written; see the header. */
const REGISTER_SIZE = 14;
const REGISTER_SIZE = 5;
function routeFiles(): string[] {
return walkSourceFiles(API_ROOT, { floor: 400, extensions: [".ts"] })
@@ -0,0 +1,109 @@
/**
* v1.43 — pair guard binding `DawarichDay` in `prisma/schema.prisma` to the
* backup that carries it (`src/lib/export/environment-backup.ts`, the
* `dawarichDays` arm of the environment section), on the pattern of
* `environment-backup-completeness.test.ts`.
*
* A column missing from the select comes back NULL on every restored day
* without a word, because the section schema is `.passthrough()`. So both
* ends are compared as literals:
*
* - every scalar column of `DawarichDay` is in `DAWARICH_DAY_BACKUP_SELECT`
* (or excluded here with a reason);
* - every selected column is written by the export (`row.<column>`);
* - every selected column is written back by the restore (`<column>:`).
*
* Mutation check: delete `trainDistanceM: true,` from the select and the
* first case names it; delete `trainDistanceM:` from the restore 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",
);
const EXCLUDED: Record<string, string> = {
id: "nothing addresses a day by its id; a day is identified by its date",
userId:
"user-scoped backup; the owner is re-derived from the restoring account",
};
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;
}
const columns = scalarColumns("DawarichDay");
const selected = [
...block(source, "const DAWARICH_DAY_BACKUP_SELECT = {").matchAll(
/^ {2}(\w+): true,$/gm,
),
].map((m) => m[1]);
const exportBody = block(source, "dawarichDays: dawarichRows.map(", "})),");
const restoreBody = block(
source,
"export async function restoreEnvironmentData(",
"\n}\n",
);
const restoreDawarich = block(restoreBody, "tx.dawarichDay.createMany(", "});");
describe("DawarichDay backup completeness", () => {
it("parses both ends into a plausible shape", () => {
expect(columns).toContain("countryCodeEncrypted");
expect(columns).toContain("awayFromHomeMinutes");
expect(selected.length).toBeGreaterThan(8);
});
it("the backup select covers every column", () => {
const missing = columns.filter(
(c) => !selected.includes(c) && !(c in EXCLUDED),
);
expect(
missing,
"DAWARICH_DAY_BACKUP_SELECT omits these columns; a restore brings them back NULL",
).toEqual([]);
});
it("the export writes every selected column into the entry", () => {
const unwritten = selected.filter(
(c) => !new RegExp(`row\\.${c}\\b`).test(exportBody),
);
expect(unwritten, "selected but never exported").toEqual([]);
});
it("the restore writes every selected column back", () => {
const unwritten = selected.filter(
(c) => !new RegExp(`\\b${c}:`).test(restoreDawarich),
);
expect(unwritten, "selected but never restored").toEqual([]);
});
it("the restore clears the days it replaces", () => {
expect(restoreBody).toMatch(/tx\.dawarichDay\.deleteMany\(/);
});
});
@@ -0,0 +1,73 @@
/**
* v1.43 — Dawarich is a movement context, never a location store. The schema
* holds daily values only: no column of `DawarichDay`, and no Dawarich column
* on `User`, may hold a coordinate, a place name, an address or a visit.
*
* The check reads the column names and their doc comments in
* `prisma/schema.prisma`. A new column whose name or description says
* latitude, longitude, coordinates, geometry, address, label, a place name or
* a visit list fails here and has to argue its case in review.
*
* Mutation check: add `placeName String?` to `DawarichDay` and the first case
* names it.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const schema = readFileSync(
join(process.cwd(), "prisma", "schema.prisma"),
"utf8",
);
const FORBIDDEN =
/\b(lat|lon|lng|latitude|longitude|coord\w*|geo\w*|geometry|address|label|street|city|placeName|place_name|visits?Json|track|path|point)\b/i;
/** Column lines of a model with the `///` doc comment above each. */
function columns(model: string): Array<{ name: string; text: string }> {
const start = schema.indexOf(`model ${model} {`);
expect(start, `model ${model} not found`).toBeGreaterThan(-1);
const body = schema.slice(start, schema.indexOf("\n}", start));
const out: Array<{ name: string; text: string }> = [];
let doc = "";
for (const raw of body.split("\n").slice(1)) {
const line = raw.trim();
if (line.startsWith("///")) {
doc += ` ${line.slice(3)}`;
continue;
}
const m = /^([a-zA-Z_]\w*)\s+\w/.exec(line);
if (m && !line.startsWith("@@"))
out.push({ name: m[1], text: `${m[1]} ${doc}` });
doc = "";
}
return out;
}
describe("Dawarich stores no location", () => {
it("DawarichDay holds daily values only", () => {
const cols = columns("DawarichDay");
expect(cols.map((c) => c.name)).toContain("awayFromHomeMinutes");
const offending = cols
.filter((c) => c.name !== "user")
.filter((c) => FORBIDDEN.test(c.name) || FORBIDDEN.test(c.text))
.map((c) => c.name);
expect(offending).toEqual([]);
});
it("the User row holds the connection, never a place", () => {
const cols = columns("User").filter((c) => /^dawarich/.test(c.name));
expect(cols.map((c) => c.name).sort()).toEqual([
"dawarichAllowPrivateHost",
"dawarichApiKeyEncrypted",
"dawarichDays",
"dawarichPrefsEncrypted",
"dawarichUrlEncrypted",
]);
const offending = cols
.filter((c) => FORBIDDEN.test(c.name))
.map((c) => c.name);
expect(offending).toEqual([]);
});
});
@@ -649,6 +649,14 @@ const REGISTRY: DestructiveEntry[] = [
recovery: "permanent",
confirm: ["ConfirmDialog"],
},
{
file: "components/settings/integrations/dawarich-card.tsx",
destroys:
"the stored Dawarich address, key and home settings, and with the second entry every stored movement day — reconnecting means typing them again; deleted days come back only as far as the instance still holds them",
triggers: ["components/settings/integrations/integration-card.tsx"],
recovery: "permanent",
confirm: ["ConfirmDialog"],
},
{
file: "components/settings/integrations/document-sources-card.tsx",
destroys:
@@ -157,6 +157,7 @@ describe("record settings contract", () => {
oura: false,
"google-health": false,
strava: false,
dawarich: false,
} as const;
expect(
@@ -187,6 +188,7 @@ describe("record settings contract", () => {
oura: false,
"google-health": false,
strava: false,
dawarich: false,
} as const;
expect(
@@ -377,6 +377,11 @@ const PINNED_AFFORDANCES: Record<
"toast.success": 2,
},
// The saved-credentials confirmation line.
// Connected, a fetch queued, the home saved: confirmations of a request
// that wrote no record (the fetch itself reports through `health`).
"src/components/settings/integrations/dawarich-card.tsx": {
"toast.success": 3,
},
"src/components/settings/integrations/credentials-form.tsx": {
"text-success": 1,
},