Files
monthlytracker/src/lib/insights.test.ts

144 lines
4.5 KiB
TypeScript

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<string, unknown>; create: Record<string, unknown> }) => ({
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.");
});
});