Files
arboretum/packages/server/test/claude-launcher.test.ts
T
johanleroy 17e95754b1 fix(server, desktop): plus de transcript perdu, et la mise à jour s'applique toute seule
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.
2026-08-05 11:35:51 +02:00

108 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
});
});