import { describe, expect, it, spyOn } from 'bun:test'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { runWithScopedCcsHome } from '../../../src/utils/config-manager'; async function withIsolatedHome(fn: (homeDir: string) => Promise | T): Promise { const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-account-registry-')); try { return await runWithScopedCcsHome(homeDir, () => fn(homeDir)); } finally { fs.rmSync(homeDir, { recursive: true, force: true }); } } async function loadRegistryModule() { return import(`../../../src/cliproxy/accounts/registry?registry-integrity=${Date.now()}`); } async function loadAccountManager() { return import(`../../../src/cliproxy/account-manager?account-registry-integrity=${Date.now()}`); } async function captureConsoleError(fn: () => Promise | T): Promise<{ result: T; messages: string[] }> { const originalError = console.error; const messages: string[] = []; console.error = ((...args: unknown[]) => { messages.push(args.map(String).join(' ')); }) as typeof console.error; try { return { result: await fn(), messages, }; } finally { console.error = originalError; } } function listRegistryBackups(registryPath: string): string[] { return fs .readdirSync(path.dirname(registryPath)) .filter((file) => /^accounts\.json\.corrupt-.*\.bak$/.test(file)); } describe('account registry integrity', () => { it('does not create accounts.json during no-op discovery', async () => { await withIsolatedHome(async (homeDir) => { const authDir = path.join(homeDir, '.ccs', 'cliproxy', 'auth'); const registryPath = path.join(homeDir, '.ccs', 'cliproxy', 'accounts.json'); fs.mkdirSync(authDir, { recursive: true }); const { discoverExistingAccounts } = await loadRegistryModule(); discoverExistingAccounts(); expect(fs.existsSync(registryPath)).toBe(false); }); }); it('does not write accounts.json during provider account reads', async () => { await withIsolatedHome(async (homeDir) => { const authDir = path.join(homeDir, '.ccs', 'cliproxy', 'auth'); const registryPath = path.join(homeDir, '.ccs', 'cliproxy', 'accounts.json'); fs.mkdirSync(authDir, { recursive: true }); const { getProviderAccounts } = await loadAccountManager(); expect(getProviderAccounts('kiro')).toEqual([]); expect(fs.existsSync(registryPath)).toBe(false); }); }); it('removes stale accounts before choosing the next default during registration', async () => { await withIsolatedHome(async (homeDir) => { const cliproxyDir = path.join(homeDir, '.ccs', 'cliproxy'); const authDir = path.join(cliproxyDir, 'auth'); const registryPath = path.join(cliproxyDir, 'accounts.json'); fs.mkdirSync(authDir, { recursive: true }); fs.writeFileSync( path.join(authDir, 'kiro-github-ABC123.json'), JSON.stringify({ type: 'kiro' }) ); fs.writeFileSync( registryPath, JSON.stringify({ version: 1, providers: { kiro: { default: 'github-OLD999', accounts: { 'github-OLD999': { nickname: 'old', tokenFile: 'kiro-github-OLD999.json', createdAt: '2025-01-01T00:00:00.000Z', lastUsedAt: '2025-01-01T00:00:00.000Z', }, }, }, }, }), 'utf8' ); const { registerAccount } = await loadAccountManager(); const account = registerAccount('kiro', 'kiro-github-ABC123.json'); const { loadAccountsRegistry } = await loadRegistryModule(); const registry = loadAccountsRegistry(); const kiroAccounts = registry.providers.kiro; expect(account.id).toBe('github-ABC123'); expect(kiroAccounts?.default).toBe('github-ABC123'); expect(kiroAccounts?.accounts['github-OLD999']).toBeUndefined(); expect(Object.keys(kiroAccounts?.accounts ?? {})).toEqual(['github-ABC123']); }); }); it('recovers corrupted accounts.json from token-backed accounts and preserves a backup', async () => { await withIsolatedHome(async (homeDir) => { const authDir = path.join(homeDir, '.ccs', 'cliproxy', 'auth'); const registryPath = path.join(homeDir, '.ccs', 'cliproxy', 'accounts.json'); fs.mkdirSync(authDir, { recursive: true }); fs.writeFileSync( path.join(authDir, 'codex-user@example.com.json'), JSON.stringify({ type: 'codex', email: 'user@example.com' }), 'utf8' ); fs.writeFileSync(registryPath, '{not-valid-json', 'utf8'); const { getProviderAccounts } = await loadAccountManager(); const accounts = getProviderAccounts('codex'); const { loadAccountsRegistry } = await loadRegistryModule(); const registry = loadAccountsRegistry(); expect(accounts).toHaveLength(1); expect(accounts[0]?.id).toBe('user@example.com'); expect(accounts[0]?.isDefault).toBe(true); expect(registry.providers.codex?.accounts['user@example.com']?.tokenFile).toBe( 'codex-user@example.com.json' ); const backups = listRegistryBackups(registryPath); expect(backups).toHaveLength(1); expect( fs.readFileSync(path.join(path.dirname(registryPath), backups[0] as string), 'utf8') ).toBe('{not-valid-json'); }); }); it('recovers paused token-backed accounts when rebuilding a corrupted registry', async () => { await withIsolatedHome(async (homeDir) => { const pausedDir = path.join(homeDir, '.ccs', 'cliproxy', 'auth-paused'); const registryPath = path.join(homeDir, '.ccs', 'cliproxy', 'accounts.json'); fs.mkdirSync(pausedDir, { recursive: true }); fs.writeFileSync( path.join(pausedDir, 'codex-paused@example.com.json'), JSON.stringify({ type: 'codex', email: 'paused@example.com' }), 'utf8' ); fs.writeFileSync(registryPath, '{"providers":', 'utf8'); const { getProviderAccounts } = await loadAccountManager(); const accounts = getProviderAccounts('codex'); expect(accounts).toHaveLength(1); expect(accounts[0]?.id).toBe('paused@example.com'); expect(accounts[0]?.paused).toBe(true); }); }); it('surfaces skipped token files only during corruption recovery', async () => { await withIsolatedHome(async (homeDir) => { const authDir = path.join(homeDir, '.ccs', 'cliproxy', 'auth'); const registryPath = path.join(homeDir, '.ccs', 'cliproxy', 'accounts.json'); fs.mkdirSync(authDir, { recursive: true }); fs.writeFileSync( path.join(authDir, 'codex-valid@example.com.json'), JSON.stringify({ type: 'codex', email: 'valid@example.com' }), 'utf8' ); fs.writeFileSync( path.join(authDir, 'codex-invalid@example.com.json'), JSON.stringify({ type: 123, email: 'invalid@example.com' }), 'utf8' ); fs.writeFileSync(registryPath, '{"providers":', 'utf8'); const { getProviderAccounts } = await loadAccountManager(); const { result: accounts, messages } = await captureConsoleError(() => getProviderAccounts('codex') ); expect(accounts).toHaveLength(1); expect(accounts[0]?.id).toBe('valid@example.com'); expect(messages).toHaveLength(1); expect(messages[0]).toContain('Recovered corrupted account registry'); expect(messages[0]).toContain('auth/codex-invalid@example.com.json'); expect(messages[0]).toContain('invalid token type'); }); }); it('logs successful corruption recovery even when every token file is recoverable', async () => { await withIsolatedHome(async (homeDir) => { const authDir = path.join(homeDir, '.ccs', 'cliproxy', 'auth'); const registryPath = path.join(homeDir, '.ccs', 'cliproxy', 'accounts.json'); fs.mkdirSync(authDir, { recursive: true }); fs.writeFileSync( path.join(authDir, 'codex-clean@example.com.json'), JSON.stringify({ type: 'codex', email: 'clean@example.com' }), 'utf8' ); fs.writeFileSync(registryPath, '{"providers":', 'utf8'); const { getProviderAccounts } = await loadAccountManager(); const { result: accounts, messages } = await captureConsoleError(() => getProviderAccounts('codex') ); expect(accounts).toHaveLength(1); expect(messages).toHaveLength(1); expect(messages[0]).toContain('Recovered corrupted account registry'); expect(messages[0]).toContain('backup saved to'); expect(messages[0]).not.toContain('skipped'); }); }); it('prefers active tokens over paused duplicates during corruption recovery', async () => { await withIsolatedHome(async (homeDir) => { const cliproxyDir = path.join(homeDir, '.ccs', 'cliproxy'); const authDir = path.join(cliproxyDir, 'auth'); const pausedDir = path.join(cliproxyDir, 'auth-paused'); const registryPath = path.join(cliproxyDir, 'accounts.json'); fs.mkdirSync(authDir, { recursive: true }); fs.mkdirSync(pausedDir, { recursive: true }); fs.writeFileSync( path.join(authDir, 'codex-shared@example.com.json'), JSON.stringify({ type: 'codex', email: 'active@example.com' }), 'utf8' ); fs.writeFileSync( path.join(pausedDir, 'codex-shared@example.com.json'), JSON.stringify({ type: 'codex', email: 'paused@example.com' }), 'utf8' ); fs.writeFileSync(registryPath, '{"providers":', 'utf8'); const { getProviderAccounts } = await loadAccountManager(); const accounts = getProviderAccounts('codex'); expect(accounts).toHaveLength(1); expect(accounts[0]?.id).toBe('active@example.com'); expect(accounts[0]?.tokenFile).toBe('codex-shared@example.com.json'); expect(accounts[0]?.paused).toBeUndefined(); }); }); it('infers subdomain emails from token file names during corruption recovery', async () => { await withIsolatedHome(async (homeDir) => { const authDir = path.join(homeDir, '.ccs', 'cliproxy', 'auth'); const registryPath = path.join(homeDir, '.ccs', 'cliproxy', 'accounts.json'); fs.mkdirSync(authDir, { recursive: true }); fs.writeFileSync( path.join(authDir, 'codex-user@mail.example.com.json'), JSON.stringify({ type: 'codex' }), 'utf8' ); fs.writeFileSync(registryPath, '{"providers":', 'utf8'); const { getProviderAccounts } = await loadAccountManager(); const accounts = getProviderAccounts('codex'); expect(accounts).toHaveLength(1); expect(accounts[0]?.id).toBe('user@mail.example.com'); expect(accounts[0]?.email).toBe('user@mail.example.com'); expect(accounts[0]?.tokenFile).toBe('codex-user@mail.example.com.json'); }); }); it('preserves the corrupted registry backup when recovery cannot rewrite accounts.json', async () => { await withIsolatedHome(async (homeDir) => { const authDir = path.join(homeDir, '.ccs', 'cliproxy', 'auth'); const registryPath = path.join(homeDir, '.ccs', 'cliproxy', 'accounts.json'); fs.mkdirSync(authDir, { recursive: true }); fs.writeFileSync( path.join(authDir, 'codex-user@example.com.json'), JSON.stringify({ type: 'codex', email: 'user@example.com' }), 'utf8' ); fs.writeFileSync(registryPath, '{"providers":', 'utf8'); const originalWriteFileSync = fs.writeFileSync; const writeSpy = spyOn(fs, 'writeFileSync').mockImplementation(((file, data, options) => { if (String(file).includes('accounts.json.tmp.')) { throw new Error('simulated recovery write failure'); } return originalWriteFileSync(file, data, options as never); }) as typeof fs.writeFileSync); try { const { loadAccountsRegistry } = await loadRegistryModule(); expect(() => loadAccountsRegistry()).toThrow('simulated recovery write failure'); } finally { writeSpy.mockRestore(); } expect(fs.existsSync(registryPath)).toBe(false); const backups = listRegistryBackups(registryPath); expect(backups).toHaveLength(1); expect( fs.readFileSync(path.join(path.dirname(registryPath), backups[0] as string), 'utf8') ).toBe('{"providers":'); const { loadAccountsRegistry } = await loadRegistryModule(); expect(loadAccountsRegistry()).toEqual({ version: 1, providers: {}, }); }); }); it('does not repopulate paused tokens during normal startup discovery', async () => { await withIsolatedHome(async (homeDir) => { const pausedDir = path.join(homeDir, '.ccs', 'cliproxy', 'auth-paused'); const registryPath = path.join(homeDir, '.ccs', 'cliproxy', 'accounts.json'); fs.mkdirSync(pausedDir, { recursive: true }); fs.writeFileSync( path.join(pausedDir, 'codex-paused@example.com.json'), JSON.stringify({ type: 'codex', email: 'paused@example.com' }), 'utf8' ); const { discoverExistingAccounts } = await loadRegistryModule(); discoverExistingAccounts(); expect(fs.existsSync(registryPath)).toBe(false); const { getProviderAccounts } = await loadAccountManager(); expect(getProviderAccounts('codex')).toEqual([]); }); }); it('recovers to an empty registry when no token files are available', async () => { await withIsolatedHome(async (homeDir) => { const cliproxyDir = path.join(homeDir, '.ccs', 'cliproxy'); const registryPath = path.join(cliproxyDir, 'accounts.json'); fs.mkdirSync(cliproxyDir, { recursive: true }); fs.writeFileSync(registryPath, '{"providers":', 'utf8'); const { loadAccountsRegistry } = await loadRegistryModule(); const { result: registry, messages } = await captureConsoleError(() => loadAccountsRegistry()); expect(registry).toEqual({ version: 1, providers: {}, }); expect(fs.existsSync(registryPath)).toBe(true); expect(messages).toHaveLength(1); expect(messages[0]).toContain('Recovered corrupted account registry'); expect(messages[0]).toContain('backup saved to'); expect(listRegistryBackups(registryPath)).toHaveLength(1); }); }); });