test: give every case its own mock state under a shuffled order

Cases leaned on what an earlier case set up or left behind: a once-value
the code under test never consumed, an implementation swapped in and
kept by clearAllMocks, a payload only another block's setup provided,
the zone resolver's per-user cache, module state in a hoisted object,
and a scope set one case discovered for the next. Each now resets or
states what it reads.
This commit is contained in:
Marc-André Bombeck committed 2026-11-03 20:58:38 +01:00
1 parent 4f7802ba28
commit 227c80878c
11 files changed
+90 -37

No files matched your search

@@ -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);
@@ -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 });
});
@@ -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,
@@ -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", () => {
@@ -276,7 +276,10 @@ function chatReq(body: Record<string, unknown>): 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,
@@ -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(
<I18nProvider initialLocale={locale}>{node}</I18nProvider>,
@@ -162,10 +168,6 @@ describe("<DashboardHeader> — quick-add menu", () => {
});
describe("<DashboardHeader> — 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("<DashboardHeader> — Log workout", () => {
});
describe("<DashboardHeader> — 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(
@@ -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;
});
});
@@ -129,9 +129,27 @@ function day(): Event[] {
return events.sort((a, b) => a.at.localeCompare(b.at));
}
const ALL_SCOPES_AT_WARM = new Set<string>();
/**
* 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<Set<string>> {
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<string>,
): 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`);
@@ -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<unknown> {
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" });
});
@@ -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 () => {
@@ -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",