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); }); });