Add offline monthly insights with Ollama

This commit is contained in:
2026-03-23 14:12:35 -04:00
parent 696d393fca
commit a745c0ca1e
13 changed files with 415 additions and 60 deletions

91
src/lib/insights.test.ts Normal file
View File

@@ -0,0 +1,91 @@
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() },
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([]);
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.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.");
});
});