mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 22:14:22 +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';
|
} from './claude-native-credentials';
|
||||||
import { fetchClaudeQuotaWithToken } from '../../cliproxy/quota/quota-fetcher-claude';
|
import { fetchClaudeQuotaWithToken } from '../../cliproxy/quota/quota-fetcher-claude';
|
||||||
import { fetchCodexQuota } from '../../cliproxy/quota/quota-fetcher-codex';
|
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 { getCodexLocalQuota, type CodexLocalQuota } from './codex-local-quota-collector';
|
||||||
import type { ClaudeQuotaResult, CodexQuotaResult } from '../../cliproxy/quota/quota-types';
|
import type { ClaudeQuotaResult, CodexQuotaResult } from '../../cliproxy/quota/quota-types';
|
||||||
import type { BarSummaryRow, QuotaWindowDetail } from '../routes/bar-routes';
|
import type { BarSummaryRow, QuotaWindowDetail } from '../routes/bar-routes';
|
||||||
@@ -618,10 +618,17 @@ function listCodexProfilesFromDisk(): string[] {
|
|||||||
const { CodexProfileRegistry } = require('../../codex-auth/codex-profile-registry') as {
|
const { CodexProfileRegistry } = require('../../codex-auth/codex-profile-registry') as {
|
||||||
CodexProfileRegistry: new () => {
|
CodexProfileRegistry: new () => {
|
||||||
listProfiles: () => string[];
|
listProfiles: () => string[];
|
||||||
|
getProfile: (name: string) => { email?: string; account_id?: string };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const registry = new CodexProfileRegistry();
|
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
|
// 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",
|
// DEFAULT_PROFILE account when it exists. It is the "default way of running",
|
||||||
// distinct from named `ccsx <profile>` profiles.
|
// distinct from named `ccsx <profile>` profiles.
|
||||||
@@ -643,11 +650,23 @@ function getDefaultCodexProfileFromDisk(): string | null {
|
|||||||
const { CodexProfileRegistry } = require('../../codex-auth/codex-profile-registry') as {
|
const { CodexProfileRegistry } = require('../../codex-auth/codex-profile-registry') as {
|
||||||
CodexProfileRegistry: new () => {
|
CodexProfileRegistry: new () => {
|
||||||
getDefault: () => string | null;
|
getDefault: () => string | null;
|
||||||
|
getProfile: (name: string) => { email?: string; account_id?: string };
|
||||||
|
listProfiles: () => string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const registry = new CodexProfileRegistry();
|
const registry = new CodexProfileRegistry();
|
||||||
|
const pausedIdentities = getPausedCodexAccountIdentities();
|
||||||
const def = registry.getDefault();
|
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
|
// Fall back to the bare ~/.codex account (DEFAULT_PROFILE) when no registry
|
||||||
// default is set — it is the default `ccsx` invocation.
|
// default is set — it is the default `ccsx` invocation.
|
||||||
if (fs.existsSync(path.join(os.homedir(), '.codex', 'auth.json'))) return DEFAULT_PROFILE;
|
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
|
// Per-profile collectors with full safety controls
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { beforeEach, describe, expect, it } from 'bun:test';
|
import { beforeEach, describe, expect, it } from 'bun:test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
import {
|
import {
|
||||||
getNativeAccountRows,
|
getNativeAccountRows,
|
||||||
getCachedNativeAccountRows,
|
getCachedNativeAccountRows,
|
||||||
@@ -467,6 +470,202 @@ function makeCodexDeps(
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
describe('Codex network path', () => {
|
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 () => {
|
it('network success builds a fresh row from coreUsage — no staleAsOf, health ok, correct windows', async () => {
|
||||||
const clock = { now: 1_000_000 };
|
const clock = { now: 1_000_000 };
|
||||||
const deps = makeCodexDeps({ clock });
|
const deps = makeCodexDeps({ clock });
|
||||||
|
|||||||
Reference in new issue
Block a user