mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
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:
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(
|
||||
|
||||
Reference in new issue
Block a user