feat(settings): réglages Claude CLI (binaire + ~/.claude) & notif « disponible »

- Réglages → Claude CLI : override du chemin du binaire `claude` (effet à la
  prochaine session, fallback `which claude`), diagnostic de détection, et
  override de la racine ~/.claude (effet au redémarrage). Validateurs stricts.
- Push : notifie aussi sur le front busy→idle (session redevenue disponible).
- Renomme l'état affiché idle → « disponible » / « available » (web EN/FR + VS Code) ;
  l'enum SessionActivity du protocole reste inchangé.
This commit is contained in:
2026-06-24 10:15:29 +02:00
parent 2506dfb1f3
commit b5236b41c8
16 changed files with 519 additions and 27 deletions

View File

@@ -0,0 +1,89 @@
// Réglages CLI Claude (chemin du binaire + override ~/.claude) : validateurs + lecture/écriture DB.
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { openDb, setSetting, type Db } from '../src/db/index.js';
import {
CLAUDE_BIN_PATH_KEY,
CLAUDE_HOME_KEY,
normalizeClaudeBinPath,
normalizeClaudeHome,
readClaudeBinPath,
readClaudeHome,
} from '../src/core/claude-settings.js';
let dir: string;
let execFile: string;
let plainFile: string;
let subDir: string;
beforeAll(() => {
dir = mkdtempSync(join(tmpdir(), 'arboretum-claude-settings-'));
execFile = join(dir, 'claude');
writeFileSync(execFile, '#!/bin/sh\n');
chmodSync(execFile, 0o755); // exécutable
plainFile = join(dir, 'notexec');
writeFileSync(plainFile, 'x');
chmodSync(plainFile, 0o644); // non exécutable
subDir = join(dir, 'home');
mkdirSync(subDir);
});
afterAll(() => rmSync(dir, { recursive: true, force: true }));
describe('normalizeClaudeBinPath', () => {
it('accepte un chemin absolu vers un fichier exécutable', () => {
expect(normalizeClaudeBinPath(execFile)).toBe(execFile);
});
it('"" = réinitialisation (auto-détection)', () => {
expect(normalizeClaudeBinPath('')).toBe('');
expect(normalizeClaudeBinPath(' ')).toBe('');
});
it('rejette un fichier non exécutable, un répertoire, un chemin relatif ou avec ".." (null)', () => {
expect(normalizeClaudeBinPath(plainFile)).toBeNull();
expect(normalizeClaudeBinPath(subDir)).toBeNull(); // répertoire, pas un fichier
expect(normalizeClaudeBinPath('relative/claude')).toBeNull();
expect(normalizeClaudeBinPath(join(dir, '..', 'x'))).toBeNull();
expect(normalizeClaudeBinPath(join(dir, 'missing'))).toBeNull();
expect(normalizeClaudeBinPath(42)).toBeNull();
});
});
describe('normalizeClaudeHome', () => {
it('accepte un chemin absolu vers un répertoire existant', () => {
expect(normalizeClaudeHome(subDir)).toBe(subDir);
});
it('"" = réinitialisation (défaut)', () => {
expect(normalizeClaudeHome('')).toBe('');
});
it('rejette un fichier, un chemin relatif/inexistant (null)', () => {
expect(normalizeClaudeHome(execFile)).toBeNull(); // fichier, pas un répertoire
expect(normalizeClaudeHome('relative/.claude')).toBeNull();
expect(normalizeClaudeHome(join(dir, 'missing'))).toBeNull();
expect(normalizeClaudeHome(null)).toBeNull();
});
});
describe('readClaudeBinPath / readClaudeHome', () => {
let db: Db;
beforeAll(() => {
db = openDb(join(dir, 'settings.db'));
});
afterAll(() => db.close());
it('null quand non défini', () => {
expect(readClaudeBinPath(db)).toBeNull();
expect(readClaudeHome(db)).toBeNull();
});
it('lit la valeur stockée et traite "" stockée comme null', () => {
setSetting(db, CLAUDE_BIN_PATH_KEY, execFile);
setSetting(db, CLAUDE_HOME_KEY, subDir);
expect(readClaudeBinPath(db)).toBe(execFile);
expect(readClaudeHome(db)).toBe(subDir);
setSetting(db, CLAUDE_BIN_PATH_KEY, ''); // réinitialisé
setSetting(db, CLAUDE_HOME_KEY, '');
expect(readClaudeBinPath(db)).toBeNull();
expect(readClaudeHome(db)).toBeNull();
});
});