Files
ccs/tests/unit/codex-auth/codex-profile-paths.test.ts
Tam Nhu Tran 358b703d98 feat(codex-auth): add profile registry + storage foundation
Adds the storage substrate for ccsx auth profile isolation.
Each Codex profile gets its own CODEX_HOME dir under
~/.ccs/codex-instances/<name>/ with isolated auth.json and
history.jsonl; config.toml is shared via symlink to ~/.codex/config.toml
so two terminals can run two Codex accounts simultaneously without
duplicating user config.

- CodexProfileRegistry (YAML, atomic write tmp.<pid>.<rand> + rename,
  orphan cleanup, full CRUD + default pointer)
- decode-id-token: pure base64 JWT decoder for OpenAI id_token,
  reads nested https://api.openai.com/auth claims (chatgpt_plan_type,
  chatgpt_account_id) and dual-path email
- ensureSharedConfigSymlink: self-healing, idempotent, overwrites
  stale entries with stderr warning
- 45 unit tests, all green

Foundation only — no CLI, no runtime injection, no dashboard.
Subsequent commits wire those in.
2026-05-17 14:44:07 -04:00

65 lines
2.5 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as os from 'os';
import * as path from 'path';
let getCodexAuthRegistryPath: () => string;
let getCodexInstancesDir: () => string;
let resolveCodexProfileDir: (name: string) => string;
let getSharedCodexConfigPath: () => string;
const ORIGINAL_CCS_HOME = process.env.CCS_HOME;
beforeEach(async () => {
process.env.CCS_HOME = '/tmp/test-ccs-home';
// Re-import to pick up env change — use dynamic import with cache busting
const mod = await import('../../../src/codex-auth/codex-profile-paths');
getCodexAuthRegistryPath = mod.getCodexAuthRegistryPath;
getCodexInstancesDir = mod.getCodexInstancesDir;
resolveCodexProfileDir = mod.resolveCodexProfileDir;
getSharedCodexConfigPath = mod.getSharedCodexConfigPath;
});
afterEach(() => {
if (ORIGINAL_CCS_HOME === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = ORIGINAL_CCS_HOME;
}
});
describe('codex-profile-paths', () => {
it('getCodexAuthRegistryPath returns codex-profiles.yaml inside getCcsDir()', () => {
const result = getCodexAuthRegistryPath();
expect(result).toContain('codex-profiles.yaml');
expect(result).toContain('.ccs');
});
it('getCodexInstancesDir returns codex-instances inside getCcsDir()', () => {
const result = getCodexInstancesDir();
expect(result).toContain('codex-instances');
expect(result).toContain('.ccs');
});
it('resolveCodexProfileDir returns instancesDir/<name>', () => {
const instancesDir = getCodexInstancesDir();
const profileDir = resolveCodexProfileDir('work');
expect(profileDir).toBe(path.join(instancesDir, 'work'));
});
it('resolveCodexProfileDir correctly nests a different profile name', () => {
const instancesDir = getCodexInstancesDir();
const profileDir = resolveCodexProfileDir('personal');
expect(profileDir).toBe(path.join(instancesDir, 'personal'));
});
it('getSharedCodexConfigPath resolves under os.homedir() not getCcsDir()', () => {
const result = getSharedCodexConfigPath();
// Must equal os.homedir()/.codex/config.toml — uses real homedir, not getCcsDir()
expect(result).toBe(path.join(os.homedir(), '.codex', 'config.toml'));
// Must end with the Codex-canonical path fragment
expect(result).toMatch(/\.codex[/\\]config\.toml$/);
// Must NOT end inside the .ccs directory (i.e. not a CCS-owned path)
expect(result).not.toContain(path.join('.ccs', 'codex'));
});
});