Deux défauts vécus sur le poste, tous deux « invisibles » jusqu'à ce qu'on regarde. 1. Une console Claude ouverte depuis l'app affichait « Transcript saving is off, inherited CLAUDE_CODE_CHILD_SESSION marker ». Le daemon avait été lancé depuis une session Claude Code, il héritait donc de ses marqueurs d'exécution et les repassait à CHAQUE session qu'il lance. Le CLI se croyait sous-session et coupait la sauvegarde de son transcript : plus d'historique, plus de --resume, claudeSessionId restant null (et avec lui l'état fin busy/waiting/idle, ce qui explique les sessions sans activité détectée). L'environnement des PTY est desormais assaini de ces marqueurs, pour `claude` comme pour les shells (un `claude` tapé à la main en héritait aussi). La configuration légitime de l'utilisateur (CLAUDE_CONFIG_DIR, ANTHROPIC_*, proxies) passe intacte. Vérifié par acceptance-p17 : le daemon de test est lancé avec un environnement volontairement pollué, et le PTY n'en voit plus rien. 2. Une mise à jour installée à chaud demandait encore une manipulation. La 0.2.4 détectait le remplacement du binaire et proposait un dialogue « Restart now » : le travail restait à la charge de l'utilisateur. L'app redémarre maintenant d'elle-même quand cela ne coûte rien, c'est-à-dire le cas courant, et ne demande que s'il y a quelque chose à perdre : des sessions vivantes à interrompre (le dialogue dit combien) ou un daemon injoignable. Un « Later » reste définitif pour cette version : rien ne redémarre dans le dos de personne. La détection ne dépend plus d'un retour par le tray ou le Dock : un `stat` toutes les 30 s la couvre même fenêtre ouverte, par poll et non par `fs.watch`, qui ne voit souvent rien quand un paquet remplace un binaire ou tout un répertoire.
108 lines
5.0 KiB
TypeScript
108 lines
5.0 KiB
TypeScript
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;
|
||
}
|
||
});
|
||
});
|