mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
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:
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",
|
||||
|
||||
Reference in new issue
Block a user