diff --git a/tests/integration/ai-optional-data-routes.test.ts b/tests/integration/ai-optional-data-routes.test.ts index 465e775d0..c91e3657e 100644 --- a/tests/integration/ai-optional-data-routes.test.ts +++ b/tests/integration/ai-optional-data-routes.test.ts @@ -257,7 +257,20 @@ async function readAll( return out; } -let baseline: Record | null = null; +type Reads = Record; +let baseline: Reads | null = null; + +/** + * The all-on reads, taken once in a world of their own and then cleared, so + * a state case compares against them whichever case runs first. + */ +async function readBaseline(): Promise { + if (baseline === null) { + baseline = await readAll(await enterState("S0", seed), "S0"); + await resetWorld(); + } + return baseline; +} beforeEach(async () => { await resetWorld(); @@ -265,8 +278,7 @@ beforeEach(async () => { describe("data routes under every AI state", () => { it("S0 (all on) is the baseline, and every read answers", async () => { - const world = await enterState("S0", seed); - baseline = await readAll(world, "S0"); + const baseline = await readBaseline(); for (const [name, { status }] of Object.entries(baseline)) { expect(status, name).toBe(200); } @@ -288,12 +300,12 @@ describe("data routes under every AI state", () => { it.each(["S1", "S2", "S3", "S4", "S5", "S6", "S8"] as const)( "%s answers every read with the baseline's data", async (state) => { - expect(baseline, "the S0 baseline runs first").not.toBeNull(); + const base = await readBaseline(); const world = await enterState(state, seed); const reads = await readAll(world, state); for (const [name, { status, body }] of Object.entries(reads)) { expect(status, `${state} ${name}`).toBe(200); - expect(body, `${state} ${name}`).toEqual(baseline![name].body); + expect(body, `${state} ${name}`).toEqual(base[name].body); } }, ); diff --git a/tests/integration/all-time-extremes-tier-parity.test.ts b/tests/integration/all-time-extremes-tier-parity.test.ts index c73ad99b0..587e3a96c 100644 --- a/tests/integration/all-time-extremes-tier-parity.test.ts +++ b/tests/integration/all-time-extremes-tier-parity.test.ts @@ -18,7 +18,7 @@ * Means are compared to nine significant digits: the tier sums the same day * values in a different order. Extremes are compared exactly. */ -import { beforeAll, describe, expect, it } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; import { getPrismaClient, truncateAllTables } from "./setup"; import { Prisma, type MeasurementType } from "@/generated/prisma/client"; @@ -246,6 +246,12 @@ beforeAll(async () => { }, 300_000); describe("readAllTimeExtremes over the DAY tier", () => { + // A clean tier over whatever rows are left for every case: some cases + // break a tier row on purpose, and the next case must not read it. + beforeEach(async () => { + await recomputeUserRollups(USER, { granularities: ["DAY"] }); + }, 120_000); + it("reads the tier for every type", async () => { const covered = await prisma.measurementRollup.groupBy({ by: ["type"], diff --git a/tests/integration/google-health-backfill-multi-year.test.ts b/tests/integration/google-health-backfill-multi-year.test.ts index b45003c83..4ec776bfa 100644 --- a/tests/integration/google-health-backfill-multi-year.test.ts +++ b/tests/integration/google-health-backfill-multi-year.test.ts @@ -58,7 +58,7 @@ import v8 from "node:v8"; import vm from "node:vm"; import type { Job } from "pg-boss"; -import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; import type { WideEvent } from "@/lib/logging/types"; import { getPrismaClient, truncateAllTables } from "./setup"; @@ -324,10 +324,6 @@ async function seed(points: number): Promise { fake.upperBoundRequests = 0; } -beforeAll(async () => { - await seed(POINTS); -}, 120_000); - afterAll(async () => { await truncateAllTables(getPrismaClient()); }); @@ -357,6 +353,9 @@ function nextSlice(): AdmissionPayload | null { describe(`Google Health backfill over ${DAYS} days of heart rate (#1220)`, () => { it("completes slice by slice, imports every reading once, and keeps the hourly sync running", async () => { + // Its own history, as the case below seeds its own: neither reads what + // the other left behind, whichever runs first. + await seed(POINTS); const { drainIntegrationBackfillAdmission } = await import("@/lib/jobs/integration-backfill-drain"); const { handleGoogleHealthSync } = diff --git a/tests/integration/job-failures.test.ts b/tests/integration/job-failures.test.ts index 981353dd0..f13180fd7 100644 --- a/tests/integration/job-failures.test.ts +++ b/tests/integration/job-failures.test.ts @@ -13,7 +13,7 @@ * are failed through its own API. */ import { PgBoss } from "pg-boss"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { readActiveJobsStartedBefore, @@ -47,7 +47,7 @@ async function failOneJob( return jobId; } -beforeAll(async () => { +async function startBoss(): Promise { boss = new PgBoss({ connectionString: process.env.DATABASE_URL, // No maintenance or monitoring timers — the test drives every transition. @@ -57,7 +57,15 @@ beforeAll(async () => { await boss.start(); await boss.createQueue(QUEUE); await boss.createQueue(OTHER_QUEUE); -}, 60_000); +} + +beforeAll(startBoss, 60_000); + +// Every case reads the queue it filled itself: no job a case before it sent, +// failed or left active is still there to be counted or fetched. +beforeEach(async () => { + await getPrismaClient().$executeRawUnsafe(`DELETE FROM pgboss.job`); +}); afterAll(async () => { await boss?.stop({ graceful: false, close: true }); @@ -244,6 +252,8 @@ describe("no queue schema at all", () => { await prisma.$executeRawUnsafe( `ALTER SCHEMA pgboss_gone RENAME TO pgboss`, ); + // Running again for whichever case comes next. + await startBoss(); } }); }); diff --git a/tests/integration/json-import-own-exports.test.ts b/tests/integration/json-import-own-exports.test.ts index 29540ce28..3c79a918c 100644 --- a/tests/integration/json-import-own-exports.test.ts +++ b/tests/integration/json-import-own-exports.test.ts @@ -138,11 +138,14 @@ function byInstant(at: string) { String(a.type ?? "").localeCompare(String(b.type ?? "")); } -beforeAll(async () => { +/** This server: the seeded catalogue and the owner whose files are read. */ +async function seedServer(): Promise { await truncateAllTables(prisma); await seedJsonImportCatalogue(prisma); ({ ownerId } = await seedJsonImportOwner(prisma)); -}); +} + +beforeAll(seedServer); afterAll(async () => { await truncateAllTables(prisma); @@ -350,6 +353,10 @@ describe("the JSON import takes the app's own files back", () => { }); describe("a full backup moved to another server", () => { + // The case empties the database to stand in for another server. Put this + // server back for the cases that run after it. + afterAll(seedServer); + it("comes back equal, the account's own tags, groups and rated factor included", async () => { const original = await fullBackupOf(ownerId); expect(original.customMoodTags.length).toBeGreaterThanOrEqual(2); diff --git a/tests/integration/measurement-hot-update.test.ts b/tests/integration/measurement-hot-update.test.ts index 7b5144c1c..a24efcb7e 100644 --- a/tests/integration/measurement-hot-update.test.ts +++ b/tests/integration/measurement-hot-update.test.ts @@ -55,18 +55,27 @@ describe("an update that changes nothing", () => { const row = await seedReading(); await new Promise((r) => setTimeout(r, 20)); + // Read as a difference within the transaction. Since Postgres 15 the + // `pg_stat_xact_*` views also carry counts an earlier transaction on the + // same connection has not flushed yet (stats flush at most once a + // second), so an update from the case before this one could show up. const hot = await prisma.$transaction(async (tx) => { + const read = async () => { + const [stats] = await tx.$queryRaw>` + SELECT n_tup_hot_upd AS hot, n_tup_upd AS upd + FROM pg_stat_xact_user_tables WHERE relname = 'measurements'`; + return { hot: Number(stats?.hot ?? 0), upd: Number(stats?.upd ?? 0) }; + }; + const before = await read(); await tx.measurement.update({ where: { id: row.id }, data: { value: 72.3, deletedAt: null, syncVersion: { increment: 1 } }, }); - const [stats] = await tx.$queryRaw>` - SELECT n_tup_hot_upd AS hot, n_tup_upd AS upd - FROM pg_stat_xact_user_tables WHERE relname = 'measurements'`; - return stats; + const after = await read(); + return { hot: after.hot - before.hot, upd: after.upd - before.upd }; }); - expect(Number(hot.upd)).toBe(1); - expect(Number(hot.hot)).toBe(1); + expect(hot.upd).toBe(1); + expect(hot.hot).toBe(1); const after = await prisma.measurement.findUniqueOrThrow({ where: { id: row.id }, diff --git a/tests/integration/query-budget-hot-routes.test.ts b/tests/integration/query-budget-hot-routes.test.ts index 0a50202d6..f16576db8 100644 --- a/tests/integration/query-budget-hot-routes.test.ts +++ b/tests/integration/query-budget-hot-routes.test.ts @@ -428,15 +428,35 @@ describe("statement budgets on the hot routes (real Postgres)", () => { externalId: `budget-hr-${i}`, }; }); - const m = await countStatements(async () => { - const res = await (POST as unknown as Handler)( + const post = (body: unknown) => + (POST as unknown as Handler)( new NextRequest("http://localhost/api/measurements/batch", { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ entries }), + body: JSON.stringify(body), }), { params: Promise.resolve({}) }, ); + // One reading a month earlier first, as the reads warm up: it fills the + // process-level caches a running server holds, which this case would + // otherwise pay for whenever it runs before the reads do. + const warmAt = daysAgo(30).toISOString(); + const warm = await post({ + entries: [ + { + hkIdentifier: "HKQuantityTypeIdentifierHeartRate", + value: 60, + unit: "count/min", + startDate: warmAt, + endDate: warmAt, + externalId: "budget-hr-warm", + }, + ], + }); + expect(warm.status, "batch warm-up").toBe(200); + await evictUserCaches(); + const m = await countStatements(async () => { + const res = await post({ entries }); return { status: res.status, body: (await res.json()) as { data: { inserted: number } }, diff --git a/tests/integration/request-clock-windows.cases.ts b/tests/integration/request-clock-windows.cases.ts index 828ecd18e..6e59232e5 100644 --- a/tests/integration/request-clock-windows.cases.ts +++ b/tests/integration/request-clock-windows.cases.ts @@ -157,6 +157,9 @@ export function defineRequestClockCases(expectedZone: string): void { }); it("cuts every window of the live aggregate on the request's clock", async () => { + // The live path is the one without a tier; drop whatever the rollup + // case built if it ran first. + await prisma.measurementRollup.deleteMany({ where: { userId } }); await expectWindows("live"); }); diff --git a/tests/integration/sharing-manage-handler-matrix.test.ts b/tests/integration/sharing-manage-handler-matrix.test.ts index abc6f0f37..2954127f5 100644 --- a/tests/integration/sharing-manage-handler-matrix.test.ts +++ b/tests/integration/sharing-manage-handler-matrix.test.ts @@ -33,7 +33,7 @@ * the generation leg asserts both halves — nothing enqueued for the manager, * something enqueued for the owner doing the identical thing. */ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { NextRequest } from "next/server"; @@ -276,6 +276,8 @@ const STRICT_DRIVER_KEYS = new Set(); * It is the executable answer to "did this route actually do its work?". */ const REAL_EFFECT_KEYS = new Set(); +/** The drivers whose effect case started in this run, filtered or not. */ +const EFFECT_CASES_STARTED = new Set(); const ROUTE_IMPORTS = import.meta.glob>( "/src/app/api/**/route.ts", ); @@ -309,6 +311,7 @@ function manageContract(name: string, c: ManageCase) { STRICT_DRIVER_KEYS.add(matrixKey(c.contract)); describe(name, () => { it("changes the owner's record and names the manager as actor", async () => { + EFFECT_CASES_STARTED.add(matrixKey(c.contract)); const owner = await makeUser("owner"); const manager = await makeUser("manager"); const ctx = await c.prepare(owner.id); @@ -385,6 +388,7 @@ function writeEffectContract(name: string, c: ManageCase) { }); it("writes for the owner and audits the WRITE actor", async () => { + EFFECT_CASES_STARTED.add(matrixKey(c.contract)); const owner = await makeUser("owner"); const delegate = await makeUser("writer"); const ctx = await c.prepare(owner.id); @@ -3023,10 +3027,15 @@ describe("the complete MANAGE handler matrix", () => { expect(STRICT_DRIVER_KEYS.size).toBe(93); expect([...STRICT_DRIVER_KEYS].sort()).toEqual(expected); }); +}); - it("executes every registered driver through its owned effect and actor audit", () => { - const expected = ADMITTED_MUTATING_HANDLERS.map(matrixKey).sort(); - expect(REAL_EFFECT_KEYS.size).toBe(93); - expect([...REAL_EFFECT_KEYS].sort()).toEqual(expected); - }); +// Every registered driver went through its owned effect and actor audit. +// After all cases, not as one of them: the set fills as the effect cases +// run, and a case placed last in the file was read first whenever the order +// was shuffled. A run filtered down to some cases has nothing to say here. +afterAll(() => { + if (EFFECT_CASES_STARTED.size !== STRICT_DRIVER_KEYS.size) return; + const expected = ADMITTED_MUTATING_HANDLERS.map(matrixKey).sort(); + expect(REAL_EFFECT_KEYS.size).toBe(93); + expect([...REAL_EFFECT_KEYS].sort()).toEqual(expected); });