test(dawarich): pin the movement block, the channels and the mocks; document the connection

Covers the day section, the model projection without the travel country and
the two correlation channels; the backup and correlation mocks learn the new
delegate. Adds the operator page for DAWARICH_PRIVATE_ORIGINS and regenerates
the API contract.
This commit is contained in:
Marc-André Bombeck committed 2026-10-16 01:38:05 +02:00
1 parent b99bcaeec6
commit c03f79afd5
7 files changed
+372 -58

No files matched your search

@@ -216,6 +216,7 @@ beforeEach(() => {
vi.mocked(prisma.environmentTravelLocation.findMany).mockResolvedValue(
[] as never,
);
vi.mocked(prisma.dawarichDay.findMany).mockResolvedValue([] as never);
});
afterEach(() => {
@@ -160,6 +160,7 @@ beforeEach(() => {
).mockResolvedValue([]);
vi.mocked(prisma.customMetric.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.symptomDefinition.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.dawarichDay.findMany).mockResolvedValue([] as never);
vi.mocked(hadFindingsBeforeSeasonalAdjustment).mockResolvedValue(false);
});
@@ -0,0 +1,63 @@
/**
* "On the move" renders the server's values as they come and leaves out what
* the day has no value for: a null is no coverage, never a zero.
*/
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { I18nProvider } from "@/lib/i18n/context";
import { DayMovementSection } from "../day-movement";
function render(
movement: Parameters<typeof DayMovementSection>[0]["movement"],
) {
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<DayMovementSection movement={movement} />
</I18nProvider>,
);
}
const base = {
source: "dawarich" as const,
distance: { unit: "km" as const, total: 48.3, driving: 12, train: 0 },
placesVisited: 4,
awayFromHomeMinutes: 600,
travel: { isTravelDay: true, countryCode: "ES" },
fetchedAt: "2026-06-16T01:20:00.000Z",
};
describe("DayMovementSection", () => {
it("shows each value the day has", () => {
const html = render(base);
expect(html).toContain('data-movement="total"');
expect(html).toContain("48.3 km");
expect(html).toContain('data-movement="driving"');
expect(html).toContain('data-movement="away"');
expect(html).toContain("Spain");
// No train that day: no row, rather than "0 km".
expect(html).not.toContain('data-movement="train"');
});
it("leaves out time away from home without coverage", () => {
const html = render({
...base,
awayFromHomeMinutes: null,
travel: null,
});
expect(html).not.toContain('data-movement="away"');
expect(html).not.toContain('data-movement="travel"');
});
it("renders nothing when the day has no value at all", () => {
const html = render({
...base,
distance: { unit: "mi", total: null, driving: null, train: null },
placesVisited: null,
awayFromHomeMinutes: null,
travel: null,
});
expect(html).toBe("");
});
});
@@ -222,3 +222,36 @@ describe("scores", () => {
expect(JSON.stringify(projected.scores)).not.toContain("COMPUTED");
});
});
describe("the movement block (v1.43)", () => {
const withMovement: DayResponse = {
...day,
movement: {
source: "dawarich",
distance: { unit: "km", total: 48.3, driving: 12, train: 30.5 },
placesVisited: 4,
awayFromHomeMinutes: 600,
travel: { isTravelDay: true, countryCode: "ES" },
fetchedAt: "2026-03-30T01:20:00.000Z",
},
};
it("reaches a model without the country of the travel day", () => {
const model = projectDayForModel(withMovement, []);
expect(model.movement).toEqual({
distance: { unit: "km", total: 48.3, driving: 12, train: 30.5 },
placesVisited: 4,
awayFromHomeMinutes: 600,
travel: { isTravelDay: true },
});
expect(JSON.stringify(model)).not.toContain("ES");
});
it("stays out when the caller excludes the environment section", () => {
const model = projectDayForModel(withMovement, [], undefined, {
sections: new Set(["environment"]),
types: new Set(),
});
expect(model).not.toHaveProperty("movement");
});
});
@@ -0,0 +1,47 @@
/**
* The two Dawarich movement channels: hours away from home and kilometres
* travelled, one point per stored day that has the value. A day without it is
* no coverage and gets no point, never a zero.
*/
import { describe, expect, it, vi } from "vitest";
const { findMany } = vi.hoisted(() => ({ findMany: vi.fn() }));
vi.mock("@/lib/db", () => ({ prisma: { dawarichDay: { findMany } } }));
import { fetchMovementSeries } from "@/lib/insights/correlation-channel-series";
import { metricFamily } from "@/lib/insights/correlation-discovery";
describe("fetchMovementSeries", () => {
it("emits a point only where the day has the value", async () => {
findMany.mockResolvedValue([
{ date: "2026-06-14", awayFromHomeMinutes: 90, totalDistanceM: null },
{ date: "2026-06-15", awayFromHomeMinutes: null, totalDistanceM: 48_300 },
]);
const [away, distance] = await fetchMovementSeries(
"u1",
"Europe/Berlin",
new Date("2026-06-01T12:00:00Z"),
);
expect(away).toMatchObject({
key: "MOVEMENT_AWAY",
role: "behaviour",
points: [{ day: "2026-06-14", value: 1.5 }],
});
expect(distance).toMatchObject({
key: "MOVEMENT_DISTANCE",
role: "behaviour",
points: [{ day: "2026-06-15", value: 48.3 }],
});
expect(findMany.mock.calls[0][0].where).toEqual({
userId: "u1",
date: { gte: "2026-06-01" },
});
});
it("keeps the two channels in one family so they never pair with each other", () => {
expect(metricFamily("MOVEMENT_AWAY")).toBe(
metricFamily("MOVEMENT_DISTANCE"),
);
expect(metricFamily("MOVEMENT_AWAY")).not.toBe(metricFamily("MOOD"));
});
});