feat(quota): add live commandcode-quota dashboard and OMP auth fix
Fetches live account quota from Command Code alpha usage endpoints (whoami, billing/credits, billing/subscriptions, usage/summary) and renders a plain-text dashboard via ui.notify. - Graceful degradation: optional endpoint transport/timeout/parse failures degrade to null sections instead of aborting; only 401/403 are hard failures. 429 is transient, not fatal. - Overall deadline bounds the whole command to QUOTA_TIMEOUT_MS (per-request controllers are chained; post-deadline phases fail fast). - OMP auth: filter unresolved $COMMANDCODE_API_KEY placeholder and fall back to the host resolver via pickCommandCodeApiKey. - ZDR privacy header respected on quota requests. - Redaction reuses redactCommandCodeErrorText plus JSON-quoted credential fields; outer-catch errors are redacted too. - resetAt parsed from seconds, ms, numeric string, or ISO string. - 21 hermetic unit tests wired into npm test (test:quota).
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## Unreleased
|
## 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -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-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-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.
|
Set `COMMANDCODE_ZDR=1` to send Command Code's documented `x-cmd-zdr: 1` zero-data-retention header.
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { join } from "node:path"
|
|||||||
import { getConfiguredApiKey } from "./src/api-key.ts"
|
import { getConfiguredApiKey } from "./src/api-key.ts"
|
||||||
import { createStreamCommandCode } from "./src/core.ts"
|
import { createStreamCommandCode } from "./src/core.ts"
|
||||||
import { calculateCommandCodeCost } from "./src/cost.ts"
|
import { calculateCommandCodeCost } from "./src/cost.ts"
|
||||||
|
import { pickCommandCodeApiKey } from "./src/converters.ts"
|
||||||
import {
|
import {
|
||||||
baseUrlForModel,
|
baseUrlForModel,
|
||||||
DEFAULT_MODELS_URL,
|
DEFAULT_MODELS_URL,
|
||||||
@@ -32,8 +33,19 @@ import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts
|
|||||||
import { normalizeCommandCodeMessage } from "./src/overflow.ts"
|
import { normalizeCommandCodeMessage } from "./src/overflow.ts"
|
||||||
import { MODEL_COSTS, ZERO_MODEL_COST } from "./src/pricing.ts"
|
import { MODEL_COSTS, ZERO_MODEL_COST } from "./src/pricing.ts"
|
||||||
import { createCommandCodeRuntime } from "./src/runtime.ts"
|
import { createCommandCodeRuntime } from "./src/runtime.ts"
|
||||||
|
import { fetchCommandCodeQuota, formatQuota, redactValue } from "./src/quota.ts"
|
||||||
import { createCommandCodeTransportRouter } from "./src/transport.ts"
|
import { createCommandCodeTransportRouter } from "./src/transport.ts"
|
||||||
|
|
||||||
|
const COMMAND_CODE_PROVIDER_ID = "commandcode"
|
||||||
|
|
||||||
|
async function resolveCommandCodeApiKey(ctx: ExtensionCommandContext): Promise<string | undefined> {
|
||||||
|
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<string, string> | undefined {
|
function commandCodeHeaders(): Record<string, string> | undefined {
|
||||||
if (process.env.COMMANDCODE_ZDR === "1") {
|
if (process.env.COMMANDCODE_ZDR === "1") {
|
||||||
return { "x-cmd-zdr": "1" }
|
return { "x-cmd-zdr": "1" }
|
||||||
@@ -118,6 +130,43 @@ export default async function (pi: ExtensionAPI) {
|
|||||||
return normalized ? { message: normalized.message } : undefined
|
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<ProviderConfig, ExtensionCommandContext>(pi, {
|
const runtime = createCommandCodeRuntime<ProviderConfig, ExtensionCommandContext>(pi, {
|
||||||
endpoint: modelsUrl,
|
endpoint: modelsUrl,
|
||||||
cachePath: modelsCachePath,
|
cachePath: modelsCachePath,
|
||||||
|
|||||||
+2
-1
@@ -29,12 +29,13 @@
|
|||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"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",
|
"typecheck": "tsc --noEmit",
|
||||||
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
|
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
|
||||||
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
|
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
|
||||||
"pi:isolated": "node scripts/pi-isolated.mjs",
|
"pi:isolated": "node scripts/pi-isolated.mjs",
|
||||||
"pi:authenticated": "node scripts/pi-authenticated.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: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:api-key": "tsx tests/test-api-key.ts",
|
||||||
"test:models": "tsx tests/test-models.ts",
|
"test:models": "tsx tests/test-models.ts",
|
||||||
|
|||||||
@@ -138,6 +138,29 @@ export function getApiKey(
|
|||||||
return undefined
|
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 {
|
export function textContent(message: { content?: unknown }): string {
|
||||||
return recordArray(message.content)
|
return recordArray(message.content)
|
||||||
.filter((part) => part.type === "text")
|
.filter((part) => part.type === "text")
|
||||||
|
|||||||
+613
@@ -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<string, unknown> {
|
||||||
|
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<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(path: string, params: Record<string, string | undefined>): 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<CommandCodeWindowLimit["window"], string> = {
|
||||||
|
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<CommandCodeQuotaResult> {
|
||||||
|
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<unknown> => {
|
||||||
|
// 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<unknown> => {
|
||||||
|
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()
|
||||||
|
}
|
||||||
@@ -165,7 +165,13 @@ function runOmp(args, timeoutMs = 30_000) {
|
|||||||
try {
|
try {
|
||||||
console.log("[omp-compat] list models through real extension")
|
console.log("[omp-compat] list models through real extension")
|
||||||
modelListRequestCount = 0
|
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)
|
assert.equal(result.code, 0, result.stderr)
|
||||||
const listOutput = result.stdout || result.stderr
|
const listOutput = result.stdout || result.stderr
|
||||||
assert.match(listOutput, /commandcode/)
|
assert.match(listOutput, /commandcode/)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
mapFinishReason,
|
mapFinishReason,
|
||||||
messagesToCC,
|
messagesToCC,
|
||||||
parseStreamEventLine,
|
parseStreamEventLine,
|
||||||
|
pickCommandCodeApiKey,
|
||||||
projectSlugFromPath,
|
projectSlugFromPath,
|
||||||
textContent,
|
textContent,
|
||||||
toJsonSchema,
|
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()", () => {
|
describe("projectSlugFromPath()", () => {
|
||||||
it("matches the official CLI-style slug from an absolute working directory", () => {
|
it("matches the official CLI-style slug from an absolute working directory", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -359,7 +390,7 @@ describe("toJsonSchema()", () => {
|
|||||||
if (!outputProperties || typeof outputProperties !== "object") {
|
if (!outputProperties || typeof outputProperties !== "object") {
|
||||||
throw new Error("expected object properties")
|
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, {
|
assert.deepEqual(Object.getOwnPropertyDescriptor(outputProperties, "__proto__")?.value, {
|
||||||
type: "string",
|
type: "string",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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<string, unknown>) {
|
||||||
|
const urls: string[] = []
|
||||||
|
const fetchImpl = async (input: RequestInfo | URL): Promise<Response> => {
|
||||||
|
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<Response> => {
|
||||||
|
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<Response> => {
|
||||||
|
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<Response> => {
|
||||||
|
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<Response> => {
|
||||||
|
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<Response> => {
|
||||||
|
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<Response> => {
|
||||||
|
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<Response> => {
|
||||||
|
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<Response> => {
|
||||||
|
const url = String(input)
|
||||||
|
if (url.includes("whoami")) {
|
||||||
|
// Never resolve; let the per-request controller abort it at timeoutMs.
|
||||||
|
return new Promise<Response>((_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`)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user