Files
HealthLog/tests/integration/coach-budget-owner.test.ts
T
Marc-André Bombeck bcef5aeeee fix(ai): hold a reserve so the chat is never locked out by jobs
The background ceiling applied only when the operator funded the chain, so on a
chain the operator does not pay for the automatic generators and the chat shared
one undivided ceiling and the first could still take all of it. That is the
configuration the recommended remedy for the original lockout produces — put the
personal Codex account first — so the guarantee was missing on exactly the setup
it was written for.

The ceiling is now expressed as what it is meant to protect. Background work may
run until the day's spend would leave less than a fixed reserve for whoever is
waiting on an answer, on both cost owners. A reserve and a quota look alike at
the boundary and mean opposite things in the middle: a quota stops useful
generation on a day nobody was ever going to interrupt, while a reserve costs it
nothing until the moment it would take the last of the day. An account
generating 1.45 million tokens of insights on its own plan keeps every one of
them and the chat still holds 400 000.

INTERACTIVE_RESERVE_SHARE replaces JOB_SURFACE_SHARE. The old name described a
quota over the jobs rather than a floor under the person waiting, and a name
that states the opposite of the intent is what put the wrong comparison in this
ledger twice already.

The fraction is a default an operator will want to change. The setting and its
documentation are separate work.
2026-09-11 12:55:52 +02:00

439 lines
13 KiB
TypeScript

