test(measurements): mock the status re-warm where the mutation tail imports it

The bulk-delete, restore and by-external-ids suites mocked
`invalidateStatusInsightsForTypes` on the comprehensive-generate barrel,
while the shared post-mutation tail imports it from status-invalidation.
The mock was never called, the real re-warm ran the capability loader
against a stub database, and the "no invalidate" assertion could not
fail. The mock now sits on the imported module; the positive cases
assert the re-warm with the distinct types, the zero-row cases assert
its absence.
This commit is contained in:
Marc-André Bombeck committed 2026-09-24 10:52:04 +02:00
1 parent 863d32af5a
commit 00f6eb6dcb
3 files changed
+35 -6

No files matched your search

@@ -37,7 +37,10 @@ vi.mock("@/lib/idempotency", () => ({
vi.mock("@/lib/cache/invalidate", () => ({
invalidateUserMeasurements: vi.fn(),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
// The status re-warm the shared post-mutation tail fires. Mocked at the
// module the tail imports it from; a mock anywhere else is never called, and
// the real one runs the AI capability loader against this file's stub db.
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/rollups/measurement-rollups", async () => {
@@ -68,7 +71,7 @@ import { prisma } from "@/lib/db";
import { getSession } from "@/lib/auth/session";
import { checkRateLimit } from "@/lib/rate-limit";
import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate";
import { invalidateStatusInsightsForTypes } from "@/lib/insights/status-invalidation";
import { recomputeBucketsForMeasurement } from "@/lib/rollups/measurement-rollups";
const SESSION_OK = {
@@ -149,6 +152,12 @@ describe("POST /api/measurements/bulk-delete", () => {
// 4 rows in, 3 distinct (type, day) recomputes out.
expect(recomputeBucketsForMeasurement).toHaveBeenCalledTimes(3);
// And one status re-warm over the distinct types.
expect(invalidateStatusInsightsForTypes).toHaveBeenCalledTimes(1);
expect(invalidateStatusInsightsForTypes).toHaveBeenCalledWith("user-1", [
"WEIGHT",
"PULSE",
]);
});
it("is a no-op (deleted: 0) when no id is owned — no rollup, no invalidate", async () => {
@@ -163,6 +172,7 @@ describe("POST /api/measurements/bulk-delete", () => {
expect(body.data.deleted).toBe(0);
expect(invalidateUserMeasurements).not.toHaveBeenCalled();
expect(recomputeBucketsForMeasurement).not.toHaveBeenCalled();
expect(invalidateStatusInsightsForTypes).not.toHaveBeenCalled();
});
it("rejects a batch over the 200-id cap with 422", async () => {
@@ -28,7 +28,10 @@ vi.mock("@/lib/auth/audit", () => ({
vi.mock("@/lib/cache/invalidate", () => ({
invalidateUserMeasurements: vi.fn(),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
// The status re-warm the shared post-mutation tail fires. Mocked at the
// module the tail imports it from; a mock anywhere else is never called, and
// the real one runs the AI capability loader against this file's stub db.
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/rollups/measurement-rollups", async () => {
@@ -57,7 +60,7 @@ import { DELETE } from "../route";
import { prisma } from "@/lib/db";
import { getSession } from "@/lib/auth/session";
import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate";
import { invalidateStatusInsightsForTypes } from "@/lib/insights/status-invalidation";
import { recomputeBucketsForMeasurement } from "@/lib/rollups/measurement-rollups";
const SESSION_OK = {
@@ -112,6 +115,12 @@ describe("DELETE /api/measurements/by-external-ids", () => {
externalId: { in: ["uuid-aaa"] },
deletedAt: null,
});
// The deleted row's type is re-warmed; the zero-row case below is the
// twin that must not reach it.
expect(invalidateStatusInsightsForTypes).toHaveBeenCalledWith("user-1", [
"WEIGHT",
]);
});
it("is a no-op for a foreign-origin externalId that collides on a non-app-minted row", async () => {
@@ -37,7 +37,10 @@ vi.mock("@/lib/idempotency", () => ({
vi.mock("@/lib/cache/invalidate", () => ({
invalidateUserMeasurements: vi.fn(),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
// The status re-warm the shared post-mutation tail fires. Mocked at the
// module the tail imports it from; a mock anywhere else is never called, and
// the real one runs the AI capability loader against this file's stub db.
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/rollups/measurement-rollups", async () => {
@@ -68,7 +71,7 @@ import { prisma } from "@/lib/db";
import { getSession } from "@/lib/auth/session";
import { checkRateLimit } from "@/lib/rate-limit";
import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate";
import { invalidateStatusInsightsForTypes } from "@/lib/insights/status-invalidation";
import { recomputeBucketsForMeasurement } from "@/lib/rollups/measurement-rollups";
const SESSION_OK = {
@@ -144,6 +147,12 @@ describe("POST /api/measurements/restore", () => {
// 4 rows in, 3 distinct (type, day) recomputes out.
expect(recomputeBucketsForMeasurement).toHaveBeenCalledTimes(3);
// And one status re-warm over the distinct types.
expect(invalidateStatusInsightsForTypes).toHaveBeenCalledTimes(1);
expect(invalidateStatusInsightsForTypes).toHaveBeenCalledWith("user-1", [
"WEIGHT",
"PULSE",
]);
});
it("is a no-op (restored: 0) when no id is owned + tombstoned — no rollup, no invalidate", async () => {
@@ -158,6 +167,7 @@ describe("POST /api/measurements/restore", () => {
expect(body.data.restored).toBe(0);
expect(invalidateUserMeasurements).not.toHaveBeenCalled();
expect(recomputeBucketsForMeasurement).not.toHaveBeenCalled();
expect(invalidateStatusInsightsForTypes).not.toHaveBeenCalled();
});
it("rejects a batch over the 200-id cap with 422", async () => {