diff --git a/src/cliproxy/accounts/__tests__/account-safety-quota-exhaustion.test.ts b/src/cliproxy/accounts/__tests__/account-safety-quota-exhaustion.test.ts index 32b59c5c..ebac4851 100644 --- a/src/cliproxy/accounts/__tests__/account-safety-quota-exhaustion.test.ts +++ b/src/cliproxy/accounts/__tests__/account-safety-quota-exhaustion.test.ts @@ -21,6 +21,7 @@ import { restoreExpiredQuotaPauses, } from '../../accounts/account-safety'; import { sanitizeEmail } from '../../auth/auth-utils'; +import { pauseAccount } from '../registry'; // Setup test isolation let tmpDir: string; @@ -1228,6 +1229,65 @@ describe('Quota Exhaustion Handlers', () => { expect(fs.existsSync(path.join(tmpDir, '.ccs', 'cliproxy', 'quota-paused.json'))).toBe(false); }); + it('does not auto-resume when a user manually re-pauses a quota-paused account', async () => { + writeRegistry({ + agy: { + default: 'cooldown@gmail.com', + accounts: { + 'cooldown@gmail.com': { + email: 'cooldown@gmail.com', + tokenFile: 'agy-cooldown.json', + }, + }, + }, + }); + writeAuthToken('agy-cooldown.json', { + type: 'agy', + email: 'cooldown@gmail.com', + access_token: 'token', + }); + + const now = Date.now(); + expect(pauseAccountForQuotaCooldown('agy', 'cooldown@gmail.com', 5, now)).toBe(true); + + const registryPath = path.join(tmpDir, '.ccs', 'cliproxy', 'accounts.json'); + const quotaPausedPath = path.join(tmpDir, '.ccs', 'cliproxy', 'quota-paused.json'); + const originalPausedAt = '2026-01-01T00:00:00.000Z'; + const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')) as { + providers: { + agy: { + accounts: Record; + }; + }; + }; + const quotaPaused = JSON.parse(fs.readFileSync(quotaPausedPath, 'utf8')) as { + entries: Array<{ pausedAt: string }>; + }; + registry.providers.agy.accounts['cooldown@gmail.com'].pausedAt = originalPausedAt; + quotaPaused.entries[0].pausedAt = originalPausedAt; + fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2)); + fs.writeFileSync(quotaPausedPath, JSON.stringify(quotaPaused, null, 2)); + + expect(pauseAccount('agy', 'cooldown@gmail.com')).toBe(true); + + const refreshedRegistry = JSON.parse( + fs.readFileSync(registryPath, 'utf8') + ) as typeof registry; + expect(refreshedRegistry.providers.agy.accounts['cooldown@gmail.com'].pausedAt).not.toBe( + originalPausedAt + ); + + const resumed = restoreExpiredQuotaPauses(now + 6 * 60 * 1000); + const { getAccount } = await import('../account-manager'); + + expect(resumed).toBe(0); + expect(getAccount('agy', 'cooldown@gmail.com')?.paused).toBe(true); + expect( + fs.existsSync(path.join(tmpDir, '.ccs', 'cliproxy', 'auth-paused', 'agy-cooldown.json')) + ).toBe(true); + expect(fs.existsSync(quotaPausedPath)).toBe(false); + }); + it('does not auto-resume quota-paused accounts when pausedAt metadata is missing', async () => { writeRegistry({ agy: { diff --git a/src/cliproxy/accounts/registry.ts b/src/cliproxy/accounts/registry.ts index fb688341..b87d0dd6 100644 --- a/src/cliproxy/accounts/registry.ts +++ b/src/cliproxy/accounts/registry.ts @@ -660,6 +660,10 @@ export function pauseAccount(provider: CLIProxyProvider, accountId: string): boo const accountMeta = providerAccounts.accounts[accountId]; if (accountMeta.paused) { + // Treat an explicit pause request for an already paused account as a fresh + // manual decision. This changes the pause metadata so quota cooldown + // restore cannot later mistake the pause for its original auto-pause. + accountMeta.pausedAt = new Date().toISOString(); return true; } diff --git a/src/commands/cliproxy/quota-subcommand.ts b/src/commands/cliproxy/quota-subcommand.ts index b53dd770..5094e4e2 100644 --- a/src/commands/cliproxy/quota-subcommand.ts +++ b/src/commands/cliproxy/quota-subcommand.ts @@ -1056,8 +1056,13 @@ export async function handlePauseAccount(args: string[]): Promise { } if (account.paused) { + const refreshed = pauseAccount(provider, account.id); + const refreshedAccount = refreshed ? findAccountByQuery(provider, account.id) : account; console.log(warn(`Account already paused: ${formatCliAccountLabel(account)}`)); - console.log(info(`Paused at: ${account.pausedAt || 'unknown'}`)); + if (refreshed) { + console.log(info('Manual pause refreshed; account will stay out of quota rotation')); + } + console.log(info(`Paused at: ${refreshedAccount?.pausedAt || account.pausedAt || 'unknown'}`)); return; }