/**
* v1.38.19 — the daily coach ledger attributes tokens to the COST
* OWNER of the hop that actually served them.
*
* Production evidence (operator instance, 2026-09-11): the chat refused with
* `coach.budget.exceeded` at `totalAfter: 513539` although almost every token
* of that day had been served by `codex` — the operator's own ChatGPT plan —
* after the operator-funded `admin-openai` primary answered HTTP 500. The
* ledger had no provider dimension, so tokens the USER's plan paid for were
* counted against the operator-key cap and the interactive chat was locked out
* by background jobs every morning.
*
* These contracts need real Postgres: the attribution lives inside the same
* atomic statements as the total (`INSERT … ON CONFLICT DO UPDATE … RETURNING`
* and the reconcile `UPDATE`), and a mocked client cannot prove that the two
* counters move together under the row lock.
*/
import { beforeEach, describe, expect, it } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
async function seedUser(): Promise<string> {
const user = await getPrismaClient().user.create({
data: {
username: "budget-owner-user",
email: "budget-owner@example.test",
role: "USER",
},
});
return user.id;
}
async function readRow(userId: string, dateKey: string) {
return getPrismaClient().coachUsage.findUnique({
where: { userId_dateKey: { userId, dateKey } },
});
}
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
});
describe("coach_usage.operator_tokens (real Postgres)", () => {
it("bills an operator-served turn to both counters", async () => {
const { reserveBudget, reconcileSpend, OPERATOR_COST_CAP } =
await import("@/lib/ai/coach/budget");
const userId = await seedUser();
const dateKey = "2026-09-11";
const res = await reserveBudget(
userId,
1_000,
dateKey,
OPERATOR_COST_CAP,
"operator",
"coach",
);
expect(res.allowed).toBe(true);
await reconcileSpend(userId, res.reserved, 3_000, dateKey, 0, {
servedBy: "admin-openai",
reservedOwner: "operator",
});
const row = await readRow(userId, dateKey);
expect(row?.totalTokens).toBe(3_000);
expect(row?.operatorTokens).toBe(3_000);
});
it("moves the reservation out of operator_tokens when the user's own plan served", async () => {
const { reserveBudget, reconcileSpend, OPERATOR_COST_CAP } =
await import("@/lib/ai/coach/budget");
const userId = await seedUser();
const dateKey = "2026-09-11";
// The chain's primary is operator-funded, so the reservation is charged to
// the operator — but the `admin-openai` hop failed and `codex` (the user's
// own ChatGPT plan) answered. The operator pays nothing for that turn.
const res = await reserveBudget(
userId,
1_000,
dateKey,
OPERATOR_COST_CAP,
"operator",
"coach",
);
await reconcileSpend(userId, res.reserved, 3_000, dateKey, 0, {
servedBy: "codex",
reservedOwner: "operator",
});
const row = await readRow(userId, dateKey);
expect(row?.totalTokens).toBe(3_000);
expect(row?.operatorTokens).toBe(0);
});
it("a user-plan turn never touches an existing operator balance", async () => {
const { reserveBudget, reconcileSpend, USER_PLAN_CAP } =
await import("@/lib/ai/coach/budget");
const userId = await seedUser();
const dateKey = "2026-09-11";
// An operator-funded job already spent 150k earlier that day.
await getPrismaClient().coachUsage.create({
data: {
userId,
dateKey,
totalTokens: 150_000,
operatorTokens: 150_000,
messageCount: 12,
},
});
const res = await reserveBudget(
userId,
1_000,
dateKey,
USER_PLAN_CAP,
"user",
"coach",
);
await reconcileSpend(userId, res.reserved, 4_000, dateKey, 0, {
servedBy: "codex",
reservedOwner: "user",
});
const row = await readRow(userId, dateKey);
expect(row?.totalTokens).toBe(154_000);
// The operator's balance is untouched: a user-plan turn must neither add
// to it nor deplete it.
expect(row?.operatorTokens).toBe(150_000);
});
it("bills an operator-funded FALLBACK hop under a user-plan reservation", async () => {
const { reserveBudget, reconcileSpend, USER_PLAN_CAP } =
await import("@/lib/ai/coach/budget");
const userId = await seedUser();
const dateKey = "2026-09-11";
// Primary is the user's own key (user-plan reservation), but the chain
// fell back onto the operator's shared key — those tokens land on the
// operator's invoice and must be counted there.
const res = await reserveBudget(
userId,
1_000,
dateKey,
USER_PLAN_CAP,
"user",
"coach",
);
await reconcileSpend(userId, res.reserved, 2_500, dateKey, 0, {
servedBy: "admin-openai",
reservedOwner: "user",
});
const row = await readRow(userId, dateKey);
expect(row?.totalTokens).toBe(2_500);
expect(row?.operatorTokens).toBe(2_500);
});
it("admits the operator's chat on a day his own plan filled", async () => {
const { reserveBudget, OPERATOR_COST_CAP } =
await import("@/lib/ai/coach/budget");
const userId = await seedUser();
const dateKey = "2026-09-11";
// The operator's 06:42Z lockout, reproduced: 1.2 M tokens on the day, of
// which only 150 k were served by the operator-funded key — the rest by
// `codex` on his own ChatGPT plan.
await getPrismaClient().coachUsage.create({
data: {
userId,
dateKey,
totalTokens: 1_200_000,
operatorTokens: 150_000,
messageCount: 240,
},
});
const res = await reserveBudget(
userId,
3_000,
dateKey,
OPERATOR_COST_CAP,
"operator",
"coach",
);
expect(res.allowed).toBe(true);
expect(res.operatorAfter).toBe(153_000);
});
it("refuses once the operator-funded share reaches the operator cap", async () => {
const { reserveBudget, OPERATOR_COST_CAP } =
await import("@/lib/ai/coach/budget");
const userId = await seedUser();
const dateKey = "2026-09-11";
await getPrismaClient().coachUsage.create({
data: {
userId,
dateKey,
totalTokens: 1_200_000,
operatorTokens: OPERATOR_COST_CAP,
messageCount: 240,
},
});
const res = await reserveBudget(
userId,
3_000,
dateKey,
OPERATOR_COST_CAP,
"operator",
"coach",
);
expect(res.allowed).toBe(false);
const row = await readRow(userId, dateKey);
expect(row?.totalTokens).toBe(1_200_000);
expect(row?.operatorTokens).toBe(OPERATOR_COST_CAP);
});
// v1.38.19 — the incident, end to end.
//
// The 06:42Z row verbatim: 1.2 M tokens on the day, none of them the
// operator's, on the chain `[admin-openai, codex, openai-compatible]`. The
// chat must open. It was refused while the ledger had no owner dimension and
// counted every token against the operator's ceiling; this pins that the
// refusal cannot come back.
it("does not refuse the chat for a day of background spend on the user's own plan", async () => {
const { reserveBudget, resolveDailyCap, resolveCostOwner } =
await import("@/lib/ai/coach/budget");
const userId = await seedUser();
const dateKey = "2026-09-11";
const chain = [
{ providerType: "admin-openai" as const },
{ providerType: "codex" as const },
{ providerType: "openai-compatible" as const },
];
await getPrismaClient().coachUsage.create({
data: {
userId,
dateKey,
totalTokens: 1_200_000,
operatorTokens: 0,
messageCount: 240,
},
});
const chat = await reserveBudget(
userId,
3_000,
dateKey,
resolveDailyCap(chain),
resolveCostOwner(chain),
"coach",
);
expect(chat.allowed).toBe(true);
expect(chat.limit).toBeNull();
});
it("refuses the BACKGROUND generation on that same day", async () => {
// The other half of the promise: the interactive day is protected, and the
// automatic work that filled the row is the thing that stops. The operator
// counter is empty, so only the abuse ceiling on the total can say no —
// which is the ceiling the first cut dropped for operator chains.
const { reserveBudget, resolveDailyCapFor, resolveCostOwner } =
await import("@/lib/ai/coach/budget");
const userId = await seedUser();
const dateKey = "2026-09-11";
const chain = [
{ providerType: "admin-openai" as const },
{ providerType: "codex" as const },
];
await getPrismaClient().coachUsage.create({
data: {
userId,
dateKey,
totalTokens: 1_900_000,
operatorTokens: 0,
messageCount: 380,
},
});
const job = await reserveBudget(
userId,
3_000,
dateKey,
resolveDailyCapFor("job", chain),
resolveCostOwner(chain),
"job",
);
expect(job.allowed).toBe(false);
expect(job.limit).toBe("total-cap");
// Refused means refunded: the row is exactly as it was.
const row = await readRow(userId, dateKey);
expect(row?.totalTokens).toBe(1_900_000);
expect(row?.operatorTokens).toBe(0);
expect(row?.messageCount).toBe(380);
// ...and the person waiting still gets their turn.
const chat = await reserveBudget(
userId,
3_000,
dateKey,
200_000,
"operator",
"coach",
);
expect(chat.allowed).toBe(true);
});
// v1.38.20 — the reserve holds on a chain nobody is billed for.
//
// This is the configuration the remedy for the original lockout produces:
// put the personal Codex account first and the operator stops paying for his
// own egress. The day then runs on `USER_PLAN_CAP` with no invoice behind it,
// so nothing rations the automatic generators — and without a reserve the
// chat is locked out by background work all over again, on the very chain
// that was supposed to fix it.
it("admits the chat on a user-funded chain whose background work filled the day", async () => {
const {
reserveBudget,
resolveDailyCapFor,
resolveDailyCap,
resolveCostOwner,
} = await import("@/lib/ai/coach/budget");
const userId = await seedUser();
const dateKey = "2026-09-11";
const chain = [
{ providerType: "codex" as const },
{ providerType: "admin-openai" as const },
];
// 1.6 M tokens of insights on the user's own ChatGPT plan — everything the
// background surfaces are allowed to reach, and not a token of it billed.
await getPrismaClient().coachUsage.create({
data: {
userId,
dateKey,
totalTokens: 1_600_000,
operatorTokens: 0,
messageCount: 320,
},
});
const job = await reserveBudget(
userId,
3_000,
dateKey,
resolveDailyCapFor("job", chain),
resolveCostOwner(chain),
"job",
);
expect(job.allowed).toBe(false);
// Refused means refunded: the reserve is untouched by the attempt.
const afterJob = await readRow(userId, dateKey);
expect(afterJob?.totalTokens).toBe(1_600_000);
expect(afterJob?.messageCount).toBe(320);
const chat = await reserveBudget(
userId,
3_000,
dateKey,
resolveDailyCap(chain),
resolveCostOwner(chain),
"coach",
);
expect(chat.allowed).toBe(true);
expect(chat.limit).toBeNull();
});
it("lets that same background work run right up to the reserve", async () => {
// The reserve is a floor under the chat, not a quota over the jobs: a day
// of automatic generation on the user's own plan costs nobody anything and
// is not cut short until it would take the last of the day.
const { reserveBudget, resolveDailyCapFor, resolveCostOwner } =
await import("@/lib/ai/coach/budget");
const userId = await seedUser();
const dateKey = "2026-09-11";
const chain = [{ providerType: "codex" as const }];
await getPrismaClient().coachUsage.create({
data: {
userId,
dateKey,
totalTokens: 1_599_999,
operatorTokens: 0,
messageCount: 319,
},
});
const job = await reserveBudget(
userId,
3_000,
dateKey,
resolveDailyCapFor("job", chain),
resolveCostOwner(chain),
"job",
);
expect(job.allowed).toBe(true);
});
it("refunds both counters when a refused reservation is rolled back", async () => {
const { reserveBudget, OPERATOR_COST_CAP } =
await import("@/lib/ai/coach/budget");
const userId = await seedUser();
const dateKey = "2026-09-11";
await getPrismaClient().coachUsage.create({
data: {
userId,
dateKey,
totalTokens: OPERATOR_COST_CAP,
operatorTokens: OPERATOR_COST_CAP,
messageCount: 3,
},
});
const res = await reserveBudget(
userId,
1_000,
dateKey,
OPERATOR_COST_CAP,
"operator",
"coach",
);
expect(res.allowed).toBe(false);
const row = await readRow(userId, dateKey);
expect(row?.totalTokens).toBe(OPERATOR_COST_CAP);
expect(row?.operatorTokens).toBe(OPERATOR_COST_CAP);
expect(row?.messageCount).toBe(3);
});
});