feat(commandcode): quota usage dashboard, CLI-parity request, and connect timeout fixes
- Add CommandCode usage handler mirroring the official CLI /usage
(whoami → credits/subscriptions → summary) with 5h/weekly/monthly
quota rows, registered in services/usage.js and registry config
- Parse commandcode quota rows in ProviderLimits (remainingPercentage + $ unit)
- Match official CLI request shape: x-command-code-version 1.10.0,
User-Agent cli, and config.environment '${platform}-${arch}, Node.js ${version}'
- Fix connect timeout unit confusion: both profile and provider pages
now use ms with a 1s minimum guard (prevents 60ms footgun)
- Fix fetchT0 ReferenceError in base.js error path and log fetch
diagnostics only on upstream failure
- Quota Tracker defaults to the Active account filter
- Ignore .commandcode/ CLI local state
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -86,3 +86,6 @@ graphify-out/*
|
||||
.codegraph/
|
||||
.PR/
|
||||
.next-analyze/*
|
||||
|
||||
# CommandCode CLI local state (auth/taste/projects)
|
||||
.commandcode/
|
||||
|
||||
@@ -136,10 +136,11 @@ export class BaseExecutor {
|
||||
const timeoutMs = await resolveProviderTimeoutMs(this.provider, this.config?.timeoutMs, FETCH_CONNECT_TIMEOUT_MS);
|
||||
const connectTimer = setTimeout(() => connectCtrl.abort(new Error("fetch connect timeout")), timeoutMs);
|
||||
const mergedSignal = signal ? AbortSignal.any([signal, connectCtrl.signal]) : connectCtrl.signal;
|
||||
let fetchT0 = 0;
|
||||
|
||||
try {
|
||||
const bodyStr = JSON.stringify(transformedBody);
|
||||
const fetchT0 = Date.now();
|
||||
fetchT0 = Date.now();
|
||||
dbg("FETCH", `${this.provider.toUpperCase()} → ${url} | body=${bodyStr.length}B | connectTimeout=${timeoutMs}ms`);
|
||||
const response = await proxyAwareFetch(url, {
|
||||
method: "POST",
|
||||
@@ -165,6 +166,11 @@ export class BaseExecutor {
|
||||
clearTimeout(connectTimer);
|
||||
lastError = error;
|
||||
const isConnectTimeout = connectCtrl.signal.aborted && error.name === "AbortError";
|
||||
// Error diagnostic — only logs on actual upstream failure. Distinguishes
|
||||
// undici connect timeout (UND_ERR_CONNECT_TIMEOUT), DNS (ENOTFOUND),
|
||||
// refused (ECONNREFUSED) vs our own connectCtrl abort (AbortError).
|
||||
const cause = error?.cause || {};
|
||||
console.log(`[FETCH-DIAG] ${this.provider} fetch error | name=${error.name} | code=${error.code ?? cause?.code ?? "none"} | msg=${String(error.message).slice(0, 120)} | connectTimeout=${timeoutMs}ms | elapsed=${Date.now() - fetchT0}ms`);
|
||||
dbg("FETCH", `${this.provider.toUpperCase()} ✖ ${error.name}: ${error.message}${isConnectTimeout ? " (connect timeout)" : ""}`);
|
||||
// Connect timeout is internal — convert to retryable network error, don't propagate AbortError
|
||||
if (error.name === "AbortError" && !isConnectTimeout) throw error;
|
||||
|
||||
@@ -23,9 +23,24 @@ export default {
|
||||
format: "commandcode",
|
||||
forceStream: true,
|
||||
headers: {
|
||||
"x-command-code-version": "0.25.7",
|
||||
"x-command-code-version": "1.10.0",
|
||||
"x-cli-environment": "cli",
|
||||
"User-Agent": "cli",
|
||||
},
|
||||
// Quota/billing endpoints (same alpha API the official CLI /usage calls).
|
||||
// whoami resolves orgId; credits+subscription+usage/summary then report the
|
||||
// 5-hour/weekly windows, plan, and period credits. See services/usage/commandcode.js.
|
||||
usage: {
|
||||
baseUrl: "https://api.commandcode.ai",
|
||||
whoamiUrl: "/alpha/whoami",
|
||||
creditsUrl: "/alpha/billing/credits",
|
||||
subscriptionsUrl: "/alpha/billing/subscriptions",
|
||||
summaryUrl: "/alpha/usage/summary",
|
||||
},
|
||||
},
|
||||
features: {
|
||||
usage: true,
|
||||
usageApikey: true,
|
||||
},
|
||||
models: [
|
||||
{ id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro" },
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getXaiUsage } from "./usage/xai.js";
|
||||
import { getGrokCliUsage } from "./usage/grok-cli.js";
|
||||
import { getKimiUsage } from "./usage/kimi.js";
|
||||
import { getDeepseekUsage } from "./usage/deepseek.js";
|
||||
import { getCommandCodeUsage } from "./usage/commandcode.js";
|
||||
import {
|
||||
getQwenUsage,
|
||||
getIflowUsage,
|
||||
@@ -51,6 +52,7 @@ const USAGE_HANDLERS = {
|
||||
"grok-cli": (c) => getGrokCliUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
||||
kimi: (c) => getKimiUsage(c.accessToken, c.apiKey, c.proxyOptions, c.providerSpecificData),
|
||||
deepseek: (c) => getDeepseekUsage(c.apiKey, c.proxyOptions),
|
||||
commandcode: (c) => getCommandCodeUsage(c.apiKey, c.proxyOptions),
|
||||
};
|
||||
|
||||
export async function getUsageForProvider(connection, proxyOptions = null) {
|
||||
|
||||
203
open-sse/services/usage/commandcode.js
Normal file
203
open-sse/services/usage/commandcode.js
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* CommandCode usage handler
|
||||
*
|
||||
* Mirrors the official command-code CLI /usage command: it calls the alpha API
|
||||
* to surface the 5-hour + weekly usage windows, the subscription plan, and the
|
||||
* credits consumed in the current billing period.
|
||||
*
|
||||
* GET /alpha/whoami → org.id (org-scoped billing; null for personal)
|
||||
* GET /alpha/billing/credits → { credits: { monthlyCredits, purchasedCredits,
|
||||
* freeCredits }, windowLimits: { fiveHour, weekly } }
|
||||
* GET /alpha/billing/subscriptions → { data: { planId, currentPeriodStart, ... } }
|
||||
* GET /alpha/usage/summary?since= → period token/cost totals
|
||||
*
|
||||
* The CLI fetches whoami first (for orgId), then credits + subscription in
|
||||
* parallel, then the summary with since = currentPeriodStart. We keep the same
|
||||
* order/dependencies: window limits live on credits, and the plan period start
|
||||
* determines the summary window.
|
||||
*/
|
||||
|
||||
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
|
||||
import { U, parseResetTime } from "./shared.js";
|
||||
|
||||
const USAGE = U("commandcode");
|
||||
const BASE = USAGE.baseUrl || "https://api.commandcode.ai";
|
||||
const WHOAMI_URL = BASE + (USAGE.whoamiUrl || "/alpha/whoami");
|
||||
const CREDITS_URL = BASE + (USAGE.creditsUrl || "/alpha/billing/credits");
|
||||
const SUBSCRIPTIONS_URL =
|
||||
BASE + (USAGE.subscriptionsUrl || "/alpha/billing/subscriptions");
|
||||
const SUMMARY_URL = BASE + (USAGE.summaryUrl || "/alpha/usage/summary");
|
||||
|
||||
function buildHeaders(token) {
|
||||
return {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
/** Build a normalized quota row. `unit` is "$" — the API reports currency credits. */
|
||||
function makeQuota({ used, total, resetAt, unlimited = false, unit = "$" }) {
|
||||
const safeTotal = Math.max(0, Number(total) || 0);
|
||||
const safeUsed = Math.max(0, Number(used) || 0);
|
||||
if (unlimited || safeTotal === 0) {
|
||||
return {
|
||||
used: safeUsed,
|
||||
total: 0,
|
||||
remainingPercentage: unlimited ? 100 : 0,
|
||||
resetAt: resetAt || null,
|
||||
unit,
|
||||
unlimited: true,
|
||||
};
|
||||
}
|
||||
const remaining = Math.max(0, safeTotal - safeUsed);
|
||||
const remainingPercentage = (remaining / safeTotal) * 100;
|
||||
return {
|
||||
used: safeUsed,
|
||||
total: safeTotal,
|
||||
remainingPercentage,
|
||||
resetAt: resetAt || null,
|
||||
unit,
|
||||
unlimited: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} apiKey - commandcode API key (user_...)
|
||||
* @param {object|null} proxyOptions
|
||||
*/
|
||||
export async function getCommandCodeUsage(apiKey, proxyOptions = null) {
|
||||
if (!apiKey) {
|
||||
return { message: "CommandCode credential not available." };
|
||||
}
|
||||
|
||||
const headers = buildHeaders(apiKey);
|
||||
|
||||
try {
|
||||
// whoami resolves the org id (billing is org-scoped; null for personal).
|
||||
const whoamiRes = await proxyAwareFetch(
|
||||
WHOAMI_URL,
|
||||
{ method: "GET", headers },
|
||||
proxyOptions,
|
||||
);
|
||||
if (whoamiRes.status === 401 || whoamiRes.status === 403) {
|
||||
return { message: "CommandCode credential invalid or expired." };
|
||||
}
|
||||
if (!whoamiRes.ok) {
|
||||
return { message: `CommandCode whoami API error (${whoamiRes.status}).` };
|
||||
}
|
||||
const whoami = await whoamiRes.json().catch(() => null);
|
||||
const orgId = whoami?.org?.id ?? null;
|
||||
|
||||
const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
|
||||
|
||||
const [creditsRes, subsRes] = await Promise.all([
|
||||
proxyAwareFetch(
|
||||
CREDITS_URL + orgQuery,
|
||||
{ method: "GET", headers },
|
||||
proxyOptions,
|
||||
),
|
||||
proxyAwareFetch(
|
||||
SUBSCRIPTIONS_URL + orgQuery,
|
||||
{ method: "GET", headers },
|
||||
proxyOptions,
|
||||
),
|
||||
]);
|
||||
|
||||
if (
|
||||
creditsRes.status === 401 ||
|
||||
creditsRes.status === 403 ||
|
||||
subsRes.status === 401 ||
|
||||
subsRes.status === 403
|
||||
) {
|
||||
return { message: "CommandCode credential invalid or expired." };
|
||||
}
|
||||
if (!creditsRes.ok) {
|
||||
return {
|
||||
message: `CommandCode credits API error (${creditsRes.status}).`,
|
||||
};
|
||||
}
|
||||
|
||||
const credits = await creditsRes.json().catch(() => null);
|
||||
const subs = await subsRes.json().catch(() => null);
|
||||
|
||||
const subData = subs?.data;
|
||||
const planId = subData?.planId ?? null;
|
||||
const periodStart = subData?.currentPeriodStart ?? null;
|
||||
|
||||
// Summary needs `since`; the CLI falls back to first-of-month when the
|
||||
// subscription period start is unavailable.
|
||||
const since = periodStart || firstOfMonth();
|
||||
const summaryRes = await proxyAwareFetch(
|
||||
`${SUMMARY_URL}?since=${encodeURIComponent(since)}`,
|
||||
{ method: "GET", headers },
|
||||
proxyOptions,
|
||||
);
|
||||
const summary = summaryRes.ok
|
||||
? await summaryRes.json().catch(() => null)
|
||||
: null;
|
||||
|
||||
const quotas = {};
|
||||
const windowLimits = credits?.windowLimits || {};
|
||||
|
||||
const fiveHour = windowLimits.fiveHour;
|
||||
if (fiveHour && Number(fiveHour.cap) > 0) {
|
||||
quotas["5-hour window"] = makeQuota({
|
||||
used: fiveHour.used,
|
||||
total: fiveHour.cap,
|
||||
resetAt: parseResetTime(fiveHour.resetAt),
|
||||
});
|
||||
}
|
||||
|
||||
const weekly = windowLimits.weekly;
|
||||
if (weekly && Number(weekly.cap) > 0) {
|
||||
quotas["Weekly window"] = makeQuota({
|
||||
used: weekly.used,
|
||||
total: weekly.cap,
|
||||
resetAt: parseResetTime(weekly.resetAt),
|
||||
});
|
||||
}
|
||||
|
||||
// Monthly credits consumed this billing period (from summary when present,
|
||||
// else the credits object's monthlyCredits as a fallback).
|
||||
const monthlyUsed =
|
||||
typeof summary?.totalCredits === "number"
|
||||
? summary.totalCredits
|
||||
: typeof credits?.credits?.monthlyCredits === "number"
|
||||
? credits.credits.monthlyCredits
|
||||
: 0;
|
||||
|
||||
const monthlyTotal =
|
||||
typeof credits?.credits?.monthlyCredits === "number"
|
||||
? credits.credits.monthlyCredits
|
||||
: 0;
|
||||
|
||||
if (monthlyTotal > 0 || monthlyUsed > 0) {
|
||||
quotas["Monthly credits"] = makeQuota({
|
||||
used: monthlyUsed,
|
||||
total: monthlyTotal,
|
||||
resetAt: periodStart ? undefined : null,
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(quotas).length === 0) {
|
||||
return {
|
||||
plan: planId || "CommandCode",
|
||||
message: "CommandCode connected, but no quota was reported.",
|
||||
quotas: {},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
plan: planId || "CommandCode",
|
||||
quotas,
|
||||
periodBasis: summary?.periodBasis || "billing-period",
|
||||
};
|
||||
} catch (error) {
|
||||
return { message: `CommandCode usage error: ${error.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
function firstOfMonth() {
|
||||
const now = new Date();
|
||||
return new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
|
||||
}
|
||||
@@ -191,13 +191,17 @@ export function openaiToCommandCodeRequest(
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
// environment format mirrors the official command-code CLI (getEnvironmentInfo):
|
||||
// `${platform}-${arch}, Node.js ${version}` (e.g. "darwin-arm64, Node.js v24.16.0").
|
||||
const environment = `${process.platform}-${process.arch}, Node.js ${process.version}`;
|
||||
|
||||
return {
|
||||
threadId: randomUUID(),
|
||||
memory: "",
|
||||
config: {
|
||||
workingDir: process.cwd(),
|
||||
date: today,
|
||||
environment: process.platform,
|
||||
environment,
|
||||
structure: [],
|
||||
isGitRepo: false,
|
||||
currentBranch: "",
|
||||
|
||||
@@ -288,9 +288,11 @@ export default function ProfilePage() {
|
||||
const handleGlobalTimeoutChange = async (e) => {
|
||||
const raw = e.target.value.replace(/[^0-9]/g, "");
|
||||
const numTimeout = parseInt(raw, 10);
|
||||
// Enforce a sane minimum (1s) so a stray "60" never becomes a 60ms
|
||||
// connect timeout — same guard as the per-provider timeout input.
|
||||
const patchValue =
|
||||
raw !== "" && Number.isFinite(numTimeout) && numTimeout > 0
|
||||
? numTimeout
|
||||
? Math.max(1000, numTimeout)
|
||||
: null;
|
||||
|
||||
try {
|
||||
|
||||
@@ -438,7 +438,7 @@ export default function ProviderDetailPage() {
|
||||
const thinkingCfg =
|
||||
(settingsData.providerThinking || {})[providerId] || {};
|
||||
setThinkingMode(thinkingCfg.mode || "auto");
|
||||
// Load per-provider connect timeout
|
||||
// Load per-provider connect timeout (ms)
|
||||
const timeoutCfg =
|
||||
(settingsData.providerTimeouts || {})[providerId] || {};
|
||||
setProviderTimeout(
|
||||
@@ -577,15 +577,13 @@ export default function ProviderDetailPage() {
|
||||
const settingsData = settingsRes.ok ? await settingsRes.json() : {};
|
||||
const current = settingsData.providerTimeouts || {};
|
||||
const updated = { ...current };
|
||||
if (!ms || ms === "") {
|
||||
delete updated[providerId];
|
||||
const timeoutMs = parseInt(ms, 10);
|
||||
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
||||
// Stored in ms; enforce a sane minimum (1s) so a stray "60" means
|
||||
// 60 SECONDS-worth of protection — never a 60ms connect timeout.
|
||||
updated[providerId] = { timeoutMs: Math.max(1000, timeoutMs) };
|
||||
} else {
|
||||
const timeoutMs = parseInt(ms, 10);
|
||||
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
||||
updated[providerId] = { timeoutMs };
|
||||
} else {
|
||||
delete updated[providerId];
|
||||
}
|
||||
delete updated[providerId];
|
||||
}
|
||||
await fetch("/api/settings", {
|
||||
method: "PATCH",
|
||||
|
||||
@@ -147,7 +147,7 @@ export default function ProviderLimits() {
|
||||
const [proxyPools, setProxyPools] = useState([]);
|
||||
const [providerFilter, setProviderFilter] = useState("all");
|
||||
const [providerOptions, setProviderOptions] = useState([]);
|
||||
const [accountFilter, setAccountFilter] = useState("all");
|
||||
const [accountFilter, setAccountFilter] = useState("active");
|
||||
const [quotaSortMode, setQuotaSortMode] = useState("default");
|
||||
const [quotaVisibility, setQuotaVisibility] = useState({});
|
||||
const [expiringFirst, setExpiringFirst] = useState(false);
|
||||
|
||||
@@ -564,6 +564,24 @@ export function parseQuotaData(provider, data) {
|
||||
}
|
||||
break;
|
||||
|
||||
case "commandcode":
|
||||
// CommandCode reports currency credits (5-hour/weekly windows + monthly
|
||||
// credits) with used/total in dollars. Forward remainingPercentage (the
|
||||
// UI would otherwise render "$0.05" balances as "0%") and unit "$".
|
||||
if (data.quotas) {
|
||||
Object.entries(data.quotas).forEach(([name, quota]) => {
|
||||
normalizedQuotas.push({
|
||||
name,
|
||||
used: quota.used || 0,
|
||||
total: quota.total || 0,
|
||||
unit: quota.unit || "$",
|
||||
resetAt: quota.resetAt || null,
|
||||
remainingPercentage: quota.remainingPercentage,
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Generic fallback for unknown providers
|
||||
if (data.quotas) {
|
||||
|
||||
218
tests/unit/commandcode-usage.test.js
Normal file
218
tests/unit/commandcode-usage.test.js
Normal file
@@ -0,0 +1,218 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
|
||||
proxyAwareFetch: vi.fn(),
|
||||
}));
|
||||
|
||||
import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js";
|
||||
import { getUsageForProvider } from "../../open-sse/services/usage.js";
|
||||
import {
|
||||
USAGE_SUPPORTED_PROVIDERS,
|
||||
USAGE_APIKEY_PROVIDERS,
|
||||
} from "../../src/shared/constants/providers.js";
|
||||
import { parseQuotaData } from "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
|
||||
|
||||
const BASE = "https://api.commandcode.ai";
|
||||
const WHOAMI_URL = `${BASE}/alpha/whoami`;
|
||||
const CREDITS_URL = `${BASE}/alpha/billing/credits`;
|
||||
const SUBS_URL = `${BASE}/alpha/billing/subscriptions`;
|
||||
const SUMMARY_URL = `${BASE}/alpha/usage/summary`;
|
||||
|
||||
function jsonResponse(body, status = 200) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const WHOAMI = { success: true, user: { id: "u1" }, org: null };
|
||||
const CREDITS = {
|
||||
credits: {
|
||||
belowThreshold: false,
|
||||
creditThreshold: 0,
|
||||
monthlyCredits: 9.9,
|
||||
purchasedCredits: 0,
|
||||
freeCredits: 0,
|
||||
},
|
||||
windowLimits: {
|
||||
limited: true,
|
||||
exceeded: null,
|
||||
fiveHour: { used: 0.05, cap: 3, exceeded: false, resetAt: 1785812386064 },
|
||||
weekly: { used: 0.1, cap: 6, exceeded: false, resetAt: 1786379982640 },
|
||||
},
|
||||
};
|
||||
const SUBS = {
|
||||
success: true,
|
||||
data: {
|
||||
id: "sub_1",
|
||||
status: "active",
|
||||
orgId: null,
|
||||
planId: "individual-go",
|
||||
currentPeriodStart: "2026-08-03T16:38:16.000Z",
|
||||
currentPeriodEnd: "2026-09-03T16:38:16.000Z",
|
||||
},
|
||||
};
|
||||
const SUMMARY = {
|
||||
totalCount: 61,
|
||||
totalCost: 0.1,
|
||||
totalCredits: 0.1,
|
||||
totalMonthlyCredits: 0.1,
|
||||
periodBasis: "billing-period",
|
||||
};
|
||||
|
||||
describe("commandcode registry usage flags", () => {
|
||||
it("is listed for apikey quota dashboard", () => {
|
||||
expect(USAGE_SUPPORTED_PROVIDERS).toContain("commandcode");
|
||||
expect(USAGE_APIKEY_PROVIDERS).toContain("commandcode");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUsageForProvider(commandcode)", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("fetches whoami → credits+subs → summary and maps windows + credits", async () => {
|
||||
proxyAwareFetch
|
||||
.mockResolvedValueOnce(jsonResponse(WHOAMI))
|
||||
.mockResolvedValueOnce(jsonResponse(CREDITS))
|
||||
.mockResolvedValueOnce(jsonResponse(SUBS))
|
||||
.mockResolvedValueOnce(jsonResponse(SUMMARY));
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "commandcode",
|
||||
apiKey: "user_cc_test",
|
||||
});
|
||||
|
||||
expect(usage.message).toBeUndefined();
|
||||
expect(usage.plan).toBe("individual-go");
|
||||
expect(usage.periodBasis).toBe("billing-period");
|
||||
|
||||
expect(proxyAwareFetch).toHaveBeenCalledTimes(4);
|
||||
const [whoamiUrl, whoamiOpts] = proxyAwareFetch.mock.calls[0];
|
||||
expect(whoamiUrl).toBe(WHOAMI_URL);
|
||||
expect(whoamiOpts.headers.Authorization).toBe("Bearer user_cc_test");
|
||||
|
||||
// No org → credits/subscriptions called without orgId query
|
||||
const creditsCall = proxyAwareFetch.mock.calls[1][0];
|
||||
expect(creditsCall).toBe(CREDITS_URL);
|
||||
|
||||
// Summary uses currentPeriodStart as `since`
|
||||
const summaryCall = proxyAwareFetch.mock.calls[3][0];
|
||||
expect(summaryCall).toBe(
|
||||
`${SUMMARY_URL}?since=${encodeURIComponent("2026-08-03T16:38:16.000Z")}`,
|
||||
);
|
||||
|
||||
expect(usage.quotas["5-hour window"]).toMatchObject({
|
||||
used: 0.05,
|
||||
total: 3,
|
||||
resetAt: new Date(1785812386064).toISOString(),
|
||||
});
|
||||
expect(usage.quotas["Weekly window"]).toMatchObject({
|
||||
used: 0.1,
|
||||
total: 6,
|
||||
resetAt: new Date(1786379982640).toISOString(),
|
||||
});
|
||||
expect(usage.quotas["Monthly credits"]).toMatchObject({
|
||||
used: 0.1,
|
||||
total: 9.9,
|
||||
});
|
||||
});
|
||||
|
||||
it("adds orgId query when whoami returns an org", async () => {
|
||||
proxyAwareFetch
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse({ success: true, org: { id: "org_1" } }),
|
||||
)
|
||||
.mockResolvedValueOnce(jsonResponse(CREDITS))
|
||||
.mockResolvedValueOnce(jsonResponse(SUBS))
|
||||
.mockResolvedValueOnce(jsonResponse(SUMMARY));
|
||||
|
||||
await getUsageForProvider({
|
||||
provider: "commandcode",
|
||||
apiKey: "user_cc_test",
|
||||
});
|
||||
|
||||
expect(proxyAwareFetch.mock.calls[1][0]).toBe(`${CREDITS_URL}?orgId=org_1`);
|
||||
expect(proxyAwareFetch.mock.calls[2][0]).toBe(`${SUBS_URL}?orgId=org_1`);
|
||||
});
|
||||
|
||||
it("falls back to first-of-month since when subscription has no period start", async () => {
|
||||
proxyAwareFetch
|
||||
.mockResolvedValueOnce(jsonResponse(WHOAMI))
|
||||
.mockResolvedValueOnce(jsonResponse(CREDITS))
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse({ success: true, data: { planId: "individual-go" } }),
|
||||
)
|
||||
.mockResolvedValueOnce(jsonResponse(SUMMARY));
|
||||
|
||||
await getUsageForProvider({
|
||||
provider: "commandcode",
|
||||
apiKey: "user_cc_test",
|
||||
});
|
||||
|
||||
const since = new URL(proxyAwareFetch.mock.calls[3][0]).searchParams.get(
|
||||
"since",
|
||||
);
|
||||
// firstOfMonth() is local-time based; assert the local date is the 1st.
|
||||
const localDate = new Date(since);
|
||||
expect(localDate.getDate()).toBe(1);
|
||||
});
|
||||
|
||||
it("returns message on missing key / 401 / non-ok whoami", async () => {
|
||||
const missing = await getUsageForProvider({ provider: "commandcode" });
|
||||
expect(missing.message).toMatch(/credential/i);
|
||||
expect(proxyAwareFetch).not.toHaveBeenCalled();
|
||||
|
||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "no" }, 401));
|
||||
const auth = await getUsageForProvider({
|
||||
provider: "commandcode",
|
||||
apiKey: "bad",
|
||||
});
|
||||
expect(auth.message).toMatch(/invalid|expired/i);
|
||||
|
||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "x" }, 500));
|
||||
const err = await getUsageForProvider({
|
||||
provider: "commandcode",
|
||||
apiKey: "bad",
|
||||
});
|
||||
expect(err.message).toMatch(/whoami/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseQuotaData(commandcode)", () => {
|
||||
it("forwards remainingPercentage + unit for window/credit rows", () => {
|
||||
const rows = parseQuotaData("commandcode", {
|
||||
plan: "individual-go",
|
||||
quotas: {
|
||||
"5-hour window": {
|
||||
used: 0.05,
|
||||
total: 3,
|
||||
remainingPercentage: 98.33,
|
||||
resetAt: "2026-08-03T22:59:46.064Z",
|
||||
unit: "$",
|
||||
},
|
||||
"Monthly credits": {
|
||||
used: 0.1,
|
||||
total: 9.9,
|
||||
remainingPercentage: 98.99,
|
||||
resetAt: null,
|
||||
unit: "$",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(rows[0]).toMatchObject({
|
||||
name: "5-hour window",
|
||||
used: 0.05,
|
||||
total: 3,
|
||||
remainingPercentage: 98.33,
|
||||
unit: "$",
|
||||
});
|
||||
expect(rows[1]).toMatchObject({
|
||||
name: "Monthly credits",
|
||||
used: 0.1,
|
||||
total: 9.9,
|
||||
remainingPercentage: 98.99,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user