From e461c7a67e1500e28a7fa8266cd90b80212c760b Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 17 May 2026 18:33:20 -0400 Subject: [PATCH] fix(codex-auth): address persistent review focus --- .../codex-auth-dashboard-service.ts | 8 +++++ src/codex-auth/commands/login-command.ts | 12 ++++---- .../codex-auth-dashboard-service.test.ts | 17 +++++++++++ .../codex-auth/commands/login-command.test.ts | 30 +++++++++++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/codex-auth/codex-auth-dashboard-service.ts b/src/codex-auth/codex-auth-dashboard-service.ts index aaa56bbd..c2801a7e 100644 --- a/src/codex-auth/codex-auth-dashboard-service.ts +++ b/src/codex-auth/codex-auth-dashboard-service.ts @@ -193,6 +193,14 @@ function resolveActive(registry: CodexProfileData): CodexAuthActiveProfile | nul // Precedence 2: $CCS_CODEX_PROFILE env var const profileEnv = (process.env.CCS_CODEX_PROFILE ?? '').trim(); if (profileEnv) { + if (!Object.prototype.hasOwnProperty.call(registry.profiles, profileEnv)) { + logger.warn( + 'codex-auth.dashboard.stale-env-profile', + 'Ignoring CCS_CODEX_PROFILE because it is not registered', + { profileName: profileEnv } + ); + return null; + } return { name: profileEnv, source: 'env', diff --git a/src/codex-auth/commands/login-command.ts b/src/codex-auth/commands/login-command.ts index 99cf54d4..d763c99b 100644 --- a/src/codex-auth/commands/login-command.ts +++ b/src/codex-auth/commands/login-command.ts @@ -16,6 +16,7 @@ import { resolveCodexProfileDir, ensureSharedConfigSymlink } from '../index'; import { decodeAccountIdentity } from '../codex-account-identity'; import { detectCodexCli } from '../../targets/codex-detector'; import { parseArgs, rejectUnsupportedOptions, getProfileNameError } from './types'; +import type { CodexProfileMetadata } from '../types'; import type { CodexCommandContext } from './types'; const logger = createLogger('codex-auth:cmd:login'); @@ -98,12 +99,11 @@ export async function handleLoginCodex(ctx: CodexCommandContext, args: string[]) if (exitCode === 0 && fs.existsSync(authJsonPath)) { const identity = decodeAccountIdentity(authJsonPath); const now = new Date().toISOString(); - registry.updateProfile(profileName, { - last_used: now, - email: identity.email, - plan_type: identity.plan_type ?? null, - account_id: identity.account_id, - }); + const metadataUpdate: Partial = { last_used: now }; + if (identity.email !== undefined) metadataUpdate.email = identity.email; + if (identity.plan_type !== undefined) metadataUpdate.plan_type = identity.plan_type; + if (identity.account_id !== undefined) metadataUpdate.account_id = identity.account_id; + registry.updateProfile(profileName, metadataUpdate); const emailStr = identity.email ?? ''; const planStr = identity.plan_type ? ` (plan: ${identity.plan_type})` : ''; console.log(ok(`Logged in as ${emailStr}${planStr}`)); diff --git a/tests/unit/codex-auth/codex-auth-dashboard-service.test.ts b/tests/unit/codex-auth/codex-auth-dashboard-service.test.ts index df32c116..7940f0ad 100644 --- a/tests/unit/codex-auth/codex-auth-dashboard-service.test.ts +++ b/tests/unit/codex-auth/codex-auth-dashboard-service.test.ts @@ -363,6 +363,23 @@ describe('getCodexAuthProfilesSummary', () => { expect(result.active?.name).toBe('work'); }); + it('active resolution: ignores stale CCS_CODEX_PROFILE values missing from registry', async () => { + const { getCodexAuthProfilesSummary, invalidateCodexAuthProfilesCache } = await importService(); + invalidateCodexAuthProfilesCache(); + + process.env.CCS_CODEX_PROFILE = 'ghost'; + + const registryPath = path.join(ccsDir, 'codex-profiles.yaml'); + fs.writeFileSync( + registryPath, + `version: "1.0"\ndefault: null\nprofiles:\n work:\n type: codex\n created: "2026-01-01T00:00:00Z"\n last_used: null\n`, + { mode: 0o600 } + ); + + const result = await getCodexAuthProfilesSummary(); + expect(result.active).toBeNull(); + }); + it('active resolution: registry default set -> source=default', async () => { const { getCodexAuthProfilesSummary, invalidateCodexAuthProfilesCache } = await importService(); invalidateCodexAuthProfilesCache(); diff --git a/tests/unit/codex-auth/commands/login-command.test.ts b/tests/unit/codex-auth/commands/login-command.test.ts index 11782428..ff7bd033 100644 --- a/tests/unit/codex-auth/commands/login-command.test.ts +++ b/tests/unit/codex-auth/commands/login-command.test.ts @@ -217,6 +217,36 @@ describe('handleLoginCodex — clean exit updates registry', () => { expect(meta.last_used).toBeTruthy(); }); + it('preserves cached identity metadata when a re-login token is sparse', async () => { + const detectorMod = await import('../../../../src/targets/codex-detector'); + spyOn(detectorMod, 'detectCodexCli').mockReturnValue('/usr/bin/codex'); + spawnReturnsCode(0, true); // writes auth.json with a valid but sparse JWT payload + + const { handleLoginCodex } = await import('../../../../src/codex-auth/commands/login-command'); + const ctx = await makeCtx(); + ctx.registry.createProfile('preservemeta', { + created: new Date().toISOString(), + last_used: null, + email: 'cached@example.com', + plan_type: 'pro', + account_id: 'acct-cached', + }); + + const origLog = console.log; + console.log = () => {}; + try { + await handleLoginCodex(ctx, ['preservemeta']); + } finally { + console.log = origLog; + } + + const meta = ctx.registry.getProfile('preservemeta'); + expect(meta.last_used).toBeTruthy(); + expect(meta.email).toBe('cached@example.com'); + expect(meta.plan_type).toBe('pro'); + expect(meta.account_id).toBe('acct-cached'); + }); + it('persists account_id when login token has account_id only', async () => { const detectorMod = await import('../../../../src/targets/codex-detector'); spyOn(detectorMod, 'detectCodexCli').mockReturnValue('/usr/bin/codex');