diff --git a/src/app/api/admin/provider-health/__tests__/route.test.ts b/src/app/api/admin/provider-health/__tests__/route.test.ts index 75fb1b373..bcc30b8ef 100644 --- a/src/app/api/admin/provider-health/__tests__/route.test.ts +++ b/src/app/api/admin/provider-health/__tests__/route.test.ts @@ -60,6 +60,14 @@ import { GET } from "../route"; import { prisma } from "@/lib/db"; import { requireAdmin } from "@/lib/api-handler"; +// Reset, not clear, before every case: cases replace the paused rows, the +// settings row and the account groups, and a cleared mock would hand the +// next case whatever the last one left there. A reset mock goes back to the +// empty answer it was created with. +beforeEach(() => { + vi.resetAllMocks(); +}); + const groupBy = vi.mocked(prisma.providerHealth.groupBy); const usageRows = vi.mocked(prisma.coachUsage.findMany); @@ -101,7 +109,6 @@ function group( describe("GET /api/admin/provider-health", () => { beforeEach(() => { - vi.clearAllMocks(); vi.mocked(requireAdmin).mockResolvedValue({ user: { id: "admin1" }, } as never); @@ -302,7 +309,6 @@ describe("GET /api/admin/provider-health — pause and model check (v1.42)", () const settings = vi.mocked(prisma.appSettings.findUnique); beforeEach(() => { - vi.clearAllMocks(); vi.mocked(requireAdmin).mockResolvedValue({ user: { id: "admin" }, } as never); diff --git a/src/app/api/coach/about-me/__tests__/route-concurrency.test.ts b/src/app/api/coach/about-me/__tests__/route-concurrency.test.ts index c84a45162..249de579b 100644 --- a/src/app/api/coach/about-me/__tests__/route-concurrency.test.ts +++ b/src/app/api/coach/about-me/__tests__/route-concurrency.test.ts @@ -131,7 +131,10 @@ function putReq( } beforeEach(() => { - vi.clearAllMocks(); + // Reset, not clear: cases replace the profile reads and the question store + // with their own implementations, and a cleared mock keeps them. A reset + // mock returns to the implementation it was created with. + vi.resetAllMocks(); requireModuleEnabled.mockResolvedValue({ enabled: true }); }); diff --git a/src/app/api/export/__tests__/soft-delete-filter.test.ts b/src/app/api/export/__tests__/soft-delete-filter.test.ts index ef58cd010..50363cc58 100644 --- a/src/app/api/export/__tests__/soft-delete-filter.test.ts +++ b/src/app/api/export/__tests__/soft-delete-filter.test.ts @@ -174,6 +174,16 @@ beforeEach(() => { vi.mocked(isModuleEnabled).mockResolvedValue(true); vi.mocked(requireModuleEnabled).mockResolvedValue({ enabled: true } as never); vi.mocked(getSession).mockResolvedValue(SESSION_OK as never); + // A session that signed in just now. Reset, not only cleared: a case below + // swaps in a stale session, and `clearAllMocks` keeps the implementation + // it left behind for whichever case runs next. + vi.mocked(prisma.session.findUnique) + .mockReset() + .mockImplementation((async () => ({ + createdAt: new Date(), + mfaVerifiedAt: null, + reproofAt: null, + })) as never); vi.mocked(checkRateLimit).mockResolvedValue({ allowed: true, limit: 60, diff --git a/src/app/api/insights/chat/[id]/attachments/__tests__/route.test.ts b/src/app/api/insights/chat/[id]/attachments/__tests__/route.test.ts index a715ba6c3..be0ab6d2e 100644 --- a/src/app/api/insights/chat/[id]/attachments/__tests__/route.test.ts +++ b/src/app/api/insights/chat/[id]/attachments/__tests__/route.test.ts @@ -87,6 +87,15 @@ beforeEach(() => { vi.clearAllMocks(); vi.mocked(getSession).mockResolvedValue(SESSION_OK as never); vi.mocked(validateAttachmentCandidate).mockResolvedValue({ ok: true }); + // Open again for every case: the detach case closes every capability, and + // `clearAllMocks` would carry that refusal into whichever case runs next. + vi.mocked(requireAiCapability) + .mockReset() + .mockResolvedValue({ + available: true, + reason: null, + onDeviceAllowed: true, + } as never); }); describe("POST attach", () => { diff --git a/src/app/api/insights/chat/__tests__/route-tool-mode.test.ts b/src/app/api/insights/chat/__tests__/route-tool-mode.test.ts index 947964daa..8a7019860 100644 --- a/src/app/api/insights/chat/__tests__/route-tool-mode.test.ts +++ b/src/app/api/insights/chat/__tests__/route-tool-mode.test.ts @@ -276,7 +276,10 @@ function chatReq(body: Record): Request { describe("coach chat — tool-mode routing (F1)", () => { beforeEach(() => { - vi.clearAllMocks(); + // Reset, not clear: cases swap in their own tool loop and parsers, and a + // cleared mock would hand those to the next case. Every mock here goes + // back to the implementation it was created with. + vi.resetAllMocks(); reserveBudget.mockResolvedValue({ allowed: true, reserved: 3000, diff --git a/src/components/dashboard/__tests__/dashboard-header.test.tsx b/src/components/dashboard/__tests__/dashboard-header.test.tsx index 680ec64e8..6948538e8 100644 --- a/src/components/dashboard/__tests__/dashboard-header.test.tsx +++ b/src/components/dashboard/__tests__/dashboard-header.test.tsx @@ -52,6 +52,12 @@ import { primeLocales } from "@/__tests__/helpers/prime-locales"; primeLocales("de"); +// Every case starts on an account that never set a module, whichever case +// ran before it and left its own modules behind. +beforeEach(() => { + modulesRef.value = undefined; +}); + function renderSSR(node: React.ReactElement, locale: "en" | "de" = "de") { return renderToStaticMarkup( {node}, @@ -162,10 +168,6 @@ describe(" — quick-add menu", () => { }); describe(" — Log workout", () => { - beforeEach(() => { - modulesRef.value = undefined; - }); - it("offers it beside the other capture entries", () => { modulesRef.value = { workouts: true }; const html = renderSSR( @@ -187,10 +189,6 @@ describe(" — Log workout", () => { }); describe(" — every entry follows its module, as the capture picker does", () => { - beforeEach(() => { - modulesRef.value = undefined; - }); - it("withholds Log mood when the mood module is off", () => { modulesRef.value = { mood: false }; const html = renderSSR( diff --git a/src/components/illness/__tests__/use-illness-episodes-paging.test.ts b/src/components/illness/__tests__/use-illness-episodes-paging.test.ts index e447bc115..79814a155 100644 --- a/src/components/illness/__tests__/use-illness-episodes-paging.test.ts +++ b/src/components/illness/__tests__/use-illness-episodes-paging.test.ts @@ -5,7 +5,7 @@ * off the illness page, its settings and every picker once a record held more * than that, and they could then be found only through search. */ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const calls = vi.hoisted(() => [] as string[]); @@ -41,6 +41,12 @@ vi.mock("@/lib/api/api-fetch", () => ({ import { useIllnessEpisodes } from "../use-illness"; describe("useIllnessEpisodes", () => { + // Each case states its own journal and counts only its own requests. + beforeEach(() => { + calls.length = 0; + journal.total = 230; + }); + it("reads page after page until the whole journal is in", async () => { const options = useIllnessEpisodes(true) as unknown as { queryFn: () => Promise<{ id: string }[]>; @@ -55,7 +61,6 @@ describe("useIllnessEpisodes", () => { }); it("stops on a full last page the route says is the end", async () => { - calls.length = 0; journal.total = 200; const options = useIllnessEpisodes(true) as unknown as { queryFn: () => Promise<{ id: string }[]>; @@ -63,6 +68,5 @@ describe("useIllnessEpisodes", () => { expect(await options.queryFn()).toHaveLength(200); // Two full pages of 100, no third request for an empty one. expect(calls).toHaveLength(2); - journal.total = 230; }); }); diff --git a/src/lib/insights/__tests__/usage-level-simulation.test.ts b/src/lib/insights/__tests__/usage-level-simulation.test.ts index 1f51ab309..b09e8e657 100644 --- a/src/lib/insights/__tests__/usage-level-simulation.test.ts +++ b/src/lib/insights/__tests__/usage-level-simulation.test.ts @@ -129,9 +129,27 @@ function day(): Event[] { return events.sort((a, b) => a.at.localeCompare(b.at)); } -const ALL_SCOPES_AT_WARM = new Set(); +/** + * Every scope the day dirties: run it once on `full` with no warm, which + * enqueues each scope at least once. Each case derives it itself rather than + * reading what an earlier case left behind. + */ +async function discoverWarmScopes(): Promise> { + sim.notes.clear(); + sim.level = "FULL"; + for (const event of day()) { + vi.setSystemTime(new Date(`2026-10-15T${event.at}:00Z`)); + await invalidateStatusInsightsForTypes("u1", event.types, { + source: event.source, + }); + } + return new Set(sim.notes.keys()); +} -async function simulate(level: string | null): Promise<{ +async function simulate( + level: string | null, + warmScopes: ReadonlySet, +): Promise<{ warm: number; ingest: number; bySource: Record<"sync" | "manual", number>; @@ -141,8 +159,8 @@ async function simulate(level: string | null): Promise<{ sim.level = level; // The 04:30 warm regenerates every scope the day will touch. const warmAt = Date.parse("2026-10-15T04:30:00Z"); - for (const scope of ALL_SCOPES_AT_WARM) sim.notes.set(scope, warmAt); - const warm = ALL_SCOPES_AT_WARM.size; + for (const scope of warmScopes) sim.notes.set(scope, warmAt); + const warm = warmScopes.size; const bySource = { sync: 0, manual: 0 }; for (const event of day()) { vi.setSystemTime(new Date(`2026-10-15T${event.at}:00Z`)); @@ -161,25 +179,14 @@ beforeEach(() => { describe("generations per day on a watch-synced account", () => { it("counts the scopes once to seed the warm", async () => { - // Discover every scope the day dirties: run it once on `full` with no - // warm, which enqueues each scope at least once. - ALL_SCOPES_AT_WARM.clear(); - sim.notes.clear(); - sim.level = "FULL"; - for (const event of day()) { - vi.setSystemTime(new Date(`2026-10-15T${event.at}:00Z`)); - await invalidateStatusInsightsForTypes("u1", event.types, { - source: event.source, - }); - } - for (const scope of sim.notes.keys()) ALL_SCOPES_AT_WARM.add(scope); - expect(ALL_SCOPES_AT_WARM.size).toBe(19); + expect((await discoverWarmScopes()).size).toBe(19); }); it("before v1.43 (every ingest at one hour) versus each level", async () => { - const full = await simulate("FULL"); // the behaviour before v1.43 - const standard = await simulate("STANDARD"); - const frugal = await simulate("FRUGAL"); + const warmScopes = await discoverWarmScopes(); + const full = await simulate("FULL", warmScopes); // the behaviour before v1.43 + const standard = await simulate("STANDARD", warmScopes); + const frugal = await simulate("FRUGAL", warmScopes); const result = { full, standard, frugal }; process.stdout.write(`SIMULATION ${JSON.stringify(result)}\n`); diff --git a/src/lib/insights/derived/__tests__/derived-assessment-ai.test.ts b/src/lib/insights/derived/__tests__/derived-assessment-ai.test.ts index 553b3c8bc..b1a6e3b3a 100644 --- a/src/lib/insights/derived/__tests__/derived-assessment-ai.test.ts +++ b/src/lib/insights/derived/__tests__/derived-assessment-ai.test.ts @@ -34,6 +34,7 @@ import { resolveReadOnlyStatusMiss, } from "@/lib/insights/status-cache"; import { runStatusCompletion } from "@/lib/insights/status-provider"; +import { invalidateUserTimezone } from "@/lib/tz/resolver"; import { resolveDerivedAssessment, generateDerivedScoreAssessment, @@ -71,6 +72,9 @@ function okDerived(): Derived { beforeEach(() => { vi.resetAllMocks(); + // The zone resolver keeps a per-user cache for a minute; without this the + // first case to run fills it and every later one skips the read. + invalidateUserTimezone("u-ny"); findUnique.mockResolvedValue({ timezone: "America/New_York" }); }); diff --git a/src/lib/jobs/reminder/__tests__/backup-handlers.test.ts b/src/lib/jobs/reminder/__tests__/backup-handlers.test.ts index 5f386a2d7..637a97952 100644 --- a/src/lib/jobs/reminder/__tests__/backup-handlers.test.ts +++ b/src/lib/jobs/reminder/__tests__/backup-handlers.test.ts @@ -248,6 +248,13 @@ describe("handleDataBackup on a large instance", () => { beforeEach(() => { vi.clearAllMocks(); mocks.store.mockResolvedValue(undefined); + // Stated here rather than inherited: without a payload the pass stores + // nothing, and these cases used to pass only when another block's setup + // had run first and left its payload behind. + mocks.buildFullBackupPayload.mockResolvedValue({ + payload: { schemaVersion: "1", userId: "user-dr" }, + counts: {}, + }); }); it("backs up the accounts with the oldest copies first and stops on the budget", async () => { diff --git a/src/lib/withings/__tests__/sync-plausibility.test.ts b/src/lib/withings/__tests__/sync-plausibility.test.ts index 60901ebb6..cc8bb6105 100644 --- a/src/lib/withings/__tests__/sync-plausibility.test.ts +++ b/src/lib/withings/__tests__/sync-plausibility.test.ts @@ -211,8 +211,10 @@ describe("syncUserMeasurements — plausibility gate", () => { it("does not overwrite a stored reading with an out-of-band one", async () => { // The slot already holds a real pulse; the provider re-sends the same // instant carrying a decode slip. Ungated, the update branch rewrote the - // good value with the bad one. - vi.mocked(prisma.measurement.findMany).mockResolvedValueOnce([ + // good value with the bad one. Not `mockResolvedValueOnce`: the gate + // drops the reading before the stored rows are read, so a once-value + // would sit unconsumed and answer the next test's read instead. + vi.mocked(prisma.measurement.findMany).mockResolvedValue([ { id: "existing-row", type: "PULSE",