fix(quota): improve Grok credential quota accuracy and subscription details
- Keep weekly usage separate from monthly spending - Enrich subscription plans in the background with stale-result guards - Display prepaid balances and hide empty monthly quota rows - Add localized labels and regression coverage
This commit is contained in:
1 parent
8eab980d72
commit
b5189bb436
14 files changed
+217
-18
No files matched your search
@@ -1927,6 +1927,7 @@ export const jaOverrides = {
|
||||
'quota.service.limit.numbered': '制限 {index}',
|
||||
'quota.service.product.numbered': '製品 {index}',
|
||||
'quota.service.monthlyIncluded': '月間内包クォータ',
|
||||
'quota.service.prepaidBalance': '前払い残高',
|
||||
'quota.service.onDemand': '従量課金クォータ',
|
||||
'quota.service.quota': 'クォータ',
|
||||
'quota.service.quota.numbered': 'クォータ {index}',
|
||||
|
||||
@@ -1927,6 +1927,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'quota.service.limit.numbered': 'Limit {index}',
|
||||
'quota.service.product.numbered': 'Product {index}',
|
||||
'quota.service.monthlyIncluded': 'Monthly included quota',
|
||||
'quota.service.prepaidBalance': 'Prepaid balance',
|
||||
'quota.service.onDemand': 'On-demand quota',
|
||||
'quota.service.quota': 'Quota',
|
||||
'quota.service.quota.numbered': 'Quota {index}',
|
||||
|
||||
@@ -1926,6 +1926,7 @@ export const zhCN = {
|
||||
'quota.service.limit.numbered': '限制 {index}',
|
||||
'quota.service.product.numbered': '产品 {index}',
|
||||
'quota.service.monthlyIncluded': '月度包含额度',
|
||||
'quota.service.prepaidBalance': '预付余额',
|
||||
'quota.service.onDemand': '按量付费额度',
|
||||
'quota.service.quota': '配额',
|
||||
'quota.service.quota.numbered': '配额 {index}',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { commitFetchedQuota } from '../services/quotaEnrichment';
|
||||
import { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useConfirmation } from '../components/ConfirmationDialog';
|
||||
import { QuotaActionFeedback } from '../components/QuotaActionFeedback';
|
||||
@@ -49,7 +50,6 @@ import {
|
||||
} from '../services/quotaService';
|
||||
import {
|
||||
captureQuotaCacheGeneration,
|
||||
commitQuotaCacheIfCurrent,
|
||||
getQuotaCacheSnapshot,
|
||||
pruneQuotaCache,
|
||||
updateQuotaCache,
|
||||
@@ -240,9 +240,7 @@ export function AuthFileManagementPage() {
|
||||
const cacheGeneration = captureQuotaCacheGeneration();
|
||||
updateQuotaCache((current) => ({ ...current, [key]: { status: 'loading', rows: [] } }));
|
||||
const result = await loadQuota(file);
|
||||
commitQuotaCacheIfCurrent(cacheGeneration, () => {
|
||||
updateQuotaCache((current) => ({ ...current, [key]: result }));
|
||||
});
|
||||
commitFetchedQuota(cacheGeneration, file, result);
|
||||
};
|
||||
|
||||
const refreshAllQuotas = async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { commitFetchedQuota } from '../services/quotaEnrichment';
|
||||
import { ResetCreditExpiries } from '../components/ResetCreditExpiries';
|
||||
import { MessageNotice } from '../appNotice';
|
||||
import { useCallback, useEffect, useMemo, useState, type KeyboardEvent } from 'react';
|
||||
@@ -28,7 +29,6 @@ import {
|
||||
} from '../services/quotaService';
|
||||
import {
|
||||
captureQuotaCacheGeneration,
|
||||
commitQuotaCacheIfCurrent,
|
||||
getQuotaCacheSnapshot,
|
||||
pruneQuotaCache,
|
||||
updateQuotaCache,
|
||||
@@ -96,9 +96,7 @@ export function QuotaPage() {
|
||||
const cacheGeneration = captureQuotaCacheGeneration();
|
||||
updateQuotaCache((current) => ({ ...current, [key]: { status: 'loading', rows: [] } }));
|
||||
const result = await loadQuota(file);
|
||||
commitQuotaCacheIfCurrent(cacheGeneration, () => {
|
||||
updateQuotaCache((current) => ({ ...current, [key]: result }));
|
||||
});
|
||||
commitFetchedQuota(cacheGeneration, file, result);
|
||||
}, [t]);
|
||||
|
||||
const resetQuota = useQuotaReset(askConfirmation, setError);
|
||||
@@ -119,9 +117,7 @@ export function QuotaPage() {
|
||||
const batch = files.slice(index, index + REFRESH_CONCURRENCY);
|
||||
await Promise.all(batch.map(async (file) => {
|
||||
const result = await loadQuota(file);
|
||||
commitQuotaCacheIfCurrent(cacheGeneration, () => {
|
||||
updateQuotaCache((current) => ({ ...current, [quotaKey(file)]: result }));
|
||||
});
|
||||
commitFetchedQuota(cacheGeneration, file, result);
|
||||
}));
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { enrichXaiQuotaPlan, providerForFile, quotaKey, type AuthFile, type QuotaState } from './quotaService';
|
||||
import { commitQuotaCacheIfCurrent, getQuotaCacheSnapshot, updateQuotaCache } from './quotaCache';
|
||||
|
||||
export function commitFetchedQuota(generation: number, file: AuthFile, quota: QuotaState) {
|
||||
const key = quotaKey(file);
|
||||
const committed = commitQuotaCacheIfCurrent(generation, () => {
|
||||
updateQuotaCache((current) => ({ ...current, [key]: quota }));
|
||||
});
|
||||
if (committed && quota.status === 'success' && providerForFile(file) === 'xai') {
|
||||
void enrichXaiQuotaPlan(file, quota).then((enriched) => {
|
||||
if (enriched === quota || getQuotaCacheSnapshot()[key] !== quota) return;
|
||||
commitQuotaCacheIfCurrent(generation, () => {
|
||||
updateQuotaCache((current) => ({ ...current, [key]: enriched }));
|
||||
});
|
||||
}).catch(() => { /* Optional plan failures preserve the original quota. */ });
|
||||
}
|
||||
}
|
||||
@@ -527,8 +527,8 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
|
||||
reset: absoluteResetLabel(billing.periodEnd),
|
||||
resetAtMs: billing.resetAtMs ?? undefined,
|
||||
};
|
||||
if (billing.periodType === 'weekly'
|
||||
&& (billing.usagePercent !== null || billing.periodEnd || billing.productUsage.length > 0)) {
|
||||
const hasWeeklyData = billing.periodType === 'weekly';
|
||||
if (hasWeeklyData) {
|
||||
rows.push({
|
||||
label: quotaText('quota.service.weekly'),
|
||||
remainingPercent: remainingFromUsedPercent(billing.usagePercent),
|
||||
@@ -555,7 +555,8 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
|
||||
detail: amount(billing.onDemandCapCents, billing.onDemandUsedCents),
|
||||
});
|
||||
}
|
||||
if (billing.monthlyLimitCents !== null || billing.usedCents !== null || billing.billingPeriodEnd) {
|
||||
if ((billing.monthlyLimitCents !== null || billing.usedCents !== null || billing.billingPeriodEnd)
|
||||
&& !(hasWeeklyData && billing.monthlyLimitCents === 0 && billing.usedCents === 0)) {
|
||||
rows.push({
|
||||
label: quotaText('quota.service.monthlyIncluded'),
|
||||
remainingPercent: remainingFromUsedPercent(billing.usedPercent),
|
||||
@@ -563,6 +564,10 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
|
||||
...monthlyReset,
|
||||
});
|
||||
}
|
||||
if (billing.prepaidBalanceCents !== null && billing.prepaidBalanceCents > 0) {
|
||||
rows.push({ label: quotaText('quota.service.prepaidBalance'), remainingPercent: null,
|
||||
detail: formatUsdFromCents(billing.prepaidBalanceCents) });
|
||||
}
|
||||
return rows.length > 0 ? rows : [{
|
||||
label: quotaText(billing.periodType === 'weekly'
|
||||
? 'quota.service.weekly' : 'quota.service.monthlyIncluded'),
|
||||
@@ -709,6 +714,43 @@ const callXaiQuota = async (file: AuthFile): Promise<unknown> => {
|
||||
}
|
||||
};
|
||||
|
||||
// Optional subscription requests run after the quota has been committed to the shared cache.
|
||||
export const enrichXaiQuotaPlan = async (file: AuthFile, quota: QuotaState): Promise<QuotaState> => {
|
||||
if (providerForFile(file) !== 'xai' || quota.status !== 'success') return quota;
|
||||
const authIndex = normalizeAuthIndex(file.auth_index ?? file.authIndex);
|
||||
if (!authIndex) return quota;
|
||||
const header = { ...headersByProvider.xai };
|
||||
const userId = resolveXaiUserId(file);
|
||||
if (userId) header['x-userid'] = userId;
|
||||
const [user, settings] = await Promise.allSettled([
|
||||
requestQuotaPayload(authIndex, 'https://cli-chat-proxy.grok.com/v1/user?include=subscription', header, 'GET', undefined, 8_000),
|
||||
requestQuotaPayload(authIndex, 'https://cli-chat-proxy.grok.com/v1/settings', header, 'GET', undefined, 8_000),
|
||||
]);
|
||||
const readPlan = (result: PromiseSettledResult<unknown>, keys: string[]) => {
|
||||
if (result.status !== 'fulfilled' || !isRecord(result.value)) return '';
|
||||
for (const key of keys) {
|
||||
const value = result.value[key];
|
||||
if (typeof value === 'string' && value.trim()) return value.trim();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const plan = readPlan(settings, ['subscription_tier_display', 'subscriptionTierDisplay'])
|
||||
|| readPlan(user, ['subscriptionTier', 'subscription_tier']);
|
||||
return plan ? { ...quota, plan } : quota;
|
||||
};
|
||||
|
||||
const xaiBillingPlan = (payload: unknown): string | undefined => {
|
||||
if (!isRecord(payload)) return undefined;
|
||||
const configs = [payload.monthly, payload.weekly, payload].filter(isRecord)
|
||||
.map((record) => isRecord(record.config) ? record.config : record);
|
||||
for (const config of configs) {
|
||||
const limit = buildXaiBillingSummary(config as XaiBillingConfig)?.monthlyLimitCents;
|
||||
if (limit === 150_000) return 'SuperGrok Heavy';
|
||||
if (limit === 15_000) return 'SuperGrok';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const booleanValue = (value: unknown): boolean | null => {
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (typeof value === 'number') return Number.isFinite(value) ? value !== 0 : null;
|
||||
@@ -923,6 +965,7 @@ async function loadQuotaSnapshot(file: AuthFile): Promise<QuotaState> {
|
||||
status: 'success',
|
||||
rows,
|
||||
plan: (provider === 'devin' ? readDevinQuota(payload).plan : detectedPlan)
|
||||
?? (provider === 'xai' ? xaiBillingPlan(payload) : undefined)
|
||||
?? (readString(isRecord(payload) ? payload : {}, 'plan_type', 'planType') || codexMetadata?.plan),
|
||||
creditBalance: accountCredits.balance,
|
||||
creditsUnlimited: accountCredits.unlimited,
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface XaiBillingConfig {
|
||||
monthlyLimit?: XaiBillingCent | number | string | null;
|
||||
monthly_limit?: XaiBillingCent | number | string | null;
|
||||
used?: XaiBillingCent | number | string | null;
|
||||
prepaidBalance?: XaiBillingCent | number | string | null;
|
||||
prepaid_balance?: XaiBillingCent | number | string | null;
|
||||
onDemandCap?: XaiBillingCent | number | string | null;
|
||||
on_demand_cap?: XaiBillingCent | number | string | null;
|
||||
onDemandUsed?: XaiBillingCent | number | string | null;
|
||||
@@ -59,6 +61,7 @@ export interface XaiBillingSummary {
|
||||
productUsage: XaiProductUsageSummary[];
|
||||
monthlyLimitCents: number | null;
|
||||
usedCents: number | null;
|
||||
prepaidBalanceCents: number | null;
|
||||
includedUsedCents: number | null;
|
||||
onDemandCapCents: number | null;
|
||||
onDemandUsedCents: number | null;
|
||||
@@ -154,6 +157,7 @@ const emptyXaiBillingSummary = (): XaiBillingSummary => ({
|
||||
productUsage: [],
|
||||
monthlyLimitCents: null,
|
||||
usedCents: null,
|
||||
prepaidBalanceCents: null,
|
||||
includedUsedCents: null,
|
||||
onDemandCapCents: null,
|
||||
onDemandUsedCents: null,
|
||||
@@ -200,6 +204,7 @@ export function buildXaiBillingSummary(
|
||||
|
||||
const monthlyLimitCents = normalizeXaiCentValue(config.monthlyLimit ?? config.monthly_limit);
|
||||
const usedCents = normalizeXaiCentValue(config.used);
|
||||
const prepaidBalanceCents = normalizeXaiCentValue(config.prepaidBalance ?? config.prepaid_balance);
|
||||
const onDemandCapCents = normalizeXaiCentValue(config.onDemandCap ?? config.on_demand_cap);
|
||||
const explicitOnDemandUsedCents = normalizeXaiCentValue(
|
||||
config.onDemandUsed ?? config.on_demand_used
|
||||
@@ -236,7 +241,7 @@ export function buildXaiBillingSummary(
|
||||
usedCents !== null ||
|
||||
(!hasWeeklyData && (onDemandCapCents !== null || !!billingPeriodEnd));
|
||||
|
||||
if (!hasWeeklyData && !hasMonthlyData) return null;
|
||||
if (!hasWeeklyData && !hasMonthlyData && prepaidBalanceCents === null) return null;
|
||||
|
||||
summary.periodType = hasWeeklyData
|
||||
? periodType === 'unknown'
|
||||
@@ -249,6 +254,7 @@ export function buildXaiBillingSummary(
|
||||
summary.productUsage = productUsage;
|
||||
summary.monthlyLimitCents = monthlyLimitCents;
|
||||
summary.usedCents = usedCents;
|
||||
summary.prepaidBalanceCents = prepaidBalanceCents;
|
||||
summary.includedUsedCents = includedUsedCents;
|
||||
summary.onDemandCapCents = onDemandCapCents;
|
||||
summary.onDemandUsedCents = onDemandUsedCents;
|
||||
@@ -285,7 +291,8 @@ export function mergeXaiBillingSummaries(
|
||||
mode: 'billing',
|
||||
source: 'cli-chat-proxy',
|
||||
periodType: periodSummary.periodType,
|
||||
usagePercent: primary.usagePercent ?? fallback.usagePercent,
|
||||
// Usage belongs to the same period as its dates; monthly spending cannot fill a weekly gap.
|
||||
usagePercent: periodSummary.usagePercent,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
resetAtMs: periodInstants.resetAtMs,
|
||||
@@ -293,6 +300,7 @@ export function mergeXaiBillingSummaries(
|
||||
productUsage: primary.productUsage.length > 0 ? primary.productUsage : fallback.productUsage,
|
||||
monthlyLimitCents: primary.monthlyLimitCents ?? fallback.monthlyLimitCents,
|
||||
usedCents: primary.usedCents ?? fallback.usedCents,
|
||||
prepaidBalanceCents: primary.prepaidBalanceCents ?? fallback.prepaidBalanceCents,
|
||||
includedUsedCents: primary.includedUsedCents ?? fallback.includedUsedCents,
|
||||
onDemandCapCents: primary.onDemandCapCents ?? fallback.onDemandCapCents,
|
||||
onDemandUsedCents: primary.onDemandUsedCents ?? fallback.onDemandUsedCents,
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<html><head><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head><body><div id="root"></div><script type="module" src="./main.tsx"></script></body></html>
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { mockIPC } from '@tauri-apps/api/mocks';
|
||||
import { I18nProvider } from '../../../src/i18n';
|
||||
import { QuotaPage } from '../../../src/pages/QuotaPage';
|
||||
import '../../../src/styles/index.css';
|
||||
|
||||
// Fictional billing and subscription responses; no upstream calls or real credentials.
|
||||
const params = new URLSearchParams(location.search);
|
||||
localStorage.setItem('easy-cli-proxy-api.locale', params.get('locale') || 'zh-CN');
|
||||
document.documentElement.dataset.theme = params.get('theme') || 'light';
|
||||
mockIPC(async (command, args) => {
|
||||
if (command !== 'management_request') throw new Error('Unexpected fixture command');
|
||||
const request = args.request as { path: string; body?: { url: string } };
|
||||
if (request.path === '/credentials') return { files: [{ name: 'grok-fictional.json', provider: 'xai', auth_index: 'fixture' }] };
|
||||
const url = request.body?.url || '';
|
||||
if (url.endsWith('/settings') || url.includes('/user?')) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
return { status_code: 200, body: { subscription_tier_display: 'SuperGrok Heavy' } };
|
||||
}
|
||||
return { status_code: 200, body: { config: url.includes('format=credits')
|
||||
? { currentPeriod: { type: 'weekly', end: '2030-01-08T00:00:00Z' }, prepaidBalance: { val: 250 } }
|
||||
: { monthlyLimit: 15000, used: 3000, billingPeriodEnd: '2030-02-01T00:00:00Z' } } };
|
||||
});
|
||||
createRoot(document.getElementById('root')!).render(<I18nProvider><QuotaPage /></I18nProvider>);
|
||||
@@ -0,0 +1,70 @@
|
||||
import { afterEach, expect, it, spyOn } from 'bun:test';
|
||||
import { managementApi } from '../src/services/managementApi';
|
||||
import { commitFetchedQuota } from '../src/services/quotaEnrichment';
|
||||
import { captureQuotaCacheGeneration, getQuotaCacheSnapshot, pruneQuotaCache, updateQuotaCache } from '../src/services/quotaCache';
|
||||
import { enrichXaiQuotaPlan, loadQuota, quotaKey, type QuotaState } from '../src/services/quotaService';
|
||||
|
||||
const file = { name: 'grok-fixture.json', provider: 'xai', auth_index: 'fixture', sub: 'fictional-user' };
|
||||
const key = quotaKey(file);
|
||||
const quota: QuotaState = { status: 'success', rows: [{ label: 'Weekly', remainingPercent: 75 }], plan: 'Paid' };
|
||||
let post: ReturnType<typeof spyOn> | undefined;
|
||||
afterEach(() => { post?.mockRestore(); updateQuotaCache({}); });
|
||||
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
it('先提交额度,再后台补套餐;两次请求使用短超时和现有用户头', async () => {
|
||||
let release!: (value: unknown) => void;
|
||||
const gate = new Promise((resolve) => { release = resolve; });
|
||||
post = spyOn(managementApi, 'post').mockImplementation(async (path, body, options) => {
|
||||
expect(path).toBe('/requests/api-call');
|
||||
expect(getQuotaCacheSnapshot()[key]).toBe(quota);
|
||||
expect(options).toEqual({ timeoutMs: 8000 });
|
||||
expect(body).toMatchObject({ authIndex: 'fixture', method: 'GET', header: { 'x-userid': 'fictional-user' } });
|
||||
return await gate as never;
|
||||
});
|
||||
commitFetchedQuota(captureQuotaCacheGeneration(), file, quota);
|
||||
expect(getQuotaCacheSnapshot()[key]).toBe(quota);
|
||||
expect(post).toHaveBeenCalledTimes(2);
|
||||
release({ status_code: 200, body: { subscription_tier_display: 'SuperGrok Heavy' } });
|
||||
await flush();
|
||||
expect(getQuotaCacheSnapshot()[key]).toEqual({ ...quota, plan: 'SuperGrok Heavy' });
|
||||
});
|
||||
|
||||
it('settings 失败仍可使用 user 套餐,两个失败保留原结果', async () => {
|
||||
post = spyOn(managementApi, 'post').mockImplementation(async (_path, body) => {
|
||||
const request = body as { url: string };
|
||||
if (request.url.endsWith('/settings')) throw new Error('offline');
|
||||
return { status_code: 200, body: JSON.stringify({ subscriptionTier: 'SuperGrok' }) } as never;
|
||||
});
|
||||
expect(await enrichXaiQuotaPlan(file, quota)).toEqual({ ...quota, plan: 'SuperGrok' });
|
||||
post.mockImplementation(async () => ({ status_code: 403, body: 'unavailable' }) as never);
|
||||
expect(await enrichXaiQuotaPlan(file, quota)).toBe(quota);
|
||||
post.mockImplementation(async () => ({ status_code: 200, body: {
|
||||
subscription_tier_display: { invalid: true }, subscriptionTier: false,
|
||||
} }) as never);
|
||||
expect(await enrichXaiQuotaPlan(file, quota)).toBe(quota);
|
||||
});
|
||||
|
||||
for (const change of ['refresh', 'remove', 'session'] as const) {
|
||||
it(`迟到套餐不覆盖 ${change}`, async () => {
|
||||
let release!: (value: unknown) => void;
|
||||
const gate = new Promise((resolve) => { release = resolve; });
|
||||
post = spyOn(managementApi, 'post').mockImplementation(async () => await gate as never);
|
||||
commitFetchedQuota(captureQuotaCacheGeneration(), file, quota);
|
||||
if (change === 'refresh') updateQuotaCache({ [key]: { status: 'loading', rows: [] } });
|
||||
else if (change === 'remove') updateQuotaCache({});
|
||||
else { pruneQuotaCache(new Set()); updateQuotaCache({ [key]: quota }); }
|
||||
const expected = getQuotaCacheSnapshot();
|
||||
release({ status_code: 200, body: { subscription_tier_display: 'SuperGrok Heavy' } });
|
||||
await flush();
|
||||
expect(getQuotaCacheSnapshot()).toBe(expected);
|
||||
});
|
||||
}
|
||||
|
||||
it('套餐接口不阻塞账单结果,并按月额度提供套餐回退', async () => {
|
||||
post = spyOn(managementApi, 'post').mockImplementation(async (_path, body) => {
|
||||
expect((body as { url: string }).url).toContain('/billing');
|
||||
return { status_code: 200, body: { config: { monthlyLimit: 15000, used: 3000 } } } as never;
|
||||
});
|
||||
expect(await loadQuota(file)).toMatchObject({ status: 'success', plan: 'SuperGrok' });
|
||||
expect(post).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -55,6 +55,18 @@ describe('quota card rendering', () => {
|
||||
expect(full).toContain('width:100%');
|
||||
expect(full).not.toContain('disabled=""');
|
||||
});
|
||||
it('Grok 套餐、未知周额度和预付余额可通过现有卡片完整显示', () => {
|
||||
const html = render({ status: 'success', plan: 'SuperGrok Heavy', rows: quotaRowsFor('xai', {
|
||||
weekly: { config: { currentPeriod: { type: 'weekly', end: '2030-01-08T00:00:00Z' }, prepaidBalance: 250 } },
|
||||
monthly: { config: { monthlyLimit: 10000, used: 2500 } },
|
||||
}) }, 'xai');
|
||||
expect(html).toContain('SuperGrok Heavy');
|
||||
expect(html).toContain('Weekly quota');
|
||||
expect(html).toContain('Prepaid balance');
|
||||
expect(html).toContain('$2.50');
|
||||
expect(html.match(/75% remaining/g)).toHaveLength(1);
|
||||
expect(html.match(/real-quota-track/g)).toHaveLength(1);
|
||||
});
|
||||
it('缓存中的原始重置时间提供动态提示,过期额度不伪造为已恢复', () => {
|
||||
const html = render({ status: 'success', rows: [{ label: '5h', remainingPercent: 0, resetAtMs: Date.parse('2020-01-01T00:00:00Z') }] });
|
||||
expect(html).toContain('Reset time reached; refresh to verify');
|
||||
|
||||
@@ -184,7 +184,7 @@ describe('xAI quota queries aligned with Management Center', () => {
|
||||
handler = (request) => success({ config: request.url.includes('format=credits')
|
||||
? { currentPeriod: { type: 'weekly' } } : { creditUsagePercent: 0 } });
|
||||
expect(await loadQuota(file)).toMatchObject({
|
||||
status: 'success', rows: [{ remainingPercent: 100 }],
|
||||
status: 'success', rows: [{ remainingPercent: null }],
|
||||
});
|
||||
expect(calls).toHaveLength(2);
|
||||
});
|
||||
|
||||
@@ -335,7 +335,32 @@ describe('quotaRowsFor', () => {
|
||||
monthly: { config: { creditUsagePercent: 0, currentPeriod: { type: 'monthly', end: '2030-01-01T00:00:00Z' } } },
|
||||
});
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].remainingPercent).toBe(100);
|
||||
expect(rows[0].remainingPercent).toBeNull();
|
||||
expect(rows[0].resetAtMs).toBeUndefined();
|
||||
});
|
||||
it('xAI 周用量缺失或无效时不会借用月度支出百分比', () => {
|
||||
for (const creditUsagePercent of [undefined, 'bad']) {
|
||||
for (const used of [0, 2500]) {
|
||||
const rows = quotaRowsFor('xai', {
|
||||
weekly: { config: { currentPeriod: { type: 'weekly', end: '2030-01-08T00:00:00Z' }, creditUsagePercent } },
|
||||
monthly: { config: { monthlyLimit: 10000, used } },
|
||||
});
|
||||
expect(rows[0]).toMatchObject({ label: 'Weekly quota', remainingPercent: null });
|
||||
expect(rows[1].remainingPercent).toBe(100 - used / 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
it('xAI 隐藏周额度下的零预算零用量月行,但保留真实支出和月度独立行', () => {
|
||||
const weekly = { config: { creditUsagePercent: 25 } };
|
||||
expect(quotaRowsFor('xai', { weekly, monthly: { config: { monthlyLimit: 0, used: 0 } } })).toHaveLength(1);
|
||||
expect(quotaRowsFor('xai', { weekly, monthly: { config: { monthlyLimit: 0, used: 100 } } })).toHaveLength(2);
|
||||
expect(quotaRowsFor('xai', { config: { monthlyLimit: 0, used: 0 } })).toHaveLength(1);
|
||||
});
|
||||
it('xAI 支持两种预付余额字段,金额不当作百分比', () => {
|
||||
for (const key of ['prepaidBalance', 'prepaid_balance']) {
|
||||
expect(quotaRowsFor('xai', { config: { [key]: { val: 250 } } })).toEqual([
|
||||
{ label: 'Prepaid balance', remainingPercent: null, detail: '$2.50' },
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user