Cover the briefing tier's ledger accounting

Pin the four guarantees at the new chokepoint and at both call sites: spend
lands on the ledger, over-cap is refused before any egress, a BYOK chain is
measured against the user-plan ceiling rather than the operator's, and a
failed call refunds in full. Also pin the ordering the consent gate depends
on — a consent-blocked user is never charged.
This commit is contained in:
Marc-André Bombeck committed 2026-07-19 05:15:30 +02:00
1 parent 8a1d7502d0
commit 79922631ce
3 files changed
+492

No files matched your search

@@ -173,6 +173,11 @@ beforeEach(() => {
// need a fresh cache so the previous test's success doesn't reorder
// the chain on the next request.
clearLastWorkingProviderCache();
// `clearAllMocks` does not undo a `mockImplementation`, so the ledger stub is
// re-seeded per test — otherwise the over-cap case below leaks into the rest.
vi.mocked(prisma.$queryRaw).mockImplementation((async () => [
{ total_tokens: 0 },
]) as never);
vi.mocked(checkRateLimit).mockResolvedValue({
allowed: true,
remaining: 9,
@@ -228,6 +233,33 @@ function makeProviderThatThrows(
} as unknown as Awaited<ReturnType<typeof resolveProvider>>);
}
describe("POST /api/insights/generate — daily token ceiling", () => {
it("refuses over-cap with 429 before any provider egress", async () => {
// The reservation reports a prior total already at the ceiling. The hourly
// rate limit bounds how OFTEN this route runs; the ledger bounds what it
// costs. Before this wiring the route had only the former, so ten
// full-price briefings an hour was the real ceiling.
// Targeted at the ledger's upsert only — other raw reads on this path
// (the plateau / derived-signal probes) must keep their own shape.
vi.mocked(prisma.$queryRaw).mockImplementation((async (
strings: TemplateStringsArray,
) =>
String(strings[0]).includes("coach_usage")
? [{ total_tokens: 10_000_000 }]
: [{ total_tokens: 0 }]) as never);
makeWorkingProvider();
const res = await POST(jsonRequest({ force: true }) as never);
expect(res.status).toBe(429);
const body = (await res.json()) as ApiErrorEnvelope;
expect(body.error).toContain("Daily AI token budget");
// Not a provider failure: no cache row written, so the last good briefing
// stays intact and the card does not blank.
expect(prisma.user.update).not.toHaveBeenCalled();
expect(enqueueStatusRefillForUser).not.toHaveBeenCalled();
});
});
describe("POST /api/insights/generate — provider error mapping", () => {
it("rejects a body over the 16 KB cap with 413 before parsing", async () => {
const res = await POST(