fix(quota): remove extra reset restrictions while preserving confirmation

- Allow resets when applicable credits are zero or a previous reset failed
- Remove applicable-credit details from the confirmation and obsolete tooltips
- Keep the existing in-app dialog and require explicit confirmation before submitting
- Update regression tests for cancellation, confirmation, and retries
This commit is contained in:
lzt404 committed 2026-09-09 12:13:57 +08:00
1 parent 5548324c8f
commit 805e2aece0
7 files changed
+54 -29

No files matched your search

-3
View File
@@ -195,10 +195,7 @@ export const jaOverrides = {
'quota.service.xaiPaidHealth': '有料 API の会話が利用可能です。xAI は現在、この OAuth 認証情報の総クォータを提供していません。',
'quota.service.xaiPaidQuotaUnavailable': 'このアカウントの残りクォータは提供されていません。',
'quota.confirm.button': 'リセットを実行',
'quota.confirm.applicableLabel': '現在適用可能な回数',
'quota.resetting': 'リセットを送信してクォータを更新中…',
'quota.resetNotApplicable': '現在適用可能なリセット回数がありません。クォータを更新して確認してください',
'quota.resetCheckFirst': '前回の結果が未確認です。重複リセットを避けるため、先にクォータを更新してください',
'quota.resetResult.submitted': 'リセット要求を送信し、クォータを再取得しました。カードの最新クォータを確認してください。',
'quota.resetResult.refreshFailed': 'リセット要求は送信済みですが、クォータの更新に失敗しました:{error}。再消費を避けるため、先にクォータを更新してください。',
'quota.resetResult.failed': 'リセットの成功を確認できませんでした:{error}。再試行する前にクォータを更新してください。',
-3
View File
@@ -182,10 +182,7 @@ export const en: Record<MessageKey, string> = {
'quota.service.xaiPaidHealth': 'Paid API chat is available. xAI does not currently provide total quota data for this OAuth credential.',
'quota.service.xaiPaidQuotaUnavailable': 'Remaining quota is not provided for this account.',
'quota.confirm.button': 'Confirm Reset',
'quota.confirm.applicableLabel': 'Currently applicable resets',
'quota.resetting': 'Submitting reset and refreshing quota…',
'quota.resetNotApplicable': 'No reset credits currently apply. Refresh quota before trying again',
'quota.resetCheckFirst': 'The previous result is unconfirmed. Refresh quota first to avoid another reset',
'quota.resetResult.submitted': 'The reset request was submitted and quota was refreshed. Check the latest quota on this card.',
'quota.resetResult.refreshFailed': 'The reset request was submitted, but refreshing quota failed: {error}. Refresh quota first to avoid consuming another reset credit.',
'quota.resetResult.failed': 'Could not confirm the reset: {error}. Refresh quota before deciding whether to retry.',
-3
View File
@@ -181,10 +181,7 @@ export const zhCN = {
'quota.service.xaiPaidHealth': '付费 API 对话可用。xAI 暂不为此 OAuth 凭证提供额度总量数据。',
'quota.service.xaiPaidQuotaUnavailable': '此账号不提供剩余额度。',
'quota.confirm.button': '确认重置',
'quota.confirm.applicableLabel': '当前适用次数',
'quota.resetting': '正在提交重置并刷新额度…',
'quota.resetNotApplicable': '当前没有适用的重置次数,请刷新额度后再确认',
'quota.resetCheckFirst': '上次操作结果尚未确认,请先刷新额度,避免重复重置',
'quota.resetResult.submitted': '重置请求已提交,额度已重新获取。请查看卡片中的最新额度。',
'quota.resetResult.refreshFailed': '重置请求已提交,但额度刷新失败:{error}。请先刷新查询,避免重复消耗重置机会。',
'quota.resetResult.failed': '无法确认重置成功:{error}。请先刷新额度,再决定是否重试。',
+1 -2
View File
@@ -102,7 +102,6 @@ export function QuotaPage() {
confirmText: t('quota.confirm.button'),
details: [
{ label: t('quota.resetCredits'), value: String(quota.resetCredits ?? '—') },
{ label: t('quota.confirm.applicableLabel'), value: String(quota.resetCreditsApplicable ?? '—') },
{ label: t('quota.earliestExpiry'), value: formatQuotaTimestamp(quota.resetCreditsEarliestExpiry, locale) },
],
warning: t('quota.confirm.warning'),
@@ -198,7 +197,7 @@ export function QuotaCard({ file, quota, onRefresh, onReset }: { file: AuthFile;
<div className="real-quota-card-header">
<div><strong title={name}>{name}</strong><span>{provider ? providerMeta[provider].label : t('quota.unknownProvider')}{quota.plan ? ' · ' + quota.plan : ''}</span></div>
<div className="quota-card-actions">
{onReset && (quota.resetCredits ?? 0) > 0 ? <button type="button" className="secondary-button compact-button" onClick={onReset} disabled={!canResetCodexQuota(file, quota)} title={quota.resetCreditsApplicable === 0 ? t('quota.resetNotApplicable') : quota.actionResult?.action === 'reset' && quota.actionResult.status !== 'success' ? t('quota.resetCheckFirst') : t('quota.reset')}>{t('quota.reset')}</button> : null}
{onReset && (quota.resetCredits ?? 0) > 0 ? <button type="button" className="secondary-button compact-button" onClick={onReset} disabled={!canResetCodexQuota(file, quota)} title={t('quota.reset')}>{t('quota.reset')}</button> : null}
<button type="button" className="icon-button quiet" onClick={onRefresh} disabled={disabled || quota.status === 'loading'} title={disabled ? t('quota.fileDisabled') : t('quota.refresh')}><RefreshCw size={16} className={quota.status === 'loading' ? 'spin' : ''} /></button>
</div>
</div>
+2 -4
View File
@@ -16,10 +16,8 @@ type QuotaActionOutcome = 'cancelled' | 'success' | 'refresh-error' | 'error';
export const canResetCodexQuota = (file: AuthFile, quota: QuotaState): boolean =>
providerForFile(file) === 'codex'
&& !readBoolean(file, 'disabled')
&& quota.status === 'success'
&& (quota.resetCredits ?? 0) > 0
&& quota.resetCreditsApplicable !== 0
&& !(quota.actionResult?.action === 'reset' && quota.actionResult.status !== 'success');
&& quota.status !== 'loading'
&& (quota.resetCredits ?? 0) > 0;
async function runConfirmedQuotaAction(
file: AuthFile,
+37 -12
View File
@@ -108,35 +108,60 @@ describe('quota action confirmation with fully mocked IPC', () => {
expect(upstreamCalls).toHaveLength(0);
});
it('does not offer or execute a reset when no credits currently apply', async () => {
updateQuotaCache({ [key]: { ...previous, resetCreditsApplicable: 0 } });
let asked = false;
await resetCodexQuotaWithConfirmation(file, async () => { asked = true; return true; });
expect(asked).toBe(false);
it.each([0, undefined])('allows a confirmed reset with full quota regardless of applicable credits: %s', async (applicable) => {
const quota: QuotaState = {
...previous, rows: [{ label: '5h', remainingPercent: 100 }], resetCreditsApplicable: applicable,
};
updateQuotaCache({ [key]: quota });
expect(canResetCodexQuota(file, quota)).toBe(true);
expect(await resetCodexQuotaWithConfirmation(file, async () => false)).toBe('cancelled');
expect(upstreamCalls).toHaveLength(0);
expect(canResetCodexQuota(file, getQuotaCacheSnapshot()[key])).toBe(false);
expect(canResetCodexQuota({ ...file, disabled: true }, previous)).toBe(false);
expect(getQuotaCacheSnapshot()[key]).toBe(quota);
let decide!: (confirmed: boolean) => void;
const resetting = resetCodexQuotaWithConfirmation(file, () => new Promise((resolve) => { decide = resolve; }));
expect(upstreamCalls).toHaveLength(0);
expect(getQuotaCacheSnapshot()[key]).toBe(quota);
decide(true);
expect(await resetting).toBe('success');
expect(upstreamCalls.filter((request) => request.url.endsWith('/consume'))).toHaveLength(1);
});
it('retains quota and reports a failed reset on the account, requiring refresh before retry', async () => {
it('keeps disabled credentials and pending requests unavailable', async () => {
expect(canResetCodexQuota({ ...file, disabled: true }, previous)).toBe(false);
expect(await resetCodexQuotaWithConfirmation({ ...file, disabled: true }, async () => true)).toBe('cancelled');
updateQuotaCache({ [key]: { ...previous, status: 'loading' } });
expect(canResetCodexQuota(file, getQuotaCacheSnapshot()[key])).toBe(false);
expect(await resetCodexQuotaWithConfirmation(file, async () => true)).toBe('cancelled');
expect(upstreamCalls).toHaveLength(0);
});
it('reports a failed reset and allows retry after a new confirmation', async () => {
consumeError = true;
expect(await resetCodexQuotaWithConfirmation(file, async () => true)).toBe('error');
expect(getQuotaCacheSnapshot()[key]).toMatchObject({
rows: previous.rows, actionResult: { action: 'reset', status: 'error', error: 'reset denied' },
});
expect(canResetCodexQuota(file, getQuotaCacheSnapshot()[key])).toBe(false);
expect(await resetCodexQuotaWithConfirmation(file, async () => true)).toBe('cancelled');
expect(canResetCodexQuota(file, getQuotaCacheSnapshot()[key])).toBe(true);
expect(await resetCodexQuotaWithConfirmation(file, async () => false)).toBe('cancelled');
expect(upstreamCalls).toHaveLength(1);
consumeError = false;
expect(await resetCodexQuotaWithConfirmation(file, async () => true)).toBe('success');
expect(upstreamCalls.filter((request) => request.url.endsWith('/consume'))).toHaveLength(2);
});
it('distinguishes an accepted reset from a failed refresh and prevents a second consume', async () => {
it('reports a failed follow-up refresh and requires a new confirmation for another reset', async () => {
refreshError = true;
expect(await resetCodexQuotaWithConfirmation(file, async () => true)).toBe('refresh-error');
expect(getQuotaCacheSnapshot()[key]).toMatchObject({
rows: previous.rows, actionResult: { action: 'reset', status: 'refresh-error', error: 'usage unavailable' },
});
await resetCodexQuotaWithConfirmation(file, async () => true);
expect(canResetCodexQuota(file, getQuotaCacheSnapshot()[key])).toBe(true);
expect(await resetCodexQuotaWithConfirmation(file, async () => false)).toBe('cancelled');
expect(upstreamCalls.filter((request) => request.url.endsWith('/consume'))).toHaveLength(1);
refreshError = false;
expect(await resetCodexQuotaWithConfirmation(file, async () => true)).toBe('success');
expect(upstreamCalls.filter((request) => request.url.endsWith('/consume'))).toHaveLength(2);
});
it('keeps application confirmations out of native and browser dialog APIs', async () => {
+14 -2
View File
@@ -10,17 +10,29 @@ const render = (quota: QuotaState, provider = 'codex') => renderToStaticMarkup(
);
describe('quota card rendering', () => {
it('当前适用次数为零时禁用重置按钮,并保留额度详情', () => {
it('当前适用次数为零时仍可重置,并保留额度详情', () => {
const html = render({
status: 'success', rows: [], resetCredits: 2, resetCreditsApplicable: 0,
resetCreditsError: 'temporary failure', subscriptionActiveUntil: '2030-01-01T00:00:00Z',
});
expect(html).toMatch(/<button[^>]*disabled=""[^>]*>重置额度<\/button>/);
expect(html).toMatch(/<button[^>]*title="重置额度"[^>]*>重置额度<\/button>/);
expect(html).not.toMatch(/<button[^>]*disabled=""[^>]*>重置额度<\/button>/);
expect(html).not.toContain('当前没有适用的重置次数');
expect(html).toContain('当前适用:0 次');
expect(html).toContain('temporary failure');
expect(html).toContain('订阅到期');
});
it.each(['error', 'refresh-error'] as const)('重置结果为 %s 时仍可再次点击重置', (status) => {
const html = render({
status: 'success', rows: [], resetCredits: 2,
actionResult: { action: 'reset', status, error: 'temporary failure' },
});
expect(html).toMatch(/<button[^>]*title="重置额度"[^>]*>重置额度<\/button>/);
expect(html).not.toMatch(/<button[^>]*disabled=""[^>]*>重置额度<\/button>/);
expect(html).toContain('temporary failure');
});
it('xAI 付费账号保留额度说明和刷新入口,不显示可用性测试', () => {
const html = render({ status: 'success', rows: [{ label: '付费 API 账号', remainingPercent: null, detail: '上游未提供剩余额度' }] }, 'xai');
expect(html).toContain('上游未提供剩余额度');