import { afterEach, describe, expect, it, vi } from "vitest"; vi.mock("@/lib/db", () => { const monthlyInsight = { upsert: vi.fn(async ({ where, update, create }: { where: { month: string }; update: Record; create: Record }) => ({ id: "insight-1", month: where.month, year: (update.year ?? create.year) as number, summary: (update.summary ?? create.summary) as string, recommendations: (update.recommendations ?? create.recommendations) as string, inputSnapshot: (update.inputSnapshot ?? create.inputSnapshot) as string, generatedAt: new Date("2026-03-23T12:00:00.000Z"), })), findUnique: vi.fn(), }; return { db: { expense: { findMany: vi.fn() }, paycheck: { findMany: vi.fn() }, recurringExpense: { findMany: vi.fn().mockResolvedValue([]) }, paySchedule: { findFirst: vi.fn().mockResolvedValue(null) }, monthlyInsight, }, }; }); describe("generateMonthlyInsight", () => { afterEach(() => { vi.restoreAllMocks(); }); it("stores a fallback for sparse months", async () => { const { db } = await import("@/lib/db"); const { generateMonthlyInsight } = await import("@/lib/insights"); vi.mocked(db.expense.findMany).mockResolvedValue([]); vi.mocked(db.paycheck.findMany).mockResolvedValue([]); vi.mocked(db.recurringExpense.findMany).mockResolvedValue([]); const result = await generateMonthlyInsight("2026-03"); expect(result.source).toBe("fallback"); expect(result.insight.summary).toContain("not enough activity"); }); it("stores model output when Ollama responds", async () => { const { db } = await import("@/lib/db"); const { generateMonthlyInsight } = await import("@/lib/insights"); vi.mocked(db.expense.findMany).mockResolvedValue([ { id: "expense-1", title: "Groceries", date: "2026-03-23", amountCents: 3200, category: "FOOD", createdAt: new Date("2026-03-23T10:00:00.000Z"), }, { id: "expense-2", title: "Rent", date: "2026-03-02", amountCents: 120000, category: "RENT", createdAt: new Date("2026-03-02T10:00:00.000Z"), }, ]); vi.mocked(db.paycheck.findMany).mockResolvedValue([ { id: "paycheck-1", payDate: "2026-03-01", amountCents: 180000, createdAt: new Date("2026-03-01T10:00:00.000Z"), }, ]); vi.mocked(db.recurringExpense.findMany).mockResolvedValue([]); vi.spyOn(globalThis, "fetch").mockResolvedValue({ ok: true, json: async () => ({ response: JSON.stringify({ summary: "Spending is stable.", recommendations: "Keep food spending under watch.", }), }), } as Response); const result = await generateMonthlyInsight("2026-03"); expect(result.source).toBe("model"); expect(result.insight.summary).toBe("Spending is stable."); expect(result.insight.recommendations).toBe('["Keep food spending under watch."]'); }); it("coerces array recommendations from the local model", async () => { const { db } = await import("@/lib/db"); const { generateMonthlyInsight } = await import("@/lib/insights"); vi.mocked(db.expense.findMany).mockResolvedValue([ { id: "expense-1", title: "Groceries", date: "2026-03-23", amountCents: 3200, category: "FOOD", createdAt: new Date("2026-03-23T10:00:00.000Z"), }, { id: "expense-2", title: "Rent", date: "2026-03-02", amountCents: 120000, category: "RENT", createdAt: new Date("2026-03-02T10:00:00.000Z"), }, ]); vi.mocked(db.paycheck.findMany).mockResolvedValue([ { id: "paycheck-1", payDate: "2026-03-01", amountCents: 180000, createdAt: new Date("2026-03-01T10:00:00.000Z"), }, ]); vi.mocked(db.recurringExpense.findMany).mockResolvedValue([]); vi.spyOn(globalThis, "fetch").mockResolvedValue({ ok: true, json: async () => ({ response: JSON.stringify({ summary: "Spending remains manageable.", recommendations: ["Keep groceries planned.", "Move surplus to savings."], }), }), } as Response); const result = await generateMonthlyInsight("2026-03"); expect(result.insight.recommendations).toContain("Keep groceries planned."); expect(result.insight.recommendations).toContain("Move surplus to savings."); }); });