fix(api): allow capacity previews through the startup route scan

This commit is contained in:
Hydra
2026-09-30 19:44:47 +03:00
parent b1dccbc71a
commit 6c4cc67410
3 changed files with 33 additions and 2 deletions
@@ -73,7 +73,7 @@ r.get("/resources", { tag: "billing:read", authorizationHandledByOperation: true
r.get("/allowances", { tag: "billing:read", authorizationHandledByOperation: true, mcp: { description: "List resources consuming workspace allowances, including the projects holding managed domains." } }, billingLocal.listAllowanceDetail);
r.get("/capacity", { tag: "billing:read", authorizationHandledByOperation: true, mcp: { description: "Read the Cloud pool's allocated CPU, memory and disk, and authorized project allocations. Includes stopped workspaces. Read this again after an adjustment to verify capacity release." } }, billingLocal.getCapacity);
r.post("/capacity/preview", { tag: "billing:read", authorizationHandledByOperation: true, body: BillingOperationSchemas.previewCapacity.input, mcp: { description: "Preview service resource edits and the services that may restart. Requires project and service write access, and the revision from capacity. Does not apply changes." } }, billingLocal.previewCapacity);
r.post("/capacity/preview", { tag: "billing:read", readOnly: true, authorizationHandledByOperation: true, body: BillingOperationSchemas.previewCapacity.input, mcp: { description: "Preview service resource edits and the services that may restart. Requires project and service write access, and the revision from capacity. Does not apply changes." } }, billingLocal.previewCapacity);
r.post("/capacity/apply", { tag: "billing:write", authorizationHandledByOperation: true, auditHandledByOperation: true, rateLimit: "billing-portal", body: BillingOperationSchemas.applyCapacity.input, mcp: { description: "Apply a reviewed capacity adjustment with explicit restart confirmation. Reuses deployed images through normal deployment logs. Requires project and service write access. Reuse the idempotency key for retries; poll the returned deployment and capacity before retrying a blocked deploy." } }, billingLocal.applyCapacity);
/* ---------- Top-ups ---------- */
@@ -71,7 +71,7 @@ r.get(
r.get("/usage", { tag: "billing:read", authorizationHandledByOperation: true, mcp: { description: "Read metered Cloud usage over the requested date range, grouped by hour or day. This is billing data, not live workload metrics." }, query: BillingOperationSchemas.getUsage.input }, billingController.getUsage);
r.get("/resources", { tag: "billing:read", authorizationHandledByOperation: true, mcp: { description: "List Cloud resources contributing to this workspace’s bill and usage." } }, billingController.getResources);
r.get("/capacity", { tag: "billing:read", authorizationHandledByOperation: true, mcp: { description: "Read the Cloud pool's allocated CPU, memory and disk, and authorized project allocations. Includes stopped workspaces. Read this again after an adjustment to verify capacity release." } }, billingController.getCapacity);
r.post("/capacity/preview", { tag: "billing:read", authorizationHandledByOperation: true, body: BillingOperationSchemas.previewCapacity.input, mcp: { description: "Preview service resource edits and the services that may restart. Requires project and service write access, and the revision from capacity. Does not apply changes." } }, billingController.previewCapacity);
r.post("/capacity/preview", { tag: "billing:read", readOnly: true, authorizationHandledByOperation: true, body: BillingOperationSchemas.previewCapacity.input, mcp: { description: "Preview service resource edits and the services that may restart. Requires project and service write access, and the revision from capacity. Does not apply changes." } }, billingController.previewCapacity);
r.post("/capacity/apply", { tag: "billing:write", authorizationHandledByOperation: true, auditHandledByOperation: true, rateLimit: "billing-portal", body: BillingOperationSchemas.applyCapacity.input, mcp: { description: "Apply a reviewed capacity adjustment with explicit restart confirmation. Reuses deployed images through normal deployment logs. Requires project and service write access. Reuse the idempotency key for retries; poll the returned deployment and capacity before retrying a blocked deploy." } }, billingController.applyCapacity);
/* ---------- Allowance detail ---------- */
@@ -0,0 +1,31 @@
import { Hono } from "hono";
import { afterEach, expect, it, vi } from "vitest";
import { enforceRouteScanAtBoot, scanRoutes } from "../../src/lib/route-scanner";
import { billingLocalRoutes } from "../../src/modules/billing/billing-local.routes";
import { billingPlansRoutes, billingSaasRoutes } from "../../src/modules/billing/billing.routes";
afterEach(() => vi.restoreAllMocks());
it.each([
["Cloud", billingSaasRoutes],
["self-hosted", billingLocalRoutes],
] as const)("allows API startup with the real %s billing routes", (_mode, routes) => {
const app = new Hono().route("/api/billing", billingPlansRoutes).route("/api/billing", routes);
expect(
app.routes.some(
(route) => route.method === "POST" && route.path === "/api/billing/capacity/preview",
),
).toBe(true);
// Exercise the production scanner, not a duplicate rule or mocked route spec.
const result = scanRoutes(app);
expect(result.errors).toEqual([]);
expect(result.ok).toBe(true);
const exit = vi.spyOn(process, "exit").mockImplementation(() => {
throw new Error("The route scanner refused API startup");
});
vi.spyOn(console, "log").mockImplementation(() => {});
enforceRouteScanAtBoot(app);
expect(exit).not.toHaveBeenCalled();
});