diff --git a/src/app/api/measurements/bulk-delete/__tests__/route.test.ts b/src/app/api/measurements/bulk-delete/__tests__/route.test.ts index 9cba46767..32ff2f171 100644 --- a/src/app/api/measurements/bulk-delete/__tests__/route.test.ts +++ b/src/app/api/measurements/bulk-delete/__tests__/route.test.ts @@ -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 () => { diff --git a/src/app/api/measurements/by-external-ids/__tests__/route.test.ts b/src/app/api/measurements/by-external-ids/__tests__/route.test.ts index c618a6bc8..072608711 100644 --- a/src/app/api/measurements/by-external-ids/__tests__/route.test.ts +++ b/src/app/api/measurements/by-external-ids/__tests__/route.test.ts @@ -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 () => { diff --git a/src/app/api/measurements/restore/__tests__/route.test.ts b/src/app/api/measurements/restore/__tests__/route.test.ts index dbbc12592..dcd8a0f15 100644 --- a/src/app/api/measurements/restore/__tests__/route.test.ts +++ b/src/app/api/measurements/restore/__tests__/route.test.ts @@ -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 () => {