diff --git a/CHANGELOG.md b/CHANGELOG.md index ee3cd9b..81128f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Reformat `/commandcode-quota` output into a dashboard layout: credits remaining/used with a percentage, monthly/purchased/free sources, plan, month-to-date cost/requests/tokens, and API key name, while keeping the 5-hour/weekly usage windows and full-detail link. +- Optionally show an aggregate token count and API key name when the usage endpoints report them. +- Fix `/commandcode-quota` on Oh My Pi: OMP surfaces an unresolved `$COMMANDCODE_API_KEY` placeholder through the model registry, which previously caused a 401 on the quota endpoints. The command now filters placeholders and falls back to the env/auth-file key resolver, matching the stream path. +- Fix `/commandcode-quota` usage windows so the 5-hour and weekly limits actually render: the API reports `windowLimits` as a top-level sibling of `credits` (not nested inside it), and its `resetAt` is in milliseconds, not seconds. Both are now parsed correctly, giving real "resets in …" countdowns instead of omitting the section or showing a huge day count. - Prefer Command Code's Provider API (`/provider/v1/chat/completions` and `/provider/v1/messages`) and automatically fall back to the existing `/alpha/generate` transport only when the Provider API returns `403 upgrade_required` for a Go-plan account. - Remember the detected transport for the running process, re-detect it when credentials change, prevent stale in-flight requests from overwriting the new credential's transport, and never fall back for unrelated authentication, permission, rate-limit, network, or server failures. - Use Pi's native OpenAI- and Anthropic-compatible providers for Provider API streaming, including adaptive thinking for current reasoning-capable Claude models, while preserving the existing hardened generate transport, dynamic model discovery, offline cache, refresh/status commands, pricing, and OAuth credentials. diff --git a/README.md b/README.md index 4b32189..b140206 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,9 @@ While pi is running, use these provider commands without restarting: - `/commandcode-refresh` fetches and re-registers the current model catalog. Overlapping refreshes are coalesced, and a failed refresh keeps the last valid catalog active. - `/commandcode-status` shows redacted discovery diagnostics, including the source, model count, timestamps, cache path, endpoint, and warning. +- `/commandcode-quota` shows your Command Code account usage and quota in a dashboard-style layout: credits remaining and used with a percentage, monthly/purchased/free sources, the current plan, month-to-date cost/requests/tokens, the API key name, and the 5-hour and weekly usage windows. + +The `commandcode-quota` command reads from the Command Code alpha usage endpoints (the same ones the `cmd` CLI `/usage` command uses): `whoami`, `billing/credits`, `billing/subscriptions`, and `usage/summary`. It authenticates with the same API key the provider already uses. If command cannot reach those endpoints or they change, the command reports a readable error instead of failing. Output is plain text (via `ui.notify`) so it works across pi and compatible hosts such as OMP. Set `COMMANDCODE_ZDR=1` to send Command Code's documented `x-cmd-zdr: 1` zero-data-retention header. diff --git a/index.ts b/index.ts index 72ae558..be7891f 100644 --- a/index.ts +++ b/index.ts @@ -18,6 +18,7 @@ import { join } from "node:path" import { getConfiguredApiKey } from "./src/api-key.ts" import { createStreamCommandCode } from "./src/core.ts" import { calculateCommandCodeCost } from "./src/cost.ts" +import { pickCommandCodeApiKey } from "./src/converters.ts" import { baseUrlForModel, DEFAULT_MODELS_URL, @@ -32,8 +33,19 @@ import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts import { normalizeCommandCodeMessage } from "./src/overflow.ts" import { MODEL_COSTS, ZERO_MODEL_COST } from "./src/pricing.ts" import { createCommandCodeRuntime } from "./src/runtime.ts" +import { fetchCommandCodeQuota, formatQuota, redactValue } from "./src/quota.ts" import { createCommandCodeTransportRouter } from "./src/transport.ts" +const COMMAND_CODE_PROVIDER_ID = "commandcode" + +async function resolveCommandCodeApiKey(ctx: ExtensionCommandContext): Promise { + const registryKey = await ctx.modelRegistry?.getApiKeyForProvider?.(COMMAND_CODE_PROVIDER_ID) + // Mirror src/core.ts: OMP may surface an unresolved placeholder; fall back + // to the env/auth-file resolver so we never send a literal placeholder as a + // Bearer token (which caused a 401 on /alpha/whoami). + return pickCommandCodeApiKey(registryKey, getConfiguredApiKey()) +} + function commandCodeHeaders(): Record | undefined { if (process.env.COMMANDCODE_ZDR === "1") { return { "x-cmd-zdr": "1" } @@ -118,6 +130,43 @@ export default async function (pi: ExtensionAPI) { return normalized ? { message: normalized.message } : undefined }) + pi.registerCommand("commandcode-quota", { + description: "Show Command Code account usage and quota", + handler: async (_args, ctx) => { + await ctx.waitForIdle?.() + + // Resolve the key in a host-agnostic way so the command also works on + // OMP (which passes an unresolved "$COMMANDCODE_API_KEY" placeholder + // through the registry): filter placeholders and fall back to the + // env/auth-file resolver, mirroring src/core.ts. + const apiKey = await resolveCommandCodeApiKey(ctx) + if (!apiKey) { + ctx.ui.notify( + "Command Code quota requires an API key. Run /login and select Command Code, or set the COMMANDCODE_API_KEY env var.", + "warning", + ) + return + } + + const result = await fetchCommandCodeQuota({ + apiKey, + // Alpha endpoints live under the legacy base (no /provider/v1), + // same as the fallback generate transport. + baseUrl: legacyApiBase(apiBase), + // Respect the user's zero-data-retention preference on usage/account + // calls too, matching the provider stream path. + extraHeaders: commandCodeHeaders(), + }) + + if (!result.ok) { + ctx.ui.notify(redactValue(result.error.message), "error") + return + } + + ctx.ui.notify(formatQuota(result.quota), "info") + }, + }) + const runtime = createCommandCodeRuntime(pi, { endpoint: modelsUrl, cachePath: modelsCachePath, diff --git a/package.json b/package.json index f284d3b..e00b16f 100644 --- a/package.json +++ b/package.json @@ -29,12 +29,13 @@ "LICENSE" ], "scripts": { - "test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs", + "test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs", "typecheck": "tsc --noEmit", "format:check": "prettier --check '**/*.{ts,mjs,json,md}'", "format": "prettier --write '**/*.{ts,mjs,json,md}'", "pi:isolated": "node scripts/pi-isolated.mjs", "pi:authenticated": "node scripts/pi-authenticated.mjs", + "test:quota": "tsx tests/test-quota.ts", "test:unit": "tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts", "test:api-key": "tsx tests/test-api-key.ts", "test:models": "tsx tests/test-models.ts", diff --git a/src/converters.ts b/src/converters.ts index 4012fbb..bb60df2 100644 --- a/src/converters.ts +++ b/src/converters.ts @@ -138,6 +138,29 @@ export function getApiKey( return undefined } +// Hosts such as OMP may pass the literal env-var name "$COMMANDCODE_API_KEY" +// (or "COMMANDCODE_API_KEY") as the "resolved" registry key instead of the +// actual credential. Treat those as unresolved. +export const COMMAND_CODE_PLACEHOLDER_KEYS = new Set([ + "$COMMANDCODE_API_KEY", + "COMMANDCODE_API_KEY", +]) + +/** + * Pick the real API key from a host registry value and/or the env/auth-file + * fallback, never returning a literal placeholder or an empty/whitespace value. + * Pure/testable. + */ +export function pickCommandCodeApiKey( + registryKey: string | undefined, + hostKey: string | undefined, +): string | undefined { + const trimmed = typeof registryKey === "string" ? registryKey.trim() : undefined + if (!trimmed) return hostKey + if (COMMAND_CODE_PLACEHOLDER_KEYS.has(trimmed)) return hostKey + return trimmed +} + export function textContent(message: { content?: unknown }): string { return recordArray(message.content) .filter((part) => part.type === "text") diff --git a/src/quota.ts b/src/quota.ts new file mode 100644 index 0000000..15a0263 --- /dev/null +++ b/src/quota.ts @@ -0,0 +1,613 @@ +/** + * Command Code usage/quota fetch layer for the `/commandcode-quota` command. + * + * Command Code exposes account usage through a set of authenticated alpha + * endpoints (the same ones the `cmd` CLI `/usage` command uses): + * + * - `/alpha/whoami` -> resolved account + optional org id + * - `/alpha/billing/credits` -> monthly/purchased/free credits + window limits + * - `/alpha/billing/subscriptions`-> plan id, status, billing period + * - `/alpha/usage/summary` -> period totals (cost, request count, optional tokens) + * + * These endpoints are not part of the documented public Provider API + * (`/provider/v1/*`) but are shipped with every `command-code` CLI release and + * authenticate with the same API key the provider already uses. Fetches are + * wrapped defensively so the quota command degrades to a readable error rather + * than surfacing raw transport details. + */ + +import { redactCommandCodeErrorText } from "./overflow.ts" + +export const DEFAULT_API_BASE = "https://api.commandcode.ai" + +export const QUOTA_TIMEOUT_MS = 15_000 + +/** + * A single rolling usage window (the 5-hour or weekly cap on a plan's monthly + * credits). Values are measured in credit value, not request count. + */ +export interface CommandCodeWindowLimit { + window: "fiveHour" | "weekly" + used: number + cap: number + /** Unix epoch seconds when this window resets, normalized from seconds or ms. */ + resetAt: number | null +} + +/** Credits exposed by the `/alpha/billing/credits` endpoint. */ +export interface CommandCodeCredits { + monthlyCredits: number + purchasedCredits: number + freeCredits: number + remainingCredits: number + windowLimits: CommandCodeWindowLimit[] +} + +/** Subscription/plan info exposed by `/alpha/billing/subscriptions`. */ +export interface CommandCodeSubscription { + planId: string | null + status: string | null + currentPeriodStart: string | null + currentPeriodEnd: string | null +} + +/** Period totals exposed by `/alpha/usage/summary`. */ +export interface CommandCodeUsageSummary { + totalCost: number + totalCount: number + /** Optional aggregate token count; only shown when the endpoint reports it. */ + totalTokens?: number +} + +/** Fully normalized quota snapshot for display. */ +export interface CommandCodeQuota { + account: { + login: string + orgId: string | null + /** Optional API key / account display name; falls back to login. */ + keyName?: string + } + credits: CommandCodeCredits | null + subscription: CommandCodeSubscription | null + summary: CommandCodeUsageSummary | null +} + +export type CommandCodeQuotaResult = + | { ok: true; quota: CommandCodeQuota } + | { ok: false; error: { message: string; kind: "config" | "http" | "network" | "timeout" } } + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function numberValue(value: unknown): number | undefined { + if (typeof value !== "number" || !Number.isFinite(value)) return undefined + return value >= 0 ? value : undefined +} + +function stringValue(value: unknown): string | undefined { + return typeof value === "string" && value.length > 0 ? value : undefined +} + +interface FetchOptions { + apiKey: string + baseUrl?: string + fetchImpl?: typeof fetch + timeoutMs?: number + /** Extra HTTP headers merged after Content-Type/Authorization (e.g. ZDR). */ + extraHeaders?: Record +} + +function buildUrl(path: string, params: Record): string { + const search = new URLSearchParams() + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null && value !== "") search.set(key, value) + } + const query = search.toString() + return `${path}${query ? `?${query}` : ""}` +} + +/** Extract the WindowLimit array from the top-level `windowLimits` object. */ +export function windowLimitsFromCredits(value: unknown): CommandCodeWindowLimit[] { + if (!isRecord(value)) return [] + const limits: CommandCodeWindowLimit[] = [] + + for (const [window, entry] of [ + ["fiveHour", value.fiveHour], + ["weekly", value.weekly], + ] as const) { + if (!isRecord(entry)) continue + const used = numberValue(entry.used) ?? 0 + const cap = numberValue(entry.cap) ?? 0 + if (cap <= 0 && used <= 0) continue + limits.push({ + window, + used, + cap, + resetAt: normalizeResetAt(entry.resetAt), + }) + } + + return limits +} + +/** + * Normalize a `resetAt` value to epoch seconds. Accepts seconds (10-digit), + * milliseconds (13-digit, live API), a numeric string, or an ISO timestamp + * string, then converts ms -> s consistently. Invalid/negative values -> null. + */ +function normalizeResetAt(value: unknown): number | null { + let num: number | undefined + if (typeof value === "number" && Number.isFinite(value)) { + num = value + } else if (typeof value === "string" && value.length > 0) { + const trimmed = value.trim() + if (/^\d+$/.test(trimmed)) { + num = Number(trimmed) + } else { + const parsed = Date.parse(trimmed) + if (!Number.isNaN(parsed)) num = Math.round(parsed / 1000) + } + } + if (num === undefined || num < 0) return null + return num >= 1e12 ? Math.round(num / 1000) : num +} + +function parseCredits(value: unknown): CommandCodeCredits | null { + const credits = isRecord(value) ? value.credits : undefined + if (!isRecord(credits)) return null + + // `windowLimits` is a top-level sibling of `credits` in the + // `/alpha/billing/credits` response, not nested inside it. + const windowLimits = isRecord(value) ? value.windowLimits : undefined + + const monthlyCredits = numberValue(credits.monthlyCredits) ?? 0 + const purchasedCredits = numberValue(credits.purchasedCredits) ?? 0 + const freeCredits = numberValue(credits.freeCredits) ?? 0 + + return { + monthlyCredits, + purchasedCredits, + freeCredits, + remainingCredits: monthlyCredits + purchasedCredits + freeCredits, + windowLimits: windowLimitsFromCredits(windowLimits), + } +} + +function parseSubscription(value: unknown): CommandCodeSubscription | null { + const data = isRecord(value) ? value.data : undefined + if (!isRecord(data)) return null + + return { + planId: stringValue(data.planId) ?? null, + status: stringValue(data.status) ?? null, + currentPeriodStart: stringValue(data.currentPeriodStart) ?? null, + currentPeriodEnd: stringValue(data.currentPeriodEnd) ?? null, + } +} + +function parseSummary(value: unknown): CommandCodeUsageSummary | null { + if (!isRecord(value)) return null + const totalTokens = numberValue(value.totalTokens) ?? numberValue(value.tokens) + return { + totalCost: numberValue(value.totalCost) ?? 0, + totalCount: numberValue(value.totalCount) ?? 0, + ...(totalTokens === undefined ? {} : { totalTokens }), + } +} + +function parseWhoami(value: unknown): { + login: string + orgId: string | null + keyName?: string +} { + const org = isRecord(value) ? value.org : undefined + const user = isRecord(value) ? value.user : undefined + + const orgLogin = isRecord(org) ? stringValue(org.login) : undefined + const orgId = isRecord(org) ? stringValue(org.id) : undefined + const userLogin = + (isRecord(user) ? stringValue(user.userName) : undefined) ?? + (isRecord(user) ? stringValue(user.name) : undefined) + + const keyName = + stringValue(isRecord(user) ? user.keyName : undefined) ?? + stringValue(isRecord(user) ? user.displayName : undefined) + + return { + login: orgLogin ?? userLogin ?? "Unknown account", + orgId: orgId ?? null, + ...(keyName === undefined ? {} : { keyName }), + } +} + +/** + * Parse the `windowLimits` into a human-readable, header-safe line list that + * the formatting layer appends. Split out so the pure shape is independently + * testable. + */ +export function formatWindowLimits( + limits: readonly CommandCodeWindowLimit[], + now: () => number = Date.now, +): string[] { + const labels: Record = { + fiveHour: "5-hour", + weekly: "Weekly", + } + + return limits.map((limit) => { + const label = labels[limit.window] ?? limit.window + const used = limit.used.toFixed(2) + const cap = limit.cap.toFixed(2) + const pct = limit.cap > 0 ? Math.round((limit.used / limit.cap) * 100) : 0 + const reset = limit.resetAt === null ? "" : ` (resets ${formatResetClock(limit.resetAt, now)})` + return `${label}: ${used} / ${cap} credits (${pct}% used)${reset}` + }) +} + +function formatResetClock(resetAtSeconds: number, now: () => number = Date.now): string { + const date = new Date(resetAtSeconds * 1000) + if (Number.isNaN(date.getTime())) return "unknown" + const nowMs = now() + const diffMs = date.getTime() - nowMs + if (diffMs <= 0) return "soon" + const minutes = Math.ceil(diffMs / 60_000) + if (minutes < 60) return `in ${minutes}m` + const hours = Math.floor(minutes / 60) + const rem = minutes % 60 + if (hours < 24) return rem > 0 ? `in ${hours}h ${rem}m` : `in ${hours}h` + const days = Math.floor(hours / 24) + return days === 1 ? "in 1 day" : `in ${days} days` +} + +/** Derived credits view for the Remaining/Used layout. */ +interface CreditView { + /** Credits remaining (monthly + purchased + free). */ + remaining: number + /** Dollars spent this period (totalCost). */ + spent: number + /** Total pool used as the percentage denominator: remaining + spent. */ + pool: number + /** Percent of the pool used, 0-100. */ + usedPercent: number + hasCreditsInfo: boolean +} + +function creditView(quota: CommandCodeQuota): CreditView { + const credits = quota.credits + const remaining = credits ? credits.remainingCredits : 0 + const spent = quota.summary?.totalCost ?? 0 + const pool = remaining + spent + const hasCreditsInfo = Boolean(credits) || spent > 0 + return { + remaining, + spent, + pool, + usedPercent: hasCreditsInfo ? Math.round((pool > 0 ? spent / pool : 0) * 100) : 0, + hasCreditsInfo, + } +} + +function creditDetailLine(credits: CommandCodeCredits | null): string { + if (!credits) return "" + const parts = [`monthly $${credits.monthlyCredits.toFixed(2)}`] + parts.push(`purchased $${credits.purchasedCredits.toFixed(2)}`) + if (credits.freeCredits > 0) parts.push(`free $${credits.freeCredits.toFixed(2)}`) + return `Sources: ${parts.join(" / ")}` +} + +function subscriptionLine(subscription: CommandCodeSubscription): string { + const rank = subscription.planId ?? "Unknown" + const plan = rank.replace(/[_-]+/g, " ").trim() + const status = subscription.status ? ` (${subscription.status})` : "" + return `Plan: ${plan}${status}` +} + +function accountName(account: CommandCodeQuota["account"]): string { + return account.keyName ?? account.login +} + +/** + * Render a normalized quota snapshot as clean, aligned, dashboard-style text + * suitable for `ui.notify`. Pure so it can be unit tested without a runtime. + */ +export function formatQuota(quota: CommandCodeQuota, now: () => number = Date.now): string { + const lines: string[] = [] + + const credit = creditView(quota) + if (credit.hasCreditsInfo) { + lines.push("") + lines.push("Credits") + lines.push(padValue(`Remaining: $${credit.remaining.toFixed(2)} of $${credit.pool.toFixed(2)}`)) + lines.push(padValue(`Used: $${credit.spent.toFixed(2)}`)) + lines.push(` ${credit.usedPercent}% used`) + } + + const detail = creditDetailLine(quota.credits) + if (detail) lines.push(detail) + + if (quota.subscription) lines.push(subscriptionLine(quota.subscription)) + + if (quota.summary) { + lines.push("") + lines.push("Usage (this month)") + lines.push(padValue(`Cost: $${quota.summary.totalCost.toFixed(2)}`)) + lines.push(padValue(`Requests: ${quota.summary.totalCount.toLocaleString("en-US")}`)) + if (quota.summary.totalTokens && quota.summary.totalTokens > 0) { + lines.push(padValue(`Tokens: ${formatTokens(quota.summary.totalTokens)}`)) + } + } + + lines.push("") + lines.push("Username") + lines.push(padValue(accountName(quota.account))) + + const limits = quota.credits?.windowLimits ?? [] + if (limits.length > 0) { + lines.push("") + lines.push("Usage windows:") + lines.push(...formatWindowLimits(limits, now).map((line) => ` ${line}`)) + } + + lines.push("") + lines.push(`Full detail: https://commandcode.ai/usage`) + + // Trim leading/trailing blank lines so sections stay cleanly separated. + while (lines.length > 0 && lines[0].length === 0) lines.shift() + while (lines.length > 0 && lines[lines.length - 1].length === 0) lines.pop() + return lines.join("\n") +} + +function padValue(value: string): string { + return ` ${value}` +} + +function formatTokens(tokens: number): string { + if (tokens >= 1_000_000_000) return `${(tokens / 1_000_000_000).toFixed(1)}B` + if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M` + if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k` + return String(tokens) +} + +/** + * Fetch the current account quota from Command Code. + * + * Resolution chain: whoami -> org id -> credits + subscription (parallel) -> + * summary (needs the billing period start). Any individual endpoint failing + * degrades gracefully: the remaining data is still reported, and a hard + * failure (auth/config, network) is surfaced as a typed error. + */ +export async function fetchCommandCodeQuota( + options: FetchOptions, +): Promise { + if (!options.apiKey) { + return { + ok: false, + error: { message: "No Command Code API key found", kind: "config" }, + } + } + + const baseUrl = options.baseUrl ?? DEFAULT_API_BASE + const fetchImpl = options.fetchImpl ?? fetch + const timeoutMs = options.timeoutMs ?? QUOTA_TIMEOUT_MS + + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${options.apiKey}`, + ...options.extraHeaders, + } + + // One overall deadline shared across the sequential phases (whoami -> billing + // -> summary) so a slow or blackholed dependency cannot compound per-request + // timeouts into a ~45s stall; the command reports within QUOTA_TIMEOUT_MS. + const overallController = new AbortController() + const overallTimer = setTimeout(() => overallController.abort(), timeoutMs) + + const request = async (path: string): Promise => { + // The overall deadline may already have fired (e.g. a prior phase consumed + // the budget) — AbortSignal does not replay past abort events to listeners + // added afterward, so check synchronously instead of relying on the listener. + if (overallController.signal.aborted) { + throw new QuotaTimeoutError() + } + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), timeoutMs) + const onOverallAbort = () => controller.abort() + overallController.signal.addEventListener("abort", onOverallAbort) + try { + const response = await fetchImpl(`${baseUrl}${path}`, { + method: "GET", + headers, + signal: controller.signal, + }) + + if (!response.ok) { + let message = response.statusText + if (response.status === 401 || response.status === 403) { + message = "Command Code rejected the API key (401/403)" + } + return { + __httpError: true, + message, + status: response.status, + body: await response.text().catch(() => ""), + } + } + return await response.json() + } catch (error) { + if (controller.signal.aborted) { + throw new QuotaTimeoutError() + } + throw error + } finally { + clearTimeout(timer) + overallController.signal.removeEventListener("abort", onOverallAbort) + } + } + + /** Optional-endpoint wrapper: never throws. Transport/timeout/parse failures + * become a sentinel so the rest of the dashboard still renders, matching the + * existing graceful degradation for HTTP 5xx responses. */ + const safeRequest = async (path: string): Promise => { + try { + return await request(path) + } catch (error) { + return { + __quotaError: true, + message: errorMessage(error), + kind: error instanceof QuotaTimeoutError ? "timeout" : "network", + } + } + } + + try { + const whoami = await request("/alpha/whoami") + if (isHttpError(whoami)) return httpFailure(whoami, "whoami") + const account = parseWhoami(whoami) + + const orgId = account.orgId ?? undefined + const creditsPath = buildUrl("/alpha/billing/credits", { orgId }) + const subPath = buildUrl("/alpha/billing/subscriptions", { orgId }) + + const [creditsRaw, subRaw] = await Promise.all([safeRequest(creditsPath), safeRequest(subPath)]) + + // Hard auth/permission failures abort; everything else (including thrown + // network/timeout/parse failures) degrades to a null section. + if (isHttpError(creditsRaw) && isBlockingQuotaHttpError(creditsRaw)) { + return httpFailure(creditsRaw, "credits") + } + if (isHttpError(subRaw) && isBlockingQuotaHttpError(subRaw)) { + return httpFailure(subRaw, "subscription") + } + + const credits = + creditsRaw && !isHttpError(creditsRaw) && !isQuotaError(creditsRaw) + ? parseCredits(creditsRaw) + : null + const subscription = + subRaw && !isHttpError(subRaw) && !isQuotaError(subRaw) ? parseSubscription(subRaw) : null + + const since = subscription?.currentPeriodStart ?? undefined + const summaryPath = buildUrl("/alpha/usage/summary", { orgId, since }) + const summaryRaw = await safeRequest(summaryPath) + if (isHttpError(summaryRaw) && isBlockingQuotaHttpError(summaryRaw)) { + return httpFailure(summaryRaw, "summary") + } + const summary = + summaryRaw && !isHttpError(summaryRaw) && !isQuotaError(summaryRaw) + ? parseSummary(summaryRaw) + : null + + if (credits === null && subscription === null && summary === null) { + if (overallController.signal.aborted) { + return { + ok: false, + error: { message: "Command Code quota request timed out", kind: "timeout" }, + } + } + return { + ok: false, + error: { + message: "Command Code returned no usage data for the account", + kind: "http", + }, + } + } + + return { + ok: true, + quota: { account, credits, subscription, summary }, + } + } catch (error) { + if (error instanceof QuotaTimeoutError || overallController.signal.aborted) { + return { + ok: false, + error: { message: "Command Code quota request timed out", kind: "timeout" }, + } + } + return { + ok: false, + error: { + message: redactValue(`Failed to fetch Command Code quota: ${errorMessage(error)}`), + kind: "network", + }, + } + } finally { + clearTimeout(overallTimer) + } +} + +class QuotaTimeoutError extends Error { + constructor() { + super("Command Code quota request timed out") + this.name = "QuotaTimeoutError" + } +} + +interface HttpErrorShape { + __httpError: true + message: string + status: number + body: string +} + +function isHttpError(value: unknown): value is HttpErrorShape { + if (!isRecord(value) || value.__httpError !== true) return false + return ( + typeof value.status === "number" && + typeof value.message === "string" && + typeof value.body === "string" + ) +} + +/** Sentinel produced by safeRequest for thrown transport/timeout/parse failures. */ +interface QuotaErrorShape { + __quotaError: true + message: string + kind: "timeout" | "network" +} + +function isQuotaError(value: unknown): value is QuotaErrorShape { + if (!isRecord(value) || value.__quotaError !== true) return false + return typeof value.message === "string" && (value.kind === "timeout" || value.kind === "network") +} + +/** + * Hard-failure HTTP statuses for the quota dashboard: authentication and + * permission failures. Rate limiting (429) is deliberately NOT included — it + * is a transient dependency condition that should degrade like other non-auth + * endpoint failures, not abort the whole command. + */ +function isBlockingQuotaHttpError(error: HttpErrorShape): boolean { + return error.status === 401 || error.status === 403 +} + +function httpFailure(error: HttpErrorShape, context: string): CommandCodeQuotaResult { + const detail = error.body.trim().slice(0, 200) + const message = detail + ? `${context} request failed (${error.status}): ${detail}` + : `${context} request failed (${error.status}): ${error.message}` + return { + ok: false, + error: { message: redactValue(message), kind: "http" }, + } +} + +/** Best-effort scrub of values that look like tokens/secrets from a message. */ +export function redactValue(value: string): string { + // Reuse the broader Command Code redaction (Bearer, credential key-value + // fields, user_/cc_ tokens, query-string secrets, standalone keys) so quota + // errors get the same protection as stream errors. Additionally catch + // JSON-quoted credential fields ({"apiKey":"..."}) that the upstream pattern + // requires to be adjacent to `=`/`:`. + return redactCommandCodeErrorText(value) + .replace( + /("\s*(?:api[-_ ]?key|apikey|access[-_ ]?token|refresh[-_ ]?token|token|secret|password|authorization)\s*"\s*:\s*")([^"]{8,})/gi, + "$1[redacted]", + ) + .trim() +} diff --git a/tests/test-omp-compat.mjs b/tests/test-omp-compat.mjs index 9daae39..eeb5793 100644 --- a/tests/test-omp-compat.mjs +++ b/tests/test-omp-compat.mjs @@ -165,7 +165,13 @@ function runOmp(args, timeoutMs = 30_000) { try { console.log("[omp-compat] list models through real extension") modelListRequestCount = 0 - const result = await runOmp(["-e", EXT_PATH, "--list-models"]) + // Prefer the flag form `omp -e EXT --list-models`; Homebrew's `omp` + // distribution only exposes the `omp models` subcommand, so fall back to + // that form when the flag invocation is not recognized. + let result = await runOmp(["-e", EXT_PATH, "--list-models"]) + if (result.code !== 0) { + result = await runOmp(["models", "-e", EXT_PATH]) + } assert.equal(result.code, 0, result.stderr) const listOutput = result.stdout || result.stderr assert.match(listOutput, /commandcode/) diff --git a/tests/test-pure-functions.ts b/tests/test-pure-functions.ts index 136025f..d71dba0 100644 --- a/tests/test-pure-functions.ts +++ b/tests/test-pure-functions.ts @@ -16,6 +16,7 @@ import { mapFinishReason, messagesToCC, parseStreamEventLine, + pickCommandCodeApiKey, projectSlugFromPath, textContent, toJsonSchema, @@ -106,6 +107,36 @@ describe("error redaction", () => { }) }) +describe("pickCommandCodeApiKey()", () => { + it("falls back to the host key for a placeholder registry value", () => { + assert.equal(pickCommandCodeApiKey("$COMMANDCODE_API_KEY", "file-key"), "file-key") + assert.equal(pickCommandCodeApiKey("COMMANDCODE_API_KEY", "file-key"), "file-key") + }) + + it("returns undefined when only a placeholder is provided (no fallback)", () => { + assert.equal(pickCommandCodeApiKey("$COMMANDCODE_API_KEY", undefined), undefined) + }) + + it("prefers a real registry key over the host fallback", () => { + assert.equal(pickCommandCodeApiKey("real-registry-key", "file-key"), "real-registry-key") + }) + + it("falls back to the host key when the registry has none", () => { + assert.equal(pickCommandCodeApiKey(undefined, "file-key"), "file-key") + assert.equal(pickCommandCodeApiKey(undefined, undefined), undefined) + }) + + it("falls back to the host key for empty or whitespace registry values", () => { + assert.equal(pickCommandCodeApiKey("", "file-key"), "file-key") + assert.equal(pickCommandCodeApiKey(" ", "file-key"), "file-key") + assert.equal(pickCommandCodeApiKey(" ", undefined), undefined) + }) + + it("trims a real registry key", () => { + assert.equal(pickCommandCodeApiKey(" real-registry-key ", "file-key"), "real-registry-key") + }) +}) + describe("projectSlugFromPath()", () => { it("matches the official CLI-style slug from an absolute working directory", () => { assert.equal( @@ -359,7 +390,7 @@ describe("toJsonSchema()", () => { if (!outputProperties || typeof outputProperties !== "object") { throw new Error("expected object properties") } - assert.ok(Object.prototype.hasOwnProperty.call(outputProperties, "__proto__")) + assert.ok(Object.hasOwn(outputProperties, "__proto__")) assert.deepEqual(Object.getOwnPropertyDescriptor(outputProperties, "__proto__")?.value, { type: "string", }) diff --git a/tests/test-quota.ts b/tests/test-quota.ts new file mode 100644 index 0000000..5eea0d8 --- /dev/null +++ b/tests/test-quota.ts @@ -0,0 +1,395 @@ +/** + * Unit tests for the Command Code quota layer (src/quota.ts). + * + * These are hermetic: no pi runtime and no network. Fetching is exercised with + * a mocked `fetchImpl`, while parsing and formatting are pure function checks. + */ + +import assert from "node:assert/strict" +import { describe, it } from "node:test" + +import { + DEFAULT_API_BASE, + fetchCommandCodeQuota, + formatQuota, + formatWindowLimits, + redactValue, + windowLimitsFromCredits, +} from "../src/quota.ts" +import type { CommandCodeQuota, CommandCodeCredits, CommandCodeWindowLimit } from "../src/quota.ts" + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" }, + }) +} + +function okFetch(handlers: Record) { + const urls: string[] = [] + const fetchImpl = async (input: RequestInfo | URL): Promise => { + const url = String(input) + urls.push(url) + for (const [needle, body] of Object.entries(handlers)) { + if (url.includes(needle)) return jsonResponse(body) + } + throw new Error(`Unexpected URL: ${url}`) + } + return { fetchImpl, urls: () => urls } +} + +describe("Command Code quota", () => { + it("parses window limits from the credits windowLimits object", () => { + const limits = windowLimitsFromCredits({ + limited: true, + // resetAt as reported by the live API: milliseconds since epoch. + fiveHour: { used: 8, cap: 14, resetAt: 1_700_000_000_000 }, + weekly: { used: 30, cap: 35, resetAt: 1_700_000_000_000 }, + }) + assert.deepEqual(limits, [ + { window: "fiveHour", used: 8, cap: 14, resetAt: 1_700_000_000 }, + { window: "weekly", used: 30, cap: 35, resetAt: 1_700_000_000 }, + ]) + }) + + it("skips empty window limit entries", () => { + const limits = windowLimitsFromCredits({ + limited: false, + fiveHour: { used: 0, cap: 0, resetAt: null }, + weekly: { used: 0, cap: 0, resetAt: null }, + }) + assert.deepEqual(limits, []) + }) + + it("parses resetAt as numeric string or ISO timestamp string", () => { + const limits = windowLimitsFromCredits({ + fiveHour: { used: 1, cap: 2, resetAt: "1700000000000" }, + weekly: { used: 1, cap: 2, resetAt: "2023-11-14T22:13:20.000Z" }, + }) + // numeric ms string -> epoch seconds; ISO string -> epoch seconds + assert.equal(limits[0]?.resetAt, 1_700_000_000) + assert.equal(limits[1]?.resetAt, 1_700_000_000) + }) + + it("renders a zero request count instead of dropping the Requests line", () => { + const quota: CommandCodeQuota = { + account: { login: "alice", orgId: null }, + credits: null, + subscription: null, + summary: { totalCost: 0, totalCount: 0 }, + } + const output = formatQuota(quota, () => 1_700_000_000_000) + assert.match(output, /Requests: 0/) + }) + + it("formats window limits with percentage and reset clock", () => { + const limits: CommandCodeWindowLimit[] = [ + { window: "fiveHour", used: 7, cap: 14, resetAt: 1_700_000_000 }, + { window: "weekly", used: 0, cap: 35, resetAt: null }, + ] + const lines = formatWindowLimits(limits) + assert.match(lines[0] ?? "", /^5-hour: 7\.00 \/ 14\.00 credits \(50% used\) \(resets/) + assert.match(lines[1] ?? "", /^Weekly: 0\.00 \/ 35\.00 credits \(0% used\)/) + }) + + it("uses the injected clock for the reset countdown", () => { + const limit: CommandCodeWindowLimit = { + window: "fiveHour", + used: 7, + cap: 14, + resetAt: 1_700_000_000, // seconds since epoch + } + // now() shortly before reset -> a short "in Nm" countdown + const soon = formatWindowLimits([limit], () => 1_699_999_000 * 1000)[0] + assert.match(soon ?? "", /\(resets in \d+m\)/) + // already past reset -> "soon" + const past = formatWindowLimits([limit], () => 1_700_100_000 * 1000)[0] + assert.match(past ?? "", /\(resets soon\)/) + }) + + it("fetches and normalizes the full quota snapshot", async () => { + const { fetchImpl, urls } = okFetch({ + whoami: { user: { userName: "alice" }, org: { id: "org_1", login: "alice-inc" } }, + credits: { + credits: { + monthlyCredits: 40, + purchasedCredits: 10, + freeCredits: 5, + planId: "pro", + }, + windowLimits: { + fiveHour: { used: 8, cap: 16, resetAt: 1_700_000_000_000 }, + weekly: { used: 20, cap: 40, resetAt: null }, + }, + }, + subscriptions: { + data: { + planId: "pro", + status: "active", + currentPeriodStart: "2026-01-01T00:00:00Z", + currentPeriodEnd: "2026-02-01T00:00:00Z", + }, + }, + summary: { totalCost: 12.34, totalCount: 1500 }, + }) + + const result = await fetchCommandCodeQuota({ apiKey: "cc_test_key", fetchImpl }) + assert.equal(result.ok, true) + if (!result.ok) return + + assert.equal(result.quota.account.login, "alice-inc") + assert.equal(result.quota.account.orgId, "org_1") + assert.deepEqual(result.quota.credits?.remainingCredits, 55) + assert.equal(result.quota.credits?.windowLimits.length, 2) + assert.equal(result.quota.subscription?.planId, "pro") + assert.equal(result.quota.summary?.totalCost, 12.34) + + // Regression: requested URLs must carry the base exactly once (no + // double prefix), and all hit the alpha usage endpoints. + const fetched = urls() + assert.equal(fetched.length, 4) + for (const url of fetched) { + assert.ok( + /^https:\/\/api\.commandcode\.ai\/alpha\//.test(url), + `expected base-prefixed alpha URL, got: ${url}`, + ) + assert.equal((url.match(/https:\/\//g) ?? []).length, 1) + assert.equal(url.includes(`${DEFAULT_API_BASE}${DEFAULT_API_BASE}`), false) + } + }) + + it("degrades gracefully when individual billing endpoints fail", async () => { + const fetchImpl = async (input: RequestInfo | URL): Promise => { + const url = String(input) + if (url.includes("whoami")) return jsonResponse({ user: { userName: "alice" }, org: null }) + if (url.includes("summary")) return jsonResponse({ totalCost: 3.0, totalCount: 10 }) + if (url.includes("credits") || url.includes("subscriptions")) { + return jsonResponse({ error: "boom" }, 500) + } + throw new Error(`Unexpected URL: ${url}`) + } + + const result = await fetchCommandCodeQuota({ apiKey: "cc_test_key", fetchImpl }) + assert.equal(result.ok, true) + if (!result.ok) return + assert.equal(result.quota.credits, null) + assert.equal(result.quota.summary?.totalCost, 3.0) + // Optional aggregate tokens are parsed when the summary reports them. + assert.equal(result.quota.summary?.totalTokens, undefined) + }) + + it("degrades on thrown network failures from optional endpoints, not just HTTP 5xx", async () => { + const fetchImpl = async (input: RequestInfo | URL): Promise => { + const url = String(input) + if (url.includes("whoami")) return jsonResponse({ user: { userName: "alice" }, org: null }) + if (url.includes("summary")) return jsonResponse({ totalCost: 3.0, totalCount: 10 }) + if (url.includes("credits")) throw new Error("network down") + if (url.includes("subscriptions")) return jsonResponse({ data: { planId: "pro" } }) + throw new Error(`Unexpected URL: ${url}`) + } + + const result = await fetchCommandCodeQuota({ apiKey: "cc_test_key", fetchImpl }) + assert.equal(result.ok, true) + if (!result.ok) return + assert.equal(result.quota.credits, null) + assert.equal(result.quota.subscription?.planId, "pro") + assert.equal(result.quota.summary?.totalCost, 3.0) + }) + + it("fails the command when the summary endpoint rejects auth/permission", async () => { + const fetchImpl = async (input: RequestInfo | URL): Promise => { + const url = String(input) + if (url.includes("whoami")) return jsonResponse({ user: { userName: "alice" }, org: null }) + if (url.includes("credits")) return jsonResponse({ credits: { monthlyCredits: 5 } }) + if (url.includes("subscriptions")) return jsonResponse({ data: { planId: "pro" } }) + if (url.includes("summary")) return jsonResponse({ error: "nope" }, 403) + throw new Error(`Unexpected URL: ${url}`) + } + const result = await fetchCommandCodeQuota({ apiKey: "cc_test_key", fetchImpl }) + assert.equal(result.ok, false) + if (result.ok) return + assert.equal(result.error.kind, "http") + assert.match(result.error.message, /summary/) + }) + + it("does not treat 429 on billing endpoints as fatal", async () => { + const fetchImpl = async (input: RequestInfo | URL): Promise => { + const url = String(input) + if (url.includes("whoami")) return jsonResponse({ user: { userName: "alice" }, org: null }) + if (url.includes("summary")) return jsonResponse({ totalCost: 3.0, totalCount: 10 }) + if (url.includes("credits") || url.includes("subscriptions")) { + return jsonResponse({ error: "rate limited" }, 429) + } + throw new Error(`Unexpected URL: ${url}`) + } + const result = await fetchCommandCodeQuota({ apiKey: "cc_test_key", fetchImpl }) + assert.equal(result.ok, true) + if (!result.ok) return + assert.equal(result.quota.credits, null) + assert.equal(result.quota.summary?.totalCost, 3.0) + }) + + it("sends extra headers (ZDR) on quota requests", async () => { + let sent: Headers | undefined + const fetchImpl = async (input: RequestInfo | URL, init?: RequestInit): Promise => { + sent = (init?.headers as Headers) ?? undefined + const url = String(input) + if (url.includes("whoami")) return jsonResponse({ user: { userName: "alice" }, org: null }) + if (url.includes("credits")) return jsonResponse({ credits: { monthlyCredits: 5 } }) + if (url.includes("subscriptions")) return jsonResponse({ data: { planId: "pro" } }) + if (url.includes("summary")) return jsonResponse({ totalCost: 1, totalCount: 1 }) + throw new Error(`Unexpected URL: ${url}`) + } + const result = await fetchCommandCodeQuota({ + apiKey: "cc_test_key", + fetchImpl, + extraHeaders: { "x-cmd-zdr": "1" }, + }) + assert.equal(result.ok, true) + const headers = new Headers(sent) + assert.equal(headers.get("x-cmd-zdr"), "1") + }) + + it("parses optional token count and key name when present", async () => { + const { fetchImpl } = okFetch({ + whoami: { user: { userName: "alice", keyName: "Pi Agent" }, org: null }, + credits: { credits: { monthlyCredits: 5, purchasedCredits: 0, freeCredits: 0 } }, + subscriptions: { data: { planId: "pro", status: "active" } }, + summary: { totalCost: 1.06, totalCount: 654, totalTokens: 74_200_000 }, + }) + const result = await fetchCommandCodeQuota({ apiKey: "cc_test_key", fetchImpl }) + assert.equal(result.ok, true) + if (!result.ok) return + assert.equal(result.quota.summary?.totalTokens, 74_200_000) + assert.equal(result.quota.account.keyName, "Pi Agent") + }) + + it("rejects missing API keys as a config error", async () => { + const result = await fetchCommandCodeQuota({ apiKey: "" }) + assert.equal(result.ok, false) + if (result.ok) return + assert.equal(result.error.kind, "config") + }) + + it("fails with a config-style error when the API key is rejected", async () => { + const fetchImpl = async (input: RequestInfo | URL): Promise => { + if (String(input).includes("whoami")) return jsonResponse({ error: "unauthorized" }, 401) + throw new Error(`Unexpected URL: ${input}`) + } + const result = await fetchCommandCodeQuota({ apiKey: "cc_bad_key", fetchImpl }) + assert.equal(result.ok, false) + if (result.ok) return + assert.equal(result.error.kind, "http") + assert.match(result.error.message, /401/) + }) + + it("formats a complete quota snapshot into readable output", () => { + const quota: CommandCodeQuota = { + account: { login: "alice-inc", orgId: "org_1" }, + credits: { + monthlyCredits: 40, + purchasedCredits: 10, + freeCredits: 5, + remainingCredits: 55, + windowLimits: [ + { window: "fiveHour", used: 8, cap: 16, resetAt: null }, + { window: "weekly", used: 20, cap: 40, resetAt: null }, + ], + } satisfies CommandCodeCredits, + subscription: { + planId: "pro", + status: "active", + currentPeriodStart: "", + currentPeriodEnd: "", + }, + summary: { totalCost: 12.34, totalCount: 1500 }, + } + + const output = formatQuota(quota, () => 1_700_000_000_000) + assert.doesNotMatch(output, /Command Code quota —/) + assert.match(output, /Credits/) + assert.match(output, /Remaining: \$55\.00 of \$67\.34/) + assert.match(output, /Used: \$12\.34/) + assert.match(output, /Sources: monthly \$40\.00 \/ purchased \$10\.00 \/ free \$5\.00/) + assert.match(output, /Plan: pro \(active\)/) + assert.match(output, /Usage \(this month\)/) + assert.match(output, /Cost: \$12\.34/) + assert.match(output, /Requests: 1,500/) + assert.match(output, /Username/) + assert.match(output, /alice-inc/) + assert.match(output, /5-hour: 8\.00 \/ 16\.00 credits/) + assert.match(output, /Weekly: 20\.00 \/ 40\.00 credits/) + assert.match(output, /https:\/\/commandcode\.ai\/usage/) + }) + + it("redacts token-like values from error messages", () => { + // 16+ char run after a credential key is redacted by the shared redactor. + assert.equal(redactValue("api_key=abcdefghijklmnop123456"), "api_key=[redacted]") + assert.equal(redactValue("Bearer user_12345678901234 failed"), "Bearer [redacted] failed") + }) + + it("redacts named credential fields and short tokens from error bodies", () => { + // Credential key-value forms (with = or : separator) are redacted. + assert.equal(redactValue("api_key=abc123"), "api_key=[redacted]") + assert.equal( + redactValue("authorization=Basic abc:def failed"), + "authorization=[redacted] abc:def failed", + ) + assert.equal(redactValue("user_123456789 failed"), "[redacted] failed") + assert.equal(redactValue("cc_abcdefghijkl failed"), "[redacted] failed") + assert.equal( + redactValue("token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.secret"), + "token=[redacted]", + ) + }) + + it("redacts JSON-quoted credential fields in error bodies", () => { + assert.equal( + redactValue('{"apiKey":"sk-abcdefghijklmnop123456","ok":true}'), + '{"apiKey":"[redacted]","ok":true}', + ) + assert.equal( + redactValue('{"error":"bad","access_token":"opaque-internal-token-12345"}'), + '{"error":"bad","access_token":"[redacted]"}', + ) + assert.equal( + redactValue('{"authorization":"Bearer user_1234"}'), + '{"authorization":"[redacted]"}', + ) + }) + + it("redacts thrown network errors from the outer catch path", async () => { + const fetchImpl = async (_input: RequestInfo | URL): Promise => { + throw new Error("connection reset by proxy api_key=supersecretvalue123456") + } + const result = await fetchCommandCodeQuota({ apiKey: "cc_test_key", fetchImpl }) + assert.equal(result.ok, false) + if (result.ok) return + assert.doesNotMatch(result.error.message, /supersecretvalue123456/) + assert.match(result.error.kind, /network/) + }) + + it("honors the overall deadline once it has already fired (no phase starts after abort)", async () => { + const start = Date.now() + const fetchImpl = async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const url = String(input) + if (url.includes("whoami")) { + // Never resolve; let the per-request controller abort it at timeoutMs. + return new Promise((_resolve, reject) => { + init?.signal?.addEventListener("abort", () => + reject(Object.assign(new Error("aborted"), { name: "AbortError" })), + ) + }) + } + throw new Error(`Unexpected URL: ${url}`) + } + + const result = await fetchCommandCodeQuota({ apiKey: "cc_test_key", fetchImpl, timeoutMs: 30 }) + const elapsed = Date.now() - start + assert.equal(result.ok, false) + if (result.ok) return + assert.equal(result.error.kind, "timeout") + // The overall deadline governs the whole command; no phase may add ~30ms on top. + assert.ok(elapsed < 200, `elapsed ${elapsed}ms exceeded overall deadline`) + }) +})