fix(ui): let key budget notes wrap, and stop encoding a per-scope operator

Regenerates schema.d.ts for the end-user docstring change, which the types sync
gate needs.

Notes now carry two clauses, e.g. "alert only, never blocks; compared against
recorded spend rather than the live counter", and the scope cell truncated them
to one clipped line behind a native tooltip. They are the most load-bearing text
in the table, so they wrap now and the column is wider to suit.

The threshold helper always read `comparison` per row, but its doc comment and
the fixtures around it asserted that team enforces ">", which is no longer true:
reservation tightens team, tag and end_user to ">=", and disabling reservation
relaxes them back. Nothing about the rendering changes, but the tests now pin
that one scope can render either operator instead of teaching a wrong default.
This commit is contained in:
ryan-crabbe-berri 2026-08-19 17:26:28 -07:00
parent 2c608b05a6
commit 7e6546e8cc
4 changed files with 30 additions and 22 deletions

View file

@ -30,9 +30,9 @@ const TEAM_MEMBER_AT_LIMIT: KeyBudgetEntry = {
status: "exceeded",
};
const TEAM_UNDER_LIMIT: KeyBudgetEntry = {
const PROJECT_UNDER_LIMIT: KeyBudgetEntry = {
...UNCONFIGURED_BUDGET,
scope: "team",
scope: "project",
comparison: ">",
max_budget: 300,
spend: 120,
@ -40,8 +40,11 @@ const TEAM_UNDER_LIMIT: KeyBudgetEntry = {
status: "ok",
};
const INCLUSIVE_AT_300: KeyBudgetEntry = {
// team is ">=" while budget reservation is on and ">" once an operator disables it, so the same
// scope must render either operator off the response rather than a value baked in per scope.
const RESERVED_TEAM_AT_300: KeyBudgetEntry = {
...UNCONFIGURED_BUDGET,
scope: "team",
comparison: ">=",
max_budget: 300,
spend: 300,
@ -49,7 +52,7 @@ const INCLUSIVE_AT_300: KeyBudgetEntry = {
status: "exceeded",
};
const EXCLUSIVE_AT_300: KeyBudgetEntry = { ...INCLUSIVE_AT_300, comparison: ">", status: "ok" };
const UNRESERVED_TEAM_AT_300: KeyBudgetEntry = { ...RESERVED_TEAM_AT_300, comparison: ">", status: "ok" };
const SOFT_OVER: KeyBudgetEntry = {
...UNCONFIGURED_BUDGET,
@ -73,12 +76,12 @@ describe("budgetThresholdRule", () => {
});
it("marks an exclusive scope as blocking only above the limit", () => {
expect(budgetThresholdRule(TEAM_UNDER_LIMIT)).toBe("Blocks at > $300.00");
expect(budgetThresholdRule(PROJECT_UNDER_LIMIT)).toBe("Blocks at > $300.00");
});
it("distinguishes two scopes sitting on identical numbers by their operator alone", () => {
expect(budgetThresholdRule(INCLUSIVE_AT_300)).toBe("Blocks at ≥ $300.00");
expect(budgetThresholdRule(EXCLUSIVE_AT_300)).toBe("Blocks at > $300.00");
it("reads the operator off each response, so one scope can render either threshold", () => {
expect(budgetThresholdRule(RESERVED_TEAM_AT_300)).toBe("Blocks at ≥ $300.00");
expect(budgetThresholdRule(UNRESERVED_TEAM_AT_300)).toBe("Blocks at > $300.00");
});
it("never promises a soft budget will block", () => {

View file

@ -41,9 +41,10 @@ export const severityRank = (entry: KeyBudgetEntry): number =>
const COMPARISON_GLYPH: Record<string, string> = { ">=": "≥", ">": ">" };
/**
* Scopes disagree on whether hitting the limit exactly is over it: team_member enforces `>=`
* so 50 of 50 is already denied, while team enforces `>` so 300 of 300 still passes. Two rows
* can therefore show identical numbers and opposite statuses, so state each row's real threshold.
* Scopes disagree on whether hitting the limit exactly is already over it, and a scope's operator
* is not fixed: budget reservation tightens some scopes to `>=`, and disabling it relaxes them back
* to `>`. So this reads `comparison` off each row rather than assuming a constant per scope. Two
* rows can show identical numbers and opposite statuses, so state the threshold each one enforces.
*/
export const budgetThresholdRule = (entry: KeyBudgetEntry): string | null => {
if (entry.max_budget == null) return null;
@ -71,12 +72,12 @@ function ScopeCell({ entry }: { entry: KeyBudgetEntry }) {
</span>
}
/>
{entity && <span className="truncate font-mono text-xs text-muted-foreground">{entity}</span>}
{entry.note && (
<span className="truncate text-xs text-muted-foreground italic" title={entry.note}>
{entry.note}
{entity && (
<span className="truncate font-mono text-xs text-muted-foreground" title={entity}>
{entity}
</span>
)}
{entry.note && <span className="text-xs text-muted-foreground italic">{entry.note}</span>}
</div>
);
}
@ -110,7 +111,7 @@ export const getKeyBudgetsTableColumns = (): ColumnDef<KeyBudgetEntry>[] => [
id: "scope",
meta: { title: "Scope" },
header: "Scope",
size: 240,
size: 300,
enableSorting: false,
cell: ({ row }) => <ScopeCell entry={row.original} />,
},

View file

@ -162,7 +162,7 @@ const TEAM_SOFT_OVER: KeyBudgetEntry = {
remaining: -400,
source: "budget_table:b-soft",
status: "exceeded",
note: "alert only",
note: "alert only, never blocks; compared against recorded spend rather than the live counter",
};
const TEAM_MEMBER_BLOCKING: KeyBudgetEntry = {
@ -268,7 +268,9 @@ describe("KeyInfoView Budgets tab", () => {
expect(within(softRow).getByText("Exceeded (alert only)")).toBeInTheDocument();
expect(within(softRow).queryByTestId("key-budget-blocking")).not.toBeInTheDocument();
expect(within(softRow).queryByText("Blocks requests")).not.toBeInTheDocument();
expect(softRow).toHaveTextContent("alert only");
expect(softRow).toHaveTextContent(
"alert only, never blocks; compared against recorded spend rather than the live counter",
);
});
it("explains why two rows on identical numbers get opposite statuses", async () => {
@ -285,8 +287,8 @@ describe("KeyInfoView Budgets tab", () => {
};
const exclusive: KeyBudgetEntry = {
...UNCONFIGURED_BUDGET,
scope: "team",
entity_type: "team",
scope: "project",
entity_type: "project",
entity_label: "Platform",
comparison: ">",
max_budget: 300,

View file

@ -6822,7 +6822,8 @@ export interface paths {
* hash. Defaults to the key in the Authorization header when omitted (`GET /key/budgets`).
* - end_user_id: str | None (query parameter) - Also report the budgets that would apply to this
* end user. Omitted end users produce no `end_user` rows, because nothing binds an end user to
* a key outside a request.
* a key outside a request. Proxy admins only, since end users are a proxy-global namespace with
* no key, team or organization scoping to check a caller against.
*
* Returns:
* - key: str - The key that was looked up, echoed back as it was passed in
@ -7512,7 +7513,8 @@ export interface paths {
* hash. Defaults to the key in the Authorization header when omitted (`GET /key/budgets`).
* - end_user_id: str | None (query parameter) - Also report the budgets that would apply to this
* end user. Omitted end users produce no `end_user` rows, because nothing binds an end user to
* a key outside a request.
* a key outside a request. Proxy admins only, since end users are a proxy-global namespace with
* no key, team or organization scoping to check a caller against.
*
* Returns:
* - key: str - The key that was looked up, echoed back as it was passed in