From 985d9b72273f5f6a274378b11eac84c4f429f537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Br=C3=A4mer?= <22003767+robinbraemer@users.noreply.github.com> Date: Mon, 14 Sep 2026 11:41:31 +0200 Subject: [PATCH] fix(contract): sync quota lifetime response Rationale: The generated QuotaInfo response schema rejected Production's valid lifetime grant entries after the server-side quota snapshot fix, turning HTTP 200 quota reads into client contract errors. The canonical snapshot now matches the producer's scope and lifetime-grant fields.\n\nRejected: A raw-response fallback or permissive decoder would hide producer/consumer drift and discard the metric-scope identity.\n\nTested: bun run test -- test/generated-command.test.ts --testNamePattern='quotas.list accepts a lifetime quota response'; mise run generate:check; mise run check; bun run test; jq QuotaInfo producer/CLI equality check; independent review (no blockers). --- openapi/public.json | 30 ++++++++++++++++++++++++++---- src/generated/openapi-api.gen.ts | 8 ++++---- test/generated-command.test.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/openapi/public.json b/openapi/public.json index a279e9c..84b155f 100644 --- a/openapi/public.json +++ b/openapi/public.json @@ -22515,18 +22515,29 @@ "metric": { "type": "string", "example": "compute/machines", - "description": "Metric: service/resource" + "description": "Metric: service/resource. Not unique on its own — see the `scope` field. Index by `(metric, scope)`." }, "type": { "type": "string", "enum": [ "allocation", "rate", - "concurrency" + "concurrency", + "lifetime" ], - "description": "Allocation = max owned, rate = max per time window, concurrency = max in-flight.", + "description": "Allocation = max owned, rate = max per time window, concurrency = max in-flight, lifetime = a one-time grant. Only `rate` has a `resets_at`: a lifetime entitlement has no period, is never released by deleting the resource it funded, and reports its position in that one-way lifecycle as `grant_state`.", "example": "allocation" }, + "scope": { + "type": "string", + "enum": [ + "workspace", + "principal", + "cluster" + ], + "description": "Identity the limit/usage numbers are bound to, and the second half of an entry's key. `workspace` is occupancy inside the workspace; `principal` aggregates across every workspace and organization owned by the same authenticated identity, so a new workspace does not grant fresh free capacity; `cluster` bounds in-flight work for one cluster. A metric enforced against more than one identity — every free-tier allocation, for example — returns one entry per scope, so `(metric, scope)` is the composite key. Indexing a listing by `metric` alone silently drops entries.", + "example": "workspace" + }, "display_name": { "type": "string", "example": "Compute machines" @@ -22560,12 +22571,23 @@ "resets_at": { "type": "integer", "minimum": 0, - "description": "For rate quotas: Unix timestamp (seconds) reset time" + "description": "For rate quotas only: Unix timestamp (seconds) at which the window resets. Absent on every other type, including `lifetime`, which has no period." + }, + "grant_state": { + "type": "string", + "enum": [ + "available", + "consumed", + "revoked" + ], + "description": "For `lifetime` quotas only: state of the one-time grant.", + "example": "available" } }, "required": [ "metric", "type", + "scope", "display_name", "unit", "limit", diff --git a/src/generated/openapi-api.gen.ts b/src/generated/openapi-api.gen.ts index 002a53e..010270f 100644 --- a/src/generated/openapi-api.gen.ts +++ b/src/generated/openapi-api.gen.ts @@ -275,10 +275,10 @@ export type NotificationList = { readonly "data": ReadonlyArray<{ readonly "id": export const NotificationList = Schema.Struct({ "data": Schema.Array(Schema.Struct({ "id": Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxLength(54).annotate({ "expected": "a value with a length of at most 54" })), "type": Schema.String, "resource_type": Schema.String, "resource_id": Schema.String, "title": Schema.Union([Schema.String, Schema.Null]), "body": Schema.Union([Schema.String, Schema.Null]), "severity": Schema.Union([Schema.Literal("info"), Schema.Literal("warning"), Schema.Literal("critical"), Schema.Null]).annotate({ "description": "Severity level surfaced to UI indicators." }), "context": Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]), "read_at": Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" })), Schema.Null]).annotate({ "description": "Unix timestamp (seconds) when read, null if unread" }), "created_at": Schema.Number.annotate({ "description": "Unix timestamp (seconds)" }).check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" })) })), "has_more": Schema.Boolean.annotate({ "description": "True if more items exist after this page." }), "next_cursor": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Opaque cursor for the next page, or null if no more pages." }) }).annotate({ "identifier": "NotificationList" }) export type UnreadCount = { readonly "count": number } export const UnreadCount = Schema.Struct({ "count": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })) }).annotate({ "identifier": "UnreadCount" }) -export type QuotaInfoList = { readonly "data": ReadonlyArray<{ readonly "metric": string, readonly "type": "allocation" | "rate" | "concurrency", readonly "display_name": string, readonly "unit": "count" | "cores" | "bytes" | "seconds" | "count/day" | "count/minute" | "cents/day", readonly "limit": number, readonly "usage": number, readonly "remaining": number, readonly "resets_at"?: number }>, readonly "has_more": boolean, readonly "next_cursor": string | null } -export const QuotaInfoList = Schema.Struct({ "data": Schema.Array(Schema.Struct({ "metric": Schema.String.annotate({ "description": "Metric: service/resource" }), "type": Schema.Literals(["allocation", "rate", "concurrency"]).annotate({ "description": "Allocation = max owned, rate = max per time window, concurrency = max in-flight." }), "display_name": Schema.String, "unit": Schema.Literals(["count", "cores", "bytes", "seconds", "count/day", "count/minute", "cents/day"]).annotate({ "description": "Unit the limit/usage numbers are measured in." }), "limit": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "usage": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "remaining": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "resets_at": Schema.optionalKey(Schema.Number.annotate({ "description": "For rate quotas: Unix timestamp (seconds) reset time" }).check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }))) })), "has_more": Schema.Boolean.annotate({ "description": "True if more items exist after this page." }), "next_cursor": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Opaque cursor for the next page, or null if no more pages." }) }).annotate({ "identifier": "QuotaInfoList" }) -export type QuotaInfo = { readonly "metric": string, readonly "type": "allocation" | "rate" | "concurrency", readonly "display_name": string, readonly "unit": "count" | "cores" | "bytes" | "seconds" | "count/day" | "count/minute" | "cents/day", readonly "limit": number, readonly "usage": number, readonly "remaining": number, readonly "resets_at"?: number } -export const QuotaInfo = Schema.Struct({ "metric": Schema.String.annotate({ "description": "Metric: service/resource" }), "type": Schema.Literals(["allocation", "rate", "concurrency"]).annotate({ "description": "Allocation = max owned, rate = max per time window, concurrency = max in-flight." }), "display_name": Schema.String, "unit": Schema.Literals(["count", "cores", "bytes", "seconds", "count/day", "count/minute", "cents/day"]).annotate({ "description": "Unit the limit/usage numbers are measured in." }), "limit": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "usage": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "remaining": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "resets_at": Schema.optionalKey(Schema.Number.annotate({ "description": "For rate quotas: Unix timestamp (seconds) reset time" }).check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }))) }).annotate({ "identifier": "QuotaInfo" }) +export type QuotaInfoList = { readonly "data": ReadonlyArray<{ readonly "metric": string, readonly "type": "allocation" | "rate" | "concurrency" | "lifetime", readonly "scope": "workspace" | "principal" | "cluster", readonly "display_name": string, readonly "unit": "count" | "cores" | "bytes" | "seconds" | "count/day" | "count/minute" | "cents/day", readonly "limit": number, readonly "usage": number, readonly "remaining": number, readonly "resets_at"?: number, readonly "grant_state"?: "available" | "consumed" | "revoked" }>, readonly "has_more": boolean, readonly "next_cursor": string | null } +export const QuotaInfoList = Schema.Struct({ "data": Schema.Array(Schema.Struct({ "metric": Schema.String.annotate({ "description": "Metric: service/resource. Not unique on its own — see the `scope` field. Index by `(metric, scope)`." }), "type": Schema.Literals(["allocation", "rate", "concurrency", "lifetime"]).annotate({ "description": "Allocation = max owned, rate = max per time window, concurrency = max in-flight, lifetime = a one-time grant. Only `rate` has a `resets_at`: a lifetime entitlement has no period, is never released by deleting the resource it funded, and reports its position in that one-way lifecycle as `grant_state`." }), "scope": Schema.Literals(["workspace", "principal", "cluster"]).annotate({ "description": "Identity the limit/usage numbers are bound to, and the second half of an entry's key. `workspace` is occupancy inside the workspace; `principal` aggregates across every workspace and organization owned by the same authenticated identity, so a new workspace does not grant fresh free capacity; `cluster` bounds in-flight work for one cluster. A metric enforced against more than one identity — every free-tier allocation, for example — returns one entry per scope, so `(metric, scope)` is the composite key. Indexing a listing by `metric` alone silently drops entries." }), "display_name": Schema.String, "unit": Schema.Literals(["count", "cores", "bytes", "seconds", "count/day", "count/minute", "cents/day"]).annotate({ "description": "Unit the limit/usage numbers are measured in." }), "limit": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "usage": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "remaining": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "resets_at": Schema.optionalKey(Schema.Number.annotate({ "description": "For rate quotas only: Unix timestamp (seconds) at which the window resets. Absent on every other type, including `lifetime`, which has no period." }).check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }))), "grant_state": Schema.optionalKey(Schema.Literals(["available", "consumed", "revoked"]).annotate({ "description": "For `lifetime` quotas only: state of the one-time grant." })) })), "has_more": Schema.Boolean.annotate({ "description": "True if more items exist after this page." }), "next_cursor": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Opaque cursor for the next page, or null if no more pages." }) }).annotate({ "identifier": "QuotaInfoList" }) +export type QuotaInfo = { readonly "metric": string, readonly "type": "allocation" | "rate" | "concurrency" | "lifetime", readonly "scope": "workspace" | "principal" | "cluster", readonly "display_name": string, readonly "unit": "count" | "cores" | "bytes" | "seconds" | "count/day" | "count/minute" | "cents/day", readonly "limit": number, readonly "usage": number, readonly "remaining": number, readonly "resets_at"?: number, readonly "grant_state"?: "available" | "consumed" | "revoked" } +export const QuotaInfo = Schema.Struct({ "metric": Schema.String.annotate({ "description": "Metric: service/resource. Not unique on its own — see the `scope` field. Index by `(metric, scope)`." }), "type": Schema.Literals(["allocation", "rate", "concurrency", "lifetime"]).annotate({ "description": "Allocation = max owned, rate = max per time window, concurrency = max in-flight, lifetime = a one-time grant. Only `rate` has a `resets_at`: a lifetime entitlement has no period, is never released by deleting the resource it funded, and reports its position in that one-way lifecycle as `grant_state`." }), "scope": Schema.Literals(["workspace", "principal", "cluster"]).annotate({ "description": "Identity the limit/usage numbers are bound to, and the second half of an entry's key. `workspace` is occupancy inside the workspace; `principal` aggregates across every workspace and organization owned by the same authenticated identity, so a new workspace does not grant fresh free capacity; `cluster` bounds in-flight work for one cluster. A metric enforced against more than one identity — every free-tier allocation, for example — returns one entry per scope, so `(metric, scope)` is the composite key. Indexing a listing by `metric` alone silently drops entries." }), "display_name": Schema.String, "unit": Schema.Literals(["count", "cores", "bytes", "seconds", "count/day", "count/minute", "cents/day"]).annotate({ "description": "Unit the limit/usage numbers are measured in." }), "limit": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "usage": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "remaining": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "resets_at": Schema.optionalKey(Schema.Number.annotate({ "description": "For rate quotas only: Unix timestamp (seconds) at which the window resets. Absent on every other type, including `lifetime`, which has no period." }).check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }))), "grant_state": Schema.optionalKey(Schema.Literals(["available", "consumed", "revoked"]).annotate({ "description": "For `lifetime` quotas only: state of the one-time grant." })) }).annotate({ "identifier": "QuotaInfo" }) export type RegionList = { readonly "data": ReadonlyArray<{ readonly "id": string, readonly "name": string, readonly "icon": string | null, readonly "workspace_id": string, readonly "created_at": number }>, readonly "has_more": boolean, readonly "next_cursor": string | null } export const RegionList = Schema.Struct({ "data": Schema.Array(Schema.Struct({ "id": Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxLength(54).annotate({ "expected": "a value with a length of at most 54" })), "name": Schema.String, "icon": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Icon URL" }), "workspace_id": Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxLength(53).annotate({ "expected": "a value with a length of at most 53" })), "created_at": Schema.Number.annotate({ "description": "Unix timestamp (seconds)" }).check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" })) })), "has_more": Schema.Boolean.annotate({ "description": "True if more items exist after this page." }), "next_cursor": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Opaque cursor for the next page, or null if no more pages." }) }).annotate({ "identifier": "RegionList" }) export type Region = { readonly "id": string, readonly "name": string, readonly "icon": string | null, readonly "workspace_id": string, readonly "created_at": number } diff --git a/test/generated-command.test.ts b/test/generated-command.test.ts index ced2091..296860e 100644 --- a/test/generated-command.test.ts +++ b/test/generated-command.test.ts @@ -200,6 +200,35 @@ describe("generated public commands", () => { expect(received?.headers.get("akua-context")).toBe("ws_123"); }); + test("quotas.list accepts a lifetime quota response", async () => { + const quotas = { + data: [ + { + metric: "compute/managed_vm_trial", + type: "lifetime", + scope: "principal", + display_name: "Managed VM trial", + unit: "count", + limit: 1, + usage: 0, + remaining: 1, + grant_state: "available", + }, + ], + has_more: false, + next_cursor: null, + }; + + const result = await runGenerated( + "quotas.list", + ["--input", "-"], + JSON.stringify({ headers: { "akua-context": "ws_123" } }), + () => Promise.resolve(Response.json(quotas)), + ); + + expect(result.data).toEqual(quotas); + }); + test("clusters.resume builds the literal :action-suffixed request path", async () => { let received: Request | undefined; const operation = clusterOperation();