Show the Grok subscription on the xAI quota card (#451)

* feat(quota): show the Grok subscription on the xAI card

The billing payload has no plan name. The display name comes from
Grok settings and the tier from the user subscription include.

* Refactor provider credential weight normalization tests and update API key structure

- Updated test cases in `providerWeightTransformers.test.ts` to reflect changes in API key structure from 'api-key-entries' to 'keys'.
- Introduced new tests in `providerWorkbenchUi.test.ts` to verify the editing surface of provider workbench.
- Modified `sponsorCustomEndpoint.test.ts` to adapt to the new API key structure.
- Adjusted `visualConfigAntigravitySensitiveWords.test.ts` and `visualConfigConcurrency.test.ts` to accommodate new YAML structure for sensitive words and concurrency settings.
- Enhanced `visualConfigDefaults.test.ts` to ensure proper handling of boolean defaults and YAML structure.
- Updated `visualConfigDevinSensitiveWords.test.ts` to reflect changes in the YAML structure for sensitive words.
- Refined `visualConfigDisableImageGeneration.test.ts` to align with new multimedia settings.
- Added comprehensive tests in `visualConfigV8.test.ts` to validate the v8 visual config contract and ensure scalar read/write path parity.
- Improved validation tests in `visualConfigValidation.test.ts` to handle new routing and requests structure.
- Cleaned up `xaiApiKeyProvider.test.ts` by removing unnecessary API client mocks and focusing on normalization of the xAI API key contract.

* fix(config): prevent v8 configuration mutation regressions

* feat(proxy): add proxyUrl support across various components and API calls

* feat: implement model discovery merging and enhance provider forms

- Add mergeDiscoveredModels function to deduplicate and merge model entries based on incoming data.
- Refactor BaseProviderForm and SponsorProviderForm to utilize mergeDiscoveredModels for applying discovered models.
- Update ModelEntryInput type to include sourceIndex for better tracking of model identities.
- Enhance tests for provider model discovery to ensure correct merging behavior and metadata preservation.
- Modify serialization logic in providers API to handle new model structure and sourceIndex.
- Improve YAML AST handling in visual config tests to ensure accurate condition management.

* feat(cooldown): implement cooldown reset functionality and UI updates

* feat(authFileCard): add disabled state styling and update tests for clarity

* refactor: reorganize logs feature structure and improve log handling

- Deleted the `useLogScroller` hook and its associated logic to streamline log scrolling functionality.
- Moved `LogsPage` component to a new directory structure under `features/logs`.
- Updated API service for logs to enhance error handling and response normalization.
- Introduced new tests for log buffer, log parsing, and log selectors to ensure robust functionality.
- Added localization tests to verify translation consistency across multiple languages.
- Improved log request handling and lifecycle management in the logs controller.
- Enhanced log scrolling tests to validate anchor adjustments and pending log line counts.

* feat(logs): enhance log viewer with line wrapping and improved filter options

- Added a toggle for wrapping long log lines in the LogsPage component.
- Updated the structured filters panel to open in a modal for better accessibility.
- Improved accessibility features with appropriate ARIA attributes.
- Refactored log rendering logic to support new wrapping functionality.
- Updated translations for filter and display settings in multiple languages.
- Enhanced styles for better layout and responsiveness in log viewer.
- Added tests to ensure layout integrity and functionality of new features.

* feat(logs): update styles for improved layout and toolbar controls

* feat(i18n): add legacy backend error messages in Chinese and Traditional Chinese

feat(api): implement config patching logic for managing configuration changes

feat(api): add legacy backend probe for diagnosing v0 management support

fix(auth): handle legacy backend detection during login process

test: add tests for config patching and legacy backend probe functionality

test: implement visual config rebase tests for maintaining draft state

* fix(config): recover confirmed list edits without replaying stale indexes

* fix(config): block conflicting payload replacements after recovery

* fix(logs): respect handled Escape events in fullscreen

* fix(quota): load xAI subscription after quota and preserve remaining amounts

---------

Co-authored-by: Supra4E8C <tan13318527080@163.com>
This commit is contained in:
giovannirco
2026-10-01 01:32:15 +08:00
committed by GitHub
co-authored by Supra4E8C
parent b87b9487f6
commit 69ebed4f3f
17 changed files with 402 additions and 8 deletions
+4 -1
View File
@@ -14,6 +14,7 @@ import {
import type { AuthFileItem } from '@/types';
import { getStatusFromError } from '@/utils/quota';
import { getQuotaCacheKey } from '@/utils/quota/identity';
import { enrichQuotaInBackground } from '../quotaEnrichment';
import { getQuotaMap, getQuotaSetter, type QuotaAdapter, type QuotaCardState } from '../providers';
const getQuotaState = (adapter: QuotaAdapter, file: AuthFileItem): QuotaCardState | undefined =>
@@ -42,10 +43,12 @@ export function useQuotaActions(disableControls: boolean) {
try {
const data = await adapter.fetchQuota(file, t);
commitIfQuotaCacheCurrent(cacheGeneration, () => {
const successState = adapter.buildSuccessState(data);
setQuota((prev) => ({
...prev,
[cacheKey]: adapter.buildSuccessState(data),
[cacheKey]: successState,
}));
void enrichQuotaInBackground(adapter, file, data, successState, t);
showNotification(t('auth_files.quota_refresh_success', { name: file.name }), 'success');
});
} catch (err: unknown) {
@@ -14,7 +14,8 @@ import { captureQuotaCacheGeneration, commitIfQuotaCacheCurrent } from '@/stores
import { getStatusFromError } from '@/utils/quota';
import { getQuotaCacheKey } from '@/utils/quota/identity';
import type { QuotaFileEntry } from '../logic';
import { QUOTA_ADAPTERS, getQuotaSetter } from '../providers';
import { QUOTA_ADAPTERS, getQuotaSetter, type QuotaCardState } from '../providers';
import { enrichQuotaInBackground } from '../quotaEnrichment';
import type { QuotaProviderType } from '../providers/types';
interface BatchFetchResult {
@@ -85,6 +86,7 @@ export function useQuotaBatchLoader() {
if (requestId !== requestIdRef.current) return;
const committedStates = new Map<string, QuotaCardState>();
setQuota((prev) => {
const nextState = { ...prev };
results.forEach((result) => {
@@ -98,12 +100,19 @@ export function useQuotaBatchLoader() {
result.error || t('common.unknown_error'),
result.errorStatus
);
committedStates.set(result.cacheKey, nextState[result.cacheKey]);
},
result.name
);
});
return nextState;
});
results.forEach((result, index) => {
const state = committedStates.get(result.cacheKey);
if (result.status === 'success' && state) {
void enrichQuotaInBackground(adapter, entries[index].file, result.data, state, t);
}
});
})
);
} finally {
+1
View File
@@ -38,6 +38,7 @@ export interface QuotaAdapter {
i18nPrefix: string;
filterFn: (file: AuthFileItem) => boolean;
fetchQuota: (file: AuthFileItem, t: TFunction) => Promise<unknown>;
enrichQuota?: (file: AuthFileItem, data: unknown, t: TFunction) => Promise<unknown>;
resetQuota?: (file: AuthFileItem, t: TFunction) => Promise<unknown>;
canResetQuota?: (quota: QuotaCardState) => boolean;
storeSelector: (state: QuotaStore) => Record<string, QuotaCardState>;
+2
View File
@@ -46,6 +46,8 @@ export interface QuotaProviderData<TState, TData> {
i18nPrefix: string;
filterFn: (file: AuthFileItem) => boolean;
fetchQuota: (file: AuthFileItem, t: TFunction) => Promise<TData>;
/** Optional details loaded only after the primary quota has been committed. */
enrichQuota?: (file: AuthFileItem, data: TData, t: TFunction) => Promise<TData>;
resetQuota?: (file: AuthFileItem, t: TFunction) => Promise<TData>;
canResetQuota?: (quota: TState) => boolean;
storeSelector: (state: QuotaStore) => Record<string, TState>;
@@ -51,6 +51,15 @@ const formatXaiPercent = (value: number | null): string => {
const XAI_SUPERGROK_LIMIT_CENTS = 15_000;
const XAI_SUPERGROK_HEAVY_LIMIT_CENTS = 150_000;
const planValueClass = (
tier: XaiBillingSummary['planTier'],
classes: QuotaBodyProps<XaiQuotaState>['classes']
) => {
if (tier === 'elite') return classes.elitePlanValue;
if (tier === 'premium') return classes.premiumPlanValue;
return classes.codexPlanValue;
};
const resolveXaiPlan = (
monthlyLimitCents: number | null
): { labelKey: string; premium: boolean } | null => {
@@ -85,7 +94,15 @@ export function XaiQuotaBody({ quota, classes }: QuotaBodyProps<XaiQuotaState>)
<>
<div className={classes.codexPlan}>
<span className={classes.codexPlanLabel}>{t('xai_quota.plan_label')}</span>
<span className={classes.premiumPlanValue}>{t('xai_quota.plan_paid')}</span>
<span
className={
billing.planLabel
? planValueClass(billing.planTier, classes)
: classes.premiumPlanValue
}
>
{billing.planLabel ?? t('xai_quota.plan_paid')}
</span>
</div>
<div className={classes.quotaMessage}>{t('xai_quota.paid_health')}</div>
</>
@@ -138,13 +155,45 @@ export function XaiQuotaBody({ quota, classes }: QuotaBodyProps<XaiQuotaState>)
Boolean(billing.billingPeriodEnd)) &&
!(hasWeeklyData && billing.monthlyLimitCents === 0 && billing.usedCents === 0);
const subscriptionLabel = billing.planLabel ?? (plan ? t(`xai_quota.${plan.labelKey}`) : null);
const subscriptionClass = billing.planLabel
? planValueClass(billing.planTier, classes)
: plan?.premium
? classes.premiumPlanValue
: classes.codexPlanValue;
const headerReset = buildResetDisplay(
null,
hasWeeklyData ? billing.resetAtMs : parseIsoToMs(billing.billingPeriodEnd),
now,
locale
);
return (
<>
{plan && (
{(subscriptionLabel || headerReset) && (
<div className={classes.codexPlan}>
<span className={classes.codexPlanLabel}>{t('xai_quota.plan_label')}</span>
<span className={plan.premium ? classes.premiumPlanValue : classes.codexPlanValue}>
{t(`xai_quota.${plan.labelKey}`)}
{subscriptionLabel && (
<span className={classes.codexPlanItem}>
<span className={classes.codexPlanLabel}>{t('xai_quota.plan_label')}</span>
<span className={subscriptionClass}>{subscriptionLabel}</span>
</span>
)}
{headerReset && (
<span className={classes.codexPlanItem}>
<span className={classes.codexPlanLabel}>{t('xai_quota.resets_label')}</span>
<span className={classes.codexPlanValue}>{headerReset.absolute}</span>
{headerReset.relative && (
<span className={classes.quotaResetRelative}>{headerReset.relative}</span>
)}
</span>
)}
</div>
)}
{typeof billing.prepaidBalanceCents === 'number' && billing.prepaidBalanceCents > 0 && (
<div className={classes.codexPlan}>
<span className={classes.codexPlanLabel}>{t('xai_quota.prepaid')}</span>
<span className={classes.quotaAmount}>
{formatUsdFromCents(billing.prepaidBalanceCents)}
</span>
</div>
)}
+61
View File
@@ -12,6 +12,8 @@ import {
XAI_API_REQUEST_HEADERS,
XAI_BILLING_MONTHLY_URL,
XAI_BILLING_WEEKLY_URL,
XAI_SETTINGS_URL,
XAI_USER_URL,
XAI_PAID_HEALTH_MODEL,
XAI_REQUEST_HEADERS,
normalizeStringValue,
@@ -19,6 +21,7 @@ import {
buildXaiBillingSummary,
buildXaiPaidHealthSummary,
mergeXaiBillingSummaries,
resolveXaiSubscriptionPlan,
createStatusError,
isDisabledAuthFile,
isPaidXaiAuthFile,
@@ -28,6 +31,7 @@ import { normalizeAuthIndex } from '@/utils/authIndex';
import type { QuotaProviderData } from '../types';
const XAI_PAID_HEALTH_REQUEST_TIMEOUT_MS = 15000;
const XAI_SUBSCRIPTION_REQUEST_TIMEOUT_MS = 8000;
const toXaiRecord = (value: unknown): Record<string, unknown> | null => {
return value && typeof value === 'object' && !Array.isArray(value)
@@ -145,6 +149,58 @@ const requestXaiPaidHealth = async (authIndex: string): Promise<XaiBillingSummar
return buildXaiPaidHealthSummary(profile);
};
const readJsonRecord = (result: { statusCode: number; body?: unknown; bodyText?: string }) => {
if (result.statusCode < 200 || result.statusCode >= 300) return null;
const body = result.body ?? result.bodyText;
if (typeof body === 'string') {
try {
const parsed = JSON.parse(body) as unknown;
return toXaiRecord(parsed);
} catch {
return null;
}
}
return toXaiRecord(body);
};
const readPlanField = (record: Record<string, unknown> | null, keys: string[]) => {
if (!record) return null;
for (const key of keys) {
const value = normalizeStringValue(record[key]);
if (value) return value;
}
return null;
};
const requestXaiSubscription = async (authIndex: string) => {
const header = { ...XAI_REQUEST_HEADERS };
const [userResult, settingsResult] = await Promise.allSettled([
apiCallApi.request(
{ authIndex, method: 'GET', url: XAI_USER_URL, header },
{ timeout: XAI_SUBSCRIPTION_REQUEST_TIMEOUT_MS }
),
apiCallApi.request(
{ authIndex, method: 'GET', url: XAI_SETTINGS_URL, header },
{ timeout: XAI_SUBSCRIPTION_REQUEST_TIMEOUT_MS }
),
]);
const user = userResult.status === 'fulfilled' ? readJsonRecord(userResult.value) : null;
const settings =
settingsResult.status === 'fulfilled' ? readJsonRecord(settingsResult.value) : null;
return resolveXaiSubscriptionPlan(
readPlanField(user, ['subscriptionTier', 'subscription_tier']),
readPlanField(settings, ['subscription_tier_display', 'subscriptionTierDisplay'])
);
};
const withSubscriptionPlan = (
summary: XaiBillingSummary,
plan: { label: string; tier: 'elite' | 'premium' | 'standard' } | null
): XaiBillingSummary => {
if (!plan) return summary;
return { ...summary, planLabel: plan.label, planTier: plan.tier };
};
const fetchXaiQuota = async (file: AuthFileItem, t: TFunction): Promise<XaiBillingSummary> => {
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
const authIndex = normalizeAuthIndex(rawAuthIndex);
@@ -184,6 +240,11 @@ export const XAI_CONFIG: QuotaProviderData<XaiQuotaState, XaiBillingSummary> = {
i18nPrefix: 'xai_quota',
filterFn: (file) => isXaiFile(file) && !isDisabledAuthFile(file),
fetchQuota: fetchXaiQuota,
enrichQuota: async (file, summary) => {
const authIndex = normalizeAuthIndex(file['auth_index'] ?? file.authIndex);
if (!authIndex) return summary;
return withSubscriptionPlan(summary, await requestXaiSubscription(authIndex));
},
storeSelector: (state) => state.xaiQuota,
storeSetter: 'setXaiQuota',
buildLoadingState: () => ({ status: 'loading', billing: null }),
+38
View File
@@ -0,0 +1,38 @@
import type { TFunction } from 'i18next';
import type { AuthFileItem } from '@/types';
import {
captureQuotaCacheGeneration,
commitIfQuotaCacheCurrent,
useQuotaStore,
} from '@/stores/useQuotaStore';
import { getQuotaCacheKey } from '@/utils/quota/identity';
import type { QuotaAdapter, QuotaCardState, QuotaMapUpdater } from './providers';
/** Optional metadata must neither delay quota rendering nor replace a newer result. */
export async function enrichQuotaInBackground(
adapter: QuotaAdapter,
file: AuthFileItem,
data: unknown,
expectedState: QuotaCardState,
t: TFunction
): Promise<void> {
if (!adapter.enrichQuota) return;
const cacheKey = getQuotaCacheKey(file);
const currentState = () => adapter.storeSelector(useQuotaStore.getState())[cacheKey];
if (currentState() !== expectedState) return;
const generation = captureQuotaCacheGeneration(file.name);
try {
const enriched = await adapter.enrichQuota(file, data, t);
if (enriched === data) return;
commitIfQuotaCacheCurrent(generation, () => {
const setQuota = useQuotaStore.getState()[adapter.storeSetter] as QuotaMapUpdater;
setQuota((prev) => {
if (prev[cacheKey] !== expectedState) return prev;
return { ...prev, [cacheKey]: adapter.buildSuccessState(enriched) };
});
});
} catch {
// Keep the successful quota and its original plan fallback on optional failures.
}
}
+2
View File
@@ -593,6 +593,8 @@
"reset_at": "Resets {{time}}",
"product_usage": "{{product}} usage",
"plan_label": "Plan",
"resets_label": "Resets",
"prepaid": "Prepaid",
"plan_supergrok": "SuperGrok",
"plan_supergrok_heavy": "SuperGrok Heavy",
"plan_paid": "Paid",
+2
View File
@@ -596,6 +596,8 @@
"reset_at": "Сброс {{time}}",
"product_usage": "Использование {{product}}",
"plan_label": "План",
"resets_label": "Сброс",
"prepaid": "Предоплата",
"plan_supergrok": "SuperGrok",
"plan_supergrok_heavy": "SuperGrok Heavy",
"plan_paid": "Платный",
+2
View File
@@ -591,6 +591,8 @@
"reset_at": "重置 {{time}}",
"product_usage": "{{product}} 使用",
"plan_label": "套餐",
"resets_label": "重置",
"prepaid": "预付",
"plan_supergrok": "SuperGrok",
"plan_supergrok_heavy": "SuperGrok Heavy",
"plan_paid": "付费",
+2
View File
@@ -591,6 +591,8 @@
"reset_at": "重置 {{time}}",
"product_usage": "{{product}} 使用",
"plan_label": "方案",
"resets_label": "重置",
"prepaid": "預付",
"plan_supergrok": "SuperGrok",
"plan_supergrok_heavy": "SuperGrok Heavy",
"plan_paid": "付費",
+6
View File
@@ -387,6 +387,8 @@ export interface XaiBillingConfig {
on_demand_cap?: XaiBillingCent | number | string | null;
onDemandUsed?: XaiBillingCent | number | string | null;
on_demand_used?: XaiBillingCent | number | string | null;
prepaidBalance?: XaiBillingCent | number | string | null;
prepaid_balance?: XaiBillingCent | number | string | null;
billingPeriodStart?: string;
billing_period_start?: string;
billingPeriodEnd?: string;
@@ -422,6 +424,10 @@ export interface XaiBillingSummary {
onDemandCapCents: number | null;
onDemandUsedCents: number | null;
onDemandUsedPercent: number | null;
prepaidBalanceCents?: number | null;
/** Display name from Grok settings, for example "SuperGrok Heavy". */
planLabel?: string;
planTier?: 'elite' | 'premium' | 'standard';
billingPeriodStart?: string;
billingPeriodEnd?: string;
usedPercent: number | null;
+24
View File
@@ -414,6 +414,7 @@ const emptyXaiBillingSummary = (): XaiBillingSummary => ({
onDemandCapCents: null,
onDemandUsedCents: null,
onDemandUsedPercent: null,
prepaidBalanceCents: null,
usedPercent: null,
});
@@ -438,6 +439,22 @@ function xaiPeriodInstants(
return { resetAtMs, periodHours };
}
export function resolveXaiSubscriptionPlan(
tier: string | null | undefined,
display: string | null | undefined
): { label: string; tier: 'elite' | 'premium' | 'standard' } | null {
const label = normalizeStringValue(display) ?? normalizeStringValue(tier);
if (!label) return null;
const key = `${display ?? ''} ${tier ?? ''}`.toLowerCase().replace(/[^a-z0-9]+/g, '');
const planTier =
key.includes('heavy')
? 'elite'
: key.includes('supergrok') || key.includes('premium')
? 'premium'
: 'standard';
return { label, tier: planTier };
}
export function buildXaiBillingSummary(
config: XaiBillingConfig | null | undefined
): XaiBillingSummary | null {
@@ -484,6 +501,9 @@ export function buildXaiBillingSummary(
? Math.max(0, usedCents - monthlyLimitCents)
: null;
const onDemandUsedCents = explicitOnDemandUsedCents ?? derivedOnDemandUsedCents;
const prepaidBalanceCents = normalizeXaiCentValue(
config.prepaidBalance ?? config.prepaid_balance
);
const usedPercent =
monthlyLimitCents !== null && monthlyLimitCents > 0 && includedUsedCents !== null
? (includedUsedCents / monthlyLimitCents) * 100
@@ -517,6 +537,7 @@ export function buildXaiBillingSummary(
summary.onDemandCapCents = onDemandCapCents;
summary.onDemandUsedCents = onDemandUsedCents;
summary.onDemandUsedPercent = onDemandUsedPercent;
summary.prepaidBalanceCents = prepaidBalanceCents;
summary.billingPeriodStart = hasMonthlyData ? billingPeriodStart : undefined;
summary.billingPeriodEnd = hasMonthlyData ? billingPeriodEnd : undefined;
summary.usedPercent = usedPercent;
@@ -566,6 +587,9 @@ export function mergeXaiBillingSummaries(
onDemandCapCents: primary.onDemandCapCents ?? fallback.onDemandCapCents,
onDemandUsedCents: primary.onDemandUsedCents ?? fallback.onDemandUsedCents,
onDemandUsedPercent: primary.onDemandUsedPercent ?? fallback.onDemandUsedPercent,
prepaidBalanceCents: primary.prepaidBalanceCents ?? fallback.prepaidBalanceCents,
planLabel: primary.planLabel ?? fallback.planLabel,
planTier: primary.planTier ?? fallback.planTier,
billingPeriodStart: primary.billingPeriodStart ?? fallback.billingPeriodStart,
billingPeriodEnd: primary.billingPeriodEnd ?? fallback.billingPeriodEnd,
usedPercent: primary.usedPercent ?? fallback.usedPercent,
+2
View File
@@ -153,6 +153,8 @@ export const KIMI_REQUEST_HEADERS = {
// xAI/Grok API configuration
export const XAI_BILLING_WEEKLY_URL = 'https://cli-chat-proxy.grok.com/v1/billing?format=credits';
export const XAI_BILLING_MONTHLY_URL = 'https://cli-chat-proxy.grok.com/v1/billing';
export const XAI_USER_URL = 'https://cli-chat-proxy.grok.com/v1/user?include=subscription';
export const XAI_SETTINGS_URL = 'https://cli-chat-proxy.grok.com/v1/settings';
export const XAI_API_ME_URL = 'https://api.x.ai/v1/me';
export const XAI_API_CHAT_URL = 'https://api.x.ai/v1/chat/completions';
export const XAI_PAID_HEALTH_MODEL = 'grok-4.5';
+2 -1
View File
@@ -78,7 +78,8 @@ describe('xAI paid OAuth quota fallback', () => {
);
expect(requests.map((request) => request.url)).toEqual([XAI_API_ME_URL, XAI_API_CHAT_URL]);
expect(JSON.parse(requests[1]?.data ?? '{}')).toMatchObject({
const chatRequest = requests.find((request) => request.url === XAI_API_CHAT_URL);
expect(JSON.parse(chatRequest?.data ?? '{}')).toMatchObject({
model: 'grok-4.5',
max_tokens: 1,
stream: false,
+124
View File
@@ -0,0 +1,124 @@
import { afterEach, expect, test } from 'bun:test';
import i18n from '@/i18n';
import type { AuthFileItem } from '@/types';
import { apiCallApi, type ApiCallResult } from '@/services/api';
import { useQuotaStore } from '@/stores/useQuotaStore';
import { XAI_CONFIG } from '@/features/quota/providers/xai/data';
import type { QuotaAdapter } from '@/features/quota/providers';
import { enrichQuotaInBackground } from '@/features/quota/quotaEnrichment';
import { getQuotaCacheKey } from '@/utils/quota/identity';
import { XAI_USER_URL, XAI_SETTINGS_URL, buildXaiBillingSummary } from '@/utils/quota';
const originalRequest = apiCallApi.request;
const file: AuthFileItem = { name: 'fixture.json', type: 'xai', auth_index: 'fixture' };
const key = getQuotaCacheKey(file);
const adapter = XAI_CONFIG as unknown as QuotaAdapter;
const result = (body: unknown, statusCode = 200): ApiCallResult => ({
statusCode,
body,
bodyText: JSON.stringify(body),
header: {},
});
const billing = () =>
buildXaiBillingSummary({ monthlyLimit: { val: 15000 }, used: { val: 1500 } })!;
const current = () => useQuotaStore.getState().xaiQuota[key];
const deferred = () => {
let resolve!: (value: ApiCallResult) => void;
const promise = new Promise<ApiCallResult>((done) => {
resolve = done;
});
return { promise, resolve };
};
const beginEnrichment = () => {
const data = billing();
const state = XAI_CONFIG.buildSuccessState(data);
useQuotaStore.getState().setXaiQuota({ [key]: state });
const pending = enrichQuotaInBackground(adapter, file, data, state, i18n.t);
return { data, state, pending };
};
afterEach(() => {
apiCallApi.request = originalRequest;
useQuotaStore.getState().clearQuotaCache();
});
test('commits quota before requesting optional plan and updates it later', async () => {
const gate = deferred();
const urls: string[] = [];
apiCallApi.request = async (request, config) => {
urls.push(request.url);
if (request.url === XAI_USER_URL || request.url === XAI_SETTINGS_URL) {
expect(current()?.status).toBe('success');
expect(config?.timeout).toBe(8000);
return gate.promise;
}
return result({ config: { monthlyLimit: { val: 15000 }, used: { val: 1500 } } });
};
const data = await XAI_CONFIG.fetchQuota(file, i18n.t);
expect(urls).toHaveLength(2);
expect(urls).not.toContain(XAI_USER_URL);
const state = XAI_CONFIG.buildSuccessState(data);
useQuotaStore.getState().setXaiQuota({ [key]: state });
const pending = enrichQuotaInBackground(adapter, file, data, state, i18n.t);
expect(current()).toBe(state);
expect(current().billing?.planLabel).toBeUndefined();
gate.resolve(result({ subscription_tier_display: 'SuperGrok Heavy' }));
await pending;
expect(current().billing).toMatchObject({
...data,
planLabel: 'SuperGrok Heavy',
planTier: 'elite',
});
});
for (const failure of ['network', 'http', 'malformed'] as const) {
test(`optional ${failure} failure preserves the exact original quota and fallback`, async () => {
apiCallApi.request = async () => {
if (failure === 'network') throw new Error('unavailable');
return failure === 'http' ? result({}, 503) : result('not-json');
};
const { state, pending } = beginEnrichment();
await pending;
expect(current()).toBe(state);
});
}
test('uses user tier when settings fails', async () => {
apiCallApi.request = async ({ url }) => {
if (url === XAI_SETTINGS_URL) throw new Error('unavailable');
return result({ subscriptionTier: 'SuperGrok' });
};
const { pending } = beginEnrichment();
await pending;
expect(current().billing).toMatchObject({ planLabel: 'SuperGrok', planTier: 'premium' });
});
for (const change of ['session', 'file', 'new-result', 'refresh'] as const) {
test(`late subscription cannot overwrite ${change}`, async () => {
const gate = deferred();
apiCallApi.request = async () => gate.promise;
const { pending } = beginEnrichment();
if (change === 'session') useQuotaStore.getState().clearQuotaCache();
if (change === 'file') useQuotaStore.getState().clearQuotaCache([file.name]);
if (change === 'new-result') {
useQuotaStore.getState().setXaiQuota({ [key]: XAI_CONFIG.buildSuccessState(billing()) });
}
if (change === 'refresh') {
useQuotaStore.getState().setXaiQuota({ [key]: XAI_CONFIG.buildLoadingState() });
}
const expected = current();
gate.resolve(result({ subscription_tier_display: 'SuperGrok Heavy' }));
await pending;
expect(current()).toBe(expected);
});
}
test('unrelated file invalidation does not discard a valid subscription', async () => {
const gate = deferred();
apiCallApi.request = async () => gate.promise;
const { pending } = beginEnrichment();
useQuotaStore.getState().clearQuotaCache(['other.json']);
gate.resolve(result({ subscription_tier_display: 'SuperGrok Heavy' }));
await pending;
expect(current().billing?.planLabel).toBe('SuperGrok Heavy');
});
+66
View File
@@ -15,8 +15,10 @@ import { XaiQuotaBody } from '@/features/quota/providers/xai/XaiQuotaBody';
import { QUOTA_CLASS_KEYS, bindQuotaClasses } from '@/features/quota/types';
import {
buildXaiBillingSummary,
formatInstantShort,
formatQuotaResetTime,
mergeXaiBillingSummaries,
resolveXaiSubscriptionPlan,
} from '@/utils/quota';
import type { XaiBillingConfig, XaiQuotaState } from '@/types';
@@ -67,6 +69,26 @@ beforeAll(async () => {
await i18n.changeLanguage('en');
});
describe('resolveXaiSubscriptionPlan', () => {
test('uses the settings display name and treats Heavy as the elite badge', () => {
expect(resolveXaiSubscriptionPlan('SuperGrokPro', 'SuperGrok Heavy')).toEqual({
label: 'SuperGrok Heavy',
tier: 'elite',
});
});
test('treats X Premium+ as the premium badge', () => {
expect(resolveXaiSubscriptionPlan('XPremiumPlus', 'X Premium+')).toEqual({
label: 'X Premium+',
tier: 'premium',
});
});
test('returns null when neither call produced a name', () => {
expect(resolveXaiSubscriptionPlan(null, ' ')).toBeNull();
});
});
describe('XaiQuotaBody unavailable weekly usage', () => {
test('reports unavailable instead of a fabricated zero when usagePercent is missing', () => {
const quota = quotaFor(weeklyConfig(), monthlyConfig());
@@ -151,6 +173,50 @@ describe('XaiQuotaBody unavailable weekly usage', () => {
expect(markup).toContain('Monthly credits');
});
test('shows the Grok subscription name and the weekly reset beside it', () => {
const quota = quotaFor(
weeklyConfig({
creditUsagePercent: 94,
prepaidBalance: { val: 250 },
productUsage: [
{ product: 'GrokBuild', usagePercent: 93 },
{ product: 'GrokChat', usagePercent: 1 },
],
}),
monthlyConfig({ used: { val: 4 } })
);
if (!quota.billing) throw new Error('missing billing');
quota.billing.planLabel = 'SuperGrok Heavy';
quota.billing.planTier = 'elite';
const markup = render(quota);
expect(markup).toContain('elitePlanValue');
expect(markup).toContain('SuperGrok Heavy');
expect(markup).toContain('Resets');
expect(markup).toContain(formatInstantShort(Date.parse(WEEKLY_PERIOD_END)));
expect(markup).toContain('GrokChat usage');
expect(markup).toContain('Prepaid');
expect(markup).toContain('$2.50');
expect(markup).toContain('$0.00 / $0.00');
});
test('keeps monthly amount, percentage, and meter in the remaining direction', () => {
const markup = render(
quotaFor(
null,
monthlyConfig({
monthlyLimit: { val: 15000 },
used: { val: 1500 },
})
)
);
expect(markup).toContain('>90%<');
expect(markup).toContain('$135.00 / $150.00');
expect(markup).not.toContain('$15.00 / $150.00');
expect(markup).toContain('width:90%');
});
test('keeps the monthly-only zero row when no weekly data exists', () => {
const markup = render(quotaFor(null, monthlyConfig()));