Files
oh-my-pi/packages/ai/test/usage-report-notes-schema.test.ts
T
oldschoola 6c3f35dfef fix(usage): dedup provider-wide notes and add report-level notes field
Provider-wide disclaimers (e.g. OpenCode Go's "OMP-observed spend
only") were duplicated onto every UsageLimit, then repeated N times
in the TUI aggregate renderer (once per account × window). With
2 accounts × 3 windows, the same disclaimer appeared 6 times
bullet-joined.

Structural fix:
- Add notes?: string[] to UsageReport (interface + both schema
  copies: usage.ts and auth-broker/wire-schemas.ts) so the field
  survives the broker client's "+": "reject" deserialization gate.
- Move opencode-go's disclaimer from per-limit notes to
  provider-level notes.

Defensive fix:
- Dedup identical per-limit notes in the TUI aggregate renderer
  (command-controller.ts) via [...new Set(...)].
- Render provider-level notes once above per-account sections in
  all three rendering paths: TUI (command-controller), CLI
  (usage-cli), and ACP (usage-report helper).

Regression tests:
- usage-cli.test.ts: provider-level notes render once, not
  duplicated per account or limit; positioned above per-account rows.
- usage-report-notes-schema.test.ts: wire-schema round-trip proving
  notes survives usageResponseSchema validation.

Fixes #3268
2026-06-23 16:02:25 -07:00

58 lines
2.2 KiB
TypeScript

/**
* Regression for #3268: provider-level `notes` on `UsageReport` must survive
* the broker wire schema. The broker client validates `/v1/usage` responses
* against `usageResponseSchema`, which uses `"+": "reject"` — unknown fields
* at the envelope level are rejected, not silently stripped. Both the
* `usage.ts` schema and the `auth-broker/wire-schemas.ts` copy must declare
* `notes?: string[]` at the report level, or the field is lost on
* deserialization. `usageReportSchema` (the non-broker copy) must also accept
* the field so local `AuthStorage.fetchUsageReports` results type-check.
*/
import { describe, expect, it } from "bun:test";
import { usageReportSchema } from "@oh-my-pi/pi-ai";
import { usageResponseSchema } from "@oh-my-pi/pi-ai/auth-broker/wire-schemas";
import { type } from "arktype";
const DISCLAIMER = "OMP-observed spend only; OpenCode usage outside OMP is not included.";
function reportWithNotes() {
return {
provider: "opencode-go",
fetchedAt: Date.now(),
limits: [
{
id: "rolling-5h",
label: "5 Hour limit",
scope: { provider: "opencode-go", windowId: "rolling-5h" },
window: { id: "rolling-5h", label: "5 Hour", durationMs: 5 * 3_600_000 },
amount: { used: 3, limit: 12, remaining: 9, usedFraction: 0.25, remainingFraction: 0.75, unit: "usd" },
status: "ok",
},
],
notes: [DISCLAIMER],
metadata: { planType: "OpenCode Go" },
};
}
describe("usage report notes wire schema", () => {
it("usageReportSchema accepts report-level notes and preserves them", () => {
const validated = usageReportSchema(reportWithNotes());
expect(validated).not.toBeInstanceOf(type.errors);
expect(validated).toHaveProperty("notes", [DISCLAIMER]);
});
it("usageResponseSchema preserves report-level notes through the broker reject gate", () => {
const response = {
generatedAt: Date.now(),
reports: [reportWithNotes()],
};
const validated = usageResponseSchema(response);
expect(validated).not.toBeInstanceOf(type.errors);
expect(validated).toHaveProperty("reports");
if (validated instanceof type.errors) throw new Error("expected valid response");
const reports = validated.reports;
expect(reports[0]).toHaveProperty("notes", [DISCLAIMER]);
});
});