Files
ccs/tests/unit/codex-auth/codex-config-symlink.test.ts
T
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

108 lines
4.3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
let ensureSharedConfigSymlink: (profileDir: string, sharedConfigPath?: string) => void;
let tempDir: string;
let profileDir: string;
let sharedConfigPath: string;
beforeEach(async () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-symlink-test-'));
profileDir = path.join(tempDir, 'profile');
// Use a temp path for the shared config so tests never touch real ~/.codex/config.toml
sharedConfigPath = path.join(tempDir, 'shared-config.toml');
const mod = await import('../../../src/codex-auth/codex-config-symlink');
ensureSharedConfigSymlink = mod.ensureSharedConfigSymlink;
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe('ensureSharedConfigSymlink', () => {
it('creates empty shared config and symlink when neither exists', () => {
// Neither profileDir nor sharedConfigPath exist yet
ensureSharedConfigSymlink(profileDir, sharedConfigPath);
expect(fs.existsSync(sharedConfigPath)).toBe(true);
expect(fs.readFileSync(sharedConfigPath, 'utf8')).toBe('');
const linkPath = path.join(profileDir, 'config.toml');
const stat = fs.lstatSync(linkPath);
expect(stat.isSymbolicLink()).toBe(true);
expect(fs.readlinkSync(linkPath)).toBe(sharedConfigPath);
});
it('is idempotent when symlink already points to correct target', () => {
ensureSharedConfigSymlink(profileDir, sharedConfigPath);
// Call again — should not throw
expect(() => ensureSharedConfigSymlink(profileDir, sharedConfigPath)).not.toThrow();
const linkPath = path.join(profileDir, 'config.toml');
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
expect(fs.readlinkSync(linkPath)).toBe(sharedConfigPath);
});
it('preserves existing shared config content (does not overwrite)', () => {
fs.writeFileSync(sharedConfigPath, '[model]\nname = "o4"', { mode: 0o600 });
ensureSharedConfigSymlink(profileDir, sharedConfigPath);
expect(fs.readFileSync(sharedConfigPath, 'utf8')).toBe('[model]\nname = "o4"');
});
it('replaces a stale symlink pointing to a wrong target with correct one', () => {
fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
const wrongTarget = path.join(tempDir, 'wrong.toml');
fs.writeFileSync(wrongTarget, '', { mode: 0o600 });
const linkPath = path.join(profileDir, 'config.toml');
fs.symlinkSync(wrongTarget, linkPath);
ensureSharedConfigSymlink(profileDir, sharedConfigPath);
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
expect(fs.readlinkSync(linkPath)).toBe(sharedConfigPath);
});
it('replaces a regular file at link path with symlink (with warning)', () => {
fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
const linkPath = path.join(profileDir, 'config.toml');
fs.writeFileSync(linkPath, '[existing]\ndata = true', { mode: 0o600 });
// Capture stderr to verify warning was written
const stderrChunks: string[] = [];
const origWrite = process.stderr.write.bind(process.stderr);
process.stderr.write = (chunk: string | Uint8Array): boolean => {
stderrChunks.push(typeof chunk === 'string' ? chunk : chunk.toString());
return origWrite(chunk);
};
try {
ensureSharedConfigSymlink(profileDir, sharedConfigPath);
} finally {
process.stderr.write = origWrite;
}
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
expect(fs.readlinkSync(linkPath)).toBe(sharedConfigPath);
// A warning should have been emitted
expect(stderrChunks.join('')).toMatch(/overwr|replaced|regular file/i);
});
it('replaces a broken symlink (dangling) with correct symlink', () => {
fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
const linkPath = path.join(profileDir, 'config.toml');
// Create symlink to non-existent target
fs.symlinkSync(path.join(tempDir, 'does-not-exist.toml'), linkPath);
// Verify it's broken
expect(fs.existsSync(linkPath)).toBe(false);
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
ensureSharedConfigSymlink(profileDir, sharedConfigPath);
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
expect(fs.readlinkSync(linkPath)).toBe(sharedConfigPath);
});
});