import { describe, expect, it, vi } from 'vitest'; import { buildSpawnSpec, diagnoseClaudeBin, INHERITED_CLAUDE_MARKERS, resolveClaudeBin, sanitizeInheritedEnv } from '../src/core/claude-launcher.js'; // resolveClaudeBin() fait `which claude` : on le stub pour ne pas dépendre d'un claude réel en PATH. vi.mock('node:child_process', () => ({ execFileSync: () => '/usr/bin/claude\n' })); describe('buildSpawnSpec · session de groupe multi-repo (P6)', () => { it('émet un --add-dir par répertoire supplémentaire (claude)', () => { const spec = buildSpawnSpec({ command: 'claude', addDirs: ['/a', '/b', '/c'] }); expect(spec.file).toBe('/usr/bin/claude'); expect(spec.args).toEqual(['--add-dir', '/a', '--add-dir', '/b', '--add-dir', '/c']); }); it('combine --resume et --add-dir (reprise d’une session de groupe)', () => { const spec = buildSpawnSpec({ command: 'claude', resume: { claudeSessionId: 'sid' }, addDirs: ['/x'] }); expect(spec.args).toEqual(['--resume', 'sid', '--add-dir', '/x']); }); it('aucun --add-dir quand addDirs est vide/absent', () => { expect(buildSpawnSpec({ command: 'claude' }).args).toEqual([]); expect(buildSpawnSpec({ command: 'claude', addDirs: [] }).args).toEqual([]); }); it('ignore addDirs pour bash (pas de --add-dir)', () => { const spec = buildSpawnSpec({ command: 'bash', addDirs: ['/a', '/b'] }); expect(spec.file).toBe('bash'); expect(spec.args).toEqual(['--norc']); }); }); describe('resolveClaudeBin / diagnoseClaudeBin · override de chemin (réglage UI)', () => { // process.execPath (node) est un fichier exécutable réel garanti sur la machine de test. const realBin = process.execPath; it('buildSpawnSpec utilise le chemin configuré tel quel quand fourni', () => { const spec = buildSpawnSpec({ command: 'claude', claudeBinPath: realBin }); expect(spec.file).toBe(realBin); }); it('resolveClaudeBin retombe sur le PATH (which) sans override', () => { expect(resolveClaudeBin()).toBe('/usr/bin/claude'); expect(resolveClaudeBin(null)).toBe('/usr/bin/claude'); }); it('resolveClaudeBin throw si le chemin configuré n’est pas exécutable', () => { expect(() => resolveClaudeBin('/no/such/claude')).toThrow(/not executable/); }); it('diagnoseClaudeBin : configuré+exécutable → ok ; configuré absent → !ok ; sinon PATH', () => { expect(diagnoseClaudeBin(realBin)).toEqual({ path: realBin, source: 'configured', ok: true }); expect(diagnoseClaudeBin('/no/such/claude')).toEqual({ path: '/no/such/claude', source: 'configured', ok: false }); expect(diagnoseClaudeBin()).toEqual({ path: '/usr/bin/claude', source: 'path', ok: true }); }); }); // Régression vécue : l'app de bureau avait été lancée depuis une session Claude Code, donc le daemon // héritait de `CLAUDE_CODE_CHILD_SESSION=1` et le repassait à CHAQUE session lancée. Le CLI se croyait // sous-session et coupait la sauvegarde du transcript : « Transcript saving is off », plus aucun // historique, plus de `--resume`, et `claudeSessionId` restait null (l'état fin tombe avec lui). describe('sanitizeInheritedEnv · marqueurs de session parente', () => { const polluted = { PATH: '/usr/bin', CLAUDECODE: '1', CLAUDE_CODE_CHILD_SESSION: '1', CLAUDE_CODE_SESSION_ID: 'afad72f8-a987-462a-9406-7fd144e05905', CLAUDE_CODE_ENTRYPOINT: 'cli', CLAUDE_CODE_EXECPATH: '/home/u/.local/share/claude/versions/2.1.222', CLAUDE_PID: '1813704', CLAUDE_EFFORT: 'xhigh', CLAUDE_CONFIG_DIR: '/home/u/.claude', ANTHROPIC_API_KEY: 'sk-test', HTTPS_PROXY: 'http://proxy:3128', }; it('retire les marqueurs d’exécution hérités', () => { const env = sanitizeInheritedEnv(polluted); for (const key of INHERITED_CLAUDE_MARKERS) expect(env[key]).toBeUndefined(); }); it('conserve la configuration légitime de l’utilisateur', () => { const env = sanitizeInheritedEnv(polluted); expect(env.CLAUDE_CONFIG_DIR).toBe('/home/u/.claude'); expect(env.ANTHROPIC_API_KEY).toBe('sk-test'); expect(env.HTTPS_PROXY).toBe('http://proxy:3128'); expect(env.PATH).toBe('/usr/bin'); }); it('ne mute pas la source', () => { const copy = { ...polluted }; sanitizeInheritedEnv(copy); expect(copy.CLAUDE_CODE_CHILD_SESSION).toBe('1'); }); it('buildSpawnSpec assainit l’env, pour claude ET pour le shell', () => { const previous = process.env.CLAUDE_CODE_CHILD_SESSION; process.env.CLAUDE_CODE_CHILD_SESSION = '1'; try { expect(buildSpawnSpec({ command: 'claude' }).env.CLAUDE_CODE_CHILD_SESSION).toBeUndefined(); // un `claude` tapé à la main dans ce shell hériterait sinon du même marqueur expect(buildSpawnSpec({ command: 'bash' }).env.CLAUDE_CODE_CHILD_SESSION).toBeUndefined(); expect(buildSpawnSpec({ command: 'claude' }).env.TERM).toBe('xterm-256color'); } finally { if (previous === undefined) delete process.env.CLAUDE_CODE_CHILD_SESSION; else process.env.CLAUDE_CODE_CHILD_SESSION = previous; } }); });