mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-06 08:13:49 +00:00
fix: skip paused codex accounts in bar refresh (#1620)
This commit is contained in:
1 parent
396e01ab6f
commit
0527e276b1
2 files changed
+248
-3
No files matched your search
@@ -43,7 +43,7 @@ import {
|
||||
} from './claude-native-credentials';
|
||||
import { fetchClaudeQuotaWithToken } from '../../cliproxy/quota/quota-fetcher-claude';
|
||||
import { fetchCodexQuota } from '../../cliproxy/quota/quota-fetcher-codex';
|
||||
import { getDefaultAccount } from '../../cliproxy/accounts/query';
|
||||
import { getDefaultAccount, getProviderAccounts } from '../../cliproxy/accounts/query';
|
||||
import { getCodexLocalQuota, type CodexLocalQuota } from './codex-local-quota-collector';
|
||||
import type { ClaudeQuotaResult, CodexQuotaResult } from '../../cliproxy/quota/quota-types';
|
||||
import type { BarSummaryRow, QuotaWindowDetail } from '../routes/bar-routes';
|
||||
@@ -618,10 +618,17 @@ function listCodexProfilesFromDisk(): string[] {
|
||||
const { CodexProfileRegistry } = require('../../codex-auth/codex-profile-registry') as {
|
||||
CodexProfileRegistry: new () => {
|
||||
listProfiles: () => string[];
|
||||
getProfile: (name: string) => { email?: string; account_id?: string };
|
||||
};
|
||||
};
|
||||
const registry = new CodexProfileRegistry();
|
||||
const profiles = registry.listProfiles();
|
||||
const pausedIdentities = getPausedCodexAccountIdentities();
|
||||
const profiles = registry
|
||||
.listProfiles()
|
||||
.filter(
|
||||
(name) =>
|
||||
!codexProfileMatchesPausedAccount(name, registry.getProfile(name), pausedIdentities)
|
||||
);
|
||||
// Add the bare ~/.codex/auth.json (the default `ccsx` invocation) as the
|
||||
// DEFAULT_PROFILE account when it exists. It is the "default way of running",
|
||||
// distinct from named `ccsx <profile>` profiles.
|
||||
@@ -643,11 +650,23 @@ function getDefaultCodexProfileFromDisk(): string | null {
|
||||
const { CodexProfileRegistry } = require('../../codex-auth/codex-profile-registry') as {
|
||||
CodexProfileRegistry: new () => {
|
||||
getDefault: () => string | null;
|
||||
getProfile: (name: string) => { email?: string; account_id?: string };
|
||||
listProfiles: () => string[];
|
||||
};
|
||||
};
|
||||
const registry = new CodexProfileRegistry();
|
||||
const pausedIdentities = getPausedCodexAccountIdentities();
|
||||
const def = registry.getDefault();
|
||||
if (def) return def;
|
||||
if (def && !codexProfileMatchesPausedAccount(def, registry.getProfile(def), pausedIdentities)) {
|
||||
return def;
|
||||
}
|
||||
const fallback = registry
|
||||
.listProfiles()
|
||||
.find(
|
||||
(name) =>
|
||||
!codexProfileMatchesPausedAccount(name, registry.getProfile(name), pausedIdentities)
|
||||
);
|
||||
if (fallback) return fallback;
|
||||
// Fall back to the bare ~/.codex account (DEFAULT_PROFILE) when no registry
|
||||
// default is set — it is the default `ccsx` invocation.
|
||||
if (fs.existsSync(path.join(os.homedir(), '.codex', 'auth.json'))) return DEFAULT_PROFILE;
|
||||
@@ -657,6 +676,33 @@ function getDefaultCodexProfileFromDisk(): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function getPausedCodexAccountIdentities(): Set<string> | null {
|
||||
try {
|
||||
const accounts = getProviderAccounts('codex');
|
||||
if (accounts.length === 0) return null;
|
||||
const identities = new Set<string>();
|
||||
for (const account of accounts) {
|
||||
if (!account.paused) continue;
|
||||
identities.add(account.id);
|
||||
if (account.email) identities.add(account.email);
|
||||
}
|
||||
return identities;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function codexProfileMatchesPausedAccount(
|
||||
profileName: string,
|
||||
profile: { email?: string; account_id?: string },
|
||||
pausedIdentities: Set<string> | null
|
||||
): boolean {
|
||||
if (!pausedIdentities || pausedIdentities.size === 0) return false;
|
||||
return [profileName, profile.email, profile.account_id].some(
|
||||
(identity): identity is string => typeof identity === 'string' && pausedIdentities.has(identity)
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Per-profile collectors with full safety controls
|
||||
// ============================================================================
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
getNativeAccountRows,
|
||||
getCachedNativeAccountRows,
|
||||
@@ -467,6 +470,202 @@ function makeCodexDeps(
|
||||
// ============================================================================
|
||||
|
||||
describe('Codex network path', () => {
|
||||
it('production profile enumeration skips paused Codex accounts before live network refresh', async () => {
|
||||
const originalCcsHome = process.env.CCS_HOME;
|
||||
const originalHome = process.env.HOME;
|
||||
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-bar-paused-codex-'));
|
||||
process.env.CCS_HOME = tempHome;
|
||||
process.env.HOME = tempHome;
|
||||
|
||||
try {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
const cliproxyDir = path.join(ccsDir, 'cliproxy');
|
||||
fs.mkdirSync(cliproxyDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ccsDir, 'codex-profiles.yaml'),
|
||||
[
|
||||
'version: "1.0"',
|
||||
'default: paused',
|
||||
'profiles:',
|
||||
' paused:',
|
||||
' type: codex',
|
||||
' created: "2026-01-01T00:00:00.000Z"',
|
||||
' last_used: null',
|
||||
' email: paused-codex@example.com',
|
||||
' account_id: paused-codex@example.com',
|
||||
' active:',
|
||||
' type: codex',
|
||||
' created: "2026-01-02T00:00:00.000Z"',
|
||||
' last_used: null',
|
||||
' email: active-codex@example.com',
|
||||
' account_id: active-codex@example.com',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(cliproxyDir, 'accounts.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
providers: {
|
||||
codex: {
|
||||
default: 'paused-codex@example.com',
|
||||
accounts: {
|
||||
'paused-codex@example.com': {
|
||||
email: 'paused-codex@example.com',
|
||||
tokenFile: 'paused-codex@example.com.json',
|
||||
paused: true,
|
||||
},
|
||||
'active-codex@example.com': {
|
||||
email: 'active-codex@example.com',
|
||||
tokenFile: 'active-codex@example.com.json',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
fs.mkdirSync(path.join(cliproxyDir, 'auth'), { recursive: true });
|
||||
fs.mkdirSync(path.join(cliproxyDir, 'auth-paused'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(cliproxyDir, 'auth', 'active-codex@example.com.json'),
|
||||
JSON.stringify({ type: 'codex' })
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(cliproxyDir, 'auth-paused', 'paused-codex@example.com.json'),
|
||||
JSON.stringify({ type: 'codex' })
|
||||
);
|
||||
|
||||
const fetchedAccountIds: string[] = [];
|
||||
const deps = makeCodexDeps({
|
||||
clock: { now: 1_000_000 },
|
||||
listClaudeProfiles: () => [],
|
||||
readCredentials: undefined,
|
||||
getDefaultCodexAccountId: undefined,
|
||||
readCodexNativeAuth: (profile: string) => ({
|
||||
accessToken: `token-${profile}`,
|
||||
accountId: profile === 'active' ? 'active-codex@example.com' : 'paused-codex@example.com',
|
||||
}),
|
||||
fetchCodexNetworkQuota: async (accountId: string) => {
|
||||
fetchedAccountIds.push(accountId);
|
||||
return { ...codexSuccessQuota(), accountId };
|
||||
},
|
||||
});
|
||||
|
||||
const rows = await getNativeAccountRows(deps);
|
||||
|
||||
expect(fetchedAccountIds).toEqual(['active-codex@example.com']);
|
||||
expect(rows.find((row) => row.profile === 'paused')).toBeUndefined();
|
||||
const active = rows.find((row) => row.profile === 'active');
|
||||
expect(active?.paused).toBe(false);
|
||||
expect(active?.is_default).toBe(true);
|
||||
} finally {
|
||||
if (originalCcsHome !== undefined) {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
} else {
|
||||
delete process.env.CCS_HOME;
|
||||
}
|
||||
if (originalHome !== undefined) {
|
||||
process.env.HOME = originalHome;
|
||||
} else {
|
||||
delete process.env.HOME;
|
||||
}
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('production profile enumeration does not live-refresh when all Codex accounts are paused', async () => {
|
||||
const originalCcsHome = process.env.CCS_HOME;
|
||||
const originalHome = process.env.HOME;
|
||||
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-bar-all-paused-codex-'));
|
||||
process.env.CCS_HOME = tempHome;
|
||||
process.env.HOME = tempHome;
|
||||
|
||||
try {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
const cliproxyDir = path.join(ccsDir, 'cliproxy');
|
||||
fs.mkdirSync(cliproxyDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ccsDir, 'codex-profiles.yaml'),
|
||||
[
|
||||
'version: "1.0"',
|
||||
'default: paused',
|
||||
'profiles:',
|
||||
' paused:',
|
||||
' type: codex',
|
||||
' created: "2026-01-01T00:00:00.000Z"',
|
||||
' last_used: null',
|
||||
' email: paused-codex@example.com',
|
||||
' account_id: paused-codex@example.com',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(cliproxyDir, 'accounts.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
providers: {
|
||||
codex: {
|
||||
default: 'paused-codex@example.com',
|
||||
accounts: {
|
||||
'paused-codex@example.com': {
|
||||
email: 'paused-codex@example.com',
|
||||
tokenFile: 'paused-codex@example.com.json',
|
||||
paused: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
fs.mkdirSync(path.join(cliproxyDir, 'auth-paused'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(cliproxyDir, 'auth-paused', 'paused-codex@example.com.json'),
|
||||
JSON.stringify({ type: 'codex' })
|
||||
);
|
||||
|
||||
let networkCalls = 0;
|
||||
const deps = makeCodexDeps({
|
||||
clock: { now: 1_000_000 },
|
||||
listClaudeProfiles: () => [],
|
||||
readCredentials: undefined,
|
||||
getDefaultCodexAccountId: undefined,
|
||||
readCodexNativeAuth: (profile: string) => ({
|
||||
accessToken: `token-${profile}`,
|
||||
accountId: 'paused-codex@example.com',
|
||||
}),
|
||||
fetchCodexNetworkQuota: async () => {
|
||||
networkCalls += 1;
|
||||
return codexSuccessQuota();
|
||||
},
|
||||
});
|
||||
|
||||
const rows = await getNativeAccountRows(deps);
|
||||
|
||||
expect(networkCalls).toBe(0);
|
||||
expect(deps.localCount()).toBe(0);
|
||||
expect(rows.find((row) => row.profile === 'paused')).toBeUndefined();
|
||||
} finally {
|
||||
if (originalCcsHome !== undefined) {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
} else {
|
||||
delete process.env.CCS_HOME;
|
||||
}
|
||||
if (originalHome !== undefined) {
|
||||
process.env.HOME = originalHome;
|
||||
} else {
|
||||
delete process.env.HOME;
|
||||
}
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
it('network success builds a fresh row from coreUsage — no staleAsOf, health ok, correct windows', async () => {
|
||||
const clock = { now: 1_000_000 };
|
||||
const deps = makeCodexDeps({ clock });
|
||||
|
||||
Reference in new issue
Block a user