Some checks failed
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / No em/en dashes (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled
Deploy site (production) / build-and-deploy (push) Successful in 20s
Remplace les 547 tirets cadratins (U+2014) et demi-cadratins (U+2013) des fichiers versionnés par la ponctuation contextuelle adaptée (point médian, deux-points, virgule, parenthèses ; tiret simple pour les plages), sur 122 fichiers (appli, vitrine, doc, tests, workflows, scripts). Ajoute le job CI « lint-dashes » (git grep -P) qui échoue si un tiret cadratin/demi-cadratin réapparaît, hors logo binaire et captures brutes du terminal (fidélité des fixtures de détection de dialogue).
195 lines
7.4 KiB
TypeScript
195 lines
7.4 KiB
TypeScript
import { afterEach, describe, expect, it } from 'vitest';
|
|
import { homedir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import {
|
|
detectPlatform,
|
|
parseInstallArgs,
|
|
buildServiceArgs,
|
|
resolveBin,
|
|
resolveScriptPath,
|
|
renderSystemdUnit,
|
|
renderLaunchAgentPlist,
|
|
xmlEscape,
|
|
systemdUnitPath,
|
|
launchAgentPlistPath,
|
|
} from '../src/cli/install.js';
|
|
|
|
describe('cli install · detectPlatform', () => {
|
|
it('accepte linux et darwin', () => {
|
|
expect(detectPlatform('linux')).toBe('linux');
|
|
expect(detectPlatform('darwin')).toBe('darwin');
|
|
});
|
|
|
|
it('rejette les autres plateformes avec un message clair', () => {
|
|
expect(() => detectPlatform('win32')).toThrow(/Linux \(systemd\) and macOS \(launchd\)/);
|
|
expect(() => detectPlatform('freebsd')).toThrow(/freebsd/);
|
|
});
|
|
});
|
|
|
|
describe('cli install · buildServiceArgs', () => {
|
|
it('aucun flag → aucun argument (le service garde les défauts loopback)', () => {
|
|
expect(buildServiceArgs(parseInstallArgs([]))).toEqual([]);
|
|
});
|
|
|
|
it('propage --port et --allow-origin (répétable) dans un ordre stable', () => {
|
|
const flags = parseInstallArgs(['--port', '8080', '--allow-origin', 'a', '--allow-origin', 'b']);
|
|
expect(buildServiceArgs(flags)).toEqual(['--port', '8080', '--allow-origin', 'a', '--allow-origin', 'b']);
|
|
});
|
|
|
|
it("n'injecte JAMAIS --i-know-this-exposes-a-terminal (modèle de sécurité)", () => {
|
|
const flags = parseInstallArgs(['--bind', '0.0.0.0', '--port', '7317']);
|
|
expect(buildServiceArgs(flags)).not.toContain('--i-know-this-exposes-a-terminal');
|
|
});
|
|
|
|
it("ne propage pas les flags propres à l'install (bin-path, label, dry-run, no-enable)", () => {
|
|
const flags = parseInstallArgs(['--bin-path', '/usr/local/bin/arboretum', '--label', 'x', '--dry-run', '--no-enable']);
|
|
expect(buildServiceArgs(flags)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('cli install · resolveBin', () => {
|
|
it('par défaut : node (process.execPath) + dist/index.js', () => {
|
|
const { exec, args } = resolveBin({});
|
|
expect(exec).toBe(process.execPath);
|
|
expect(args).toHaveLength(1);
|
|
expect(args[0]).toBe(resolveScriptPath());
|
|
expect(args[0]).toMatch(/index\.(js|ts)$/);
|
|
});
|
|
|
|
it('--bin-path force le wrapper, sans argument de script', () => {
|
|
expect(resolveBin({ binPath: '/usr/local/bin/arboretum' })).toEqual({
|
|
exec: '/usr/local/bin/arboretum',
|
|
args: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('cli install · renderSystemdUnit', () => {
|
|
it('contient le ExecStart calculé et les directives clés', () => {
|
|
const unit = renderSystemdUnit({ exec: '/usr/bin/node', scriptArgs: ['/opt/arboretum/index.js', '--port', '7317'] });
|
|
expect(unit).toContain('ExecStart=/usr/bin/node /opt/arboretum/index.js --port 7317');
|
|
expect(unit).toContain('Restart=on-failure');
|
|
expect(unit).toContain('KillSignal=SIGTERM');
|
|
expect(unit).toContain('TimeoutStopSec=10');
|
|
expect(unit).toContain('WantedBy=default.target');
|
|
});
|
|
|
|
it('quote les tokens contenant un espace', () => {
|
|
const unit = renderSystemdUnit({ exec: '/path with space/node', scriptArgs: ['/s.js'] });
|
|
expect(unit).toContain('ExecStart="/path with space/node" /s.js');
|
|
});
|
|
|
|
it('fige le PATH d\'installation quand fourni (le service systemd a un PATH minimal)', () => {
|
|
const unit = renderSystemdUnit({
|
|
exec: '/usr/bin/node',
|
|
scriptArgs: ['/s.js'],
|
|
pathEnv: '/home/me/.local/bin:/usr/bin',
|
|
});
|
|
// Doubles quotes systemd : un chemin du PATH peut contenir un espace.
|
|
expect(unit).toContain('Environment="PATH=/home/me/.local/bin:/usr/bin"');
|
|
});
|
|
|
|
it("n'émet aucune ligne PATH sans pathEnv (rétrocompat)", () => {
|
|
const unit = renderSystemdUnit({ exec: '/usr/bin/node', scriptArgs: ['/s.js'] });
|
|
expect(unit).not.toContain('PATH=');
|
|
});
|
|
|
|
it('snapshot du unit pour un jeu de flags fixe', () => {
|
|
const unit = renderSystemdUnit({
|
|
exec: '/usr/bin/node',
|
|
scriptArgs: ['/opt/arboretum/index.js', '--port', '7317', '--allow-origin', 'https://m.ts.net'],
|
|
});
|
|
expect(unit).toMatchInlineSnapshot(`
|
|
"[Unit]
|
|
Description=Arboretum · git worktree & Claude Code dashboard
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
ExecStart=/usr/bin/node /opt/arboretum/index.js --port 7317 --allow-origin https://m.ts.net
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
KillSignal=SIGTERM
|
|
TimeoutStopSec=10
|
|
Environment=NODE_ENV=production
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
"
|
|
`);
|
|
});
|
|
});
|
|
|
|
describe('cli install · renderLaunchAgentPlist', () => {
|
|
const base = {
|
|
label: 'fr.lidge.arboretum',
|
|
programArguments: ['/usr/bin/node', '/opt/index.js', '--port', '7317'],
|
|
stdoutPath: '/Users/me/Library/Logs/arboretum/out.log',
|
|
stderrPath: '/Users/me/Library/Logs/arboretum/err.log',
|
|
};
|
|
|
|
it('contient le label, les ProgramArguments ordonnés et les clés launchd', () => {
|
|
const plist = renderLaunchAgentPlist(base);
|
|
expect(plist).toContain('<string>fr.lidge.arboretum</string>');
|
|
const idxNode = plist.indexOf('<string>/usr/bin/node</string>');
|
|
const idxScript = plist.indexOf('<string>/opt/index.js</string>');
|
|
expect(idxNode).toBeGreaterThan(0);
|
|
expect(idxScript).toBeGreaterThan(idxNode);
|
|
expect(plist).toContain('<key>RunAtLoad</key>');
|
|
expect(plist).toContain('<key>KeepAlive</key>');
|
|
expect(plist).toContain('<key>StandardOutPath</key>');
|
|
});
|
|
|
|
it('échappe les caractères XML dans les arguments (URL avec &)', () => {
|
|
const plist = renderLaunchAgentPlist({
|
|
...base,
|
|
programArguments: ['/usr/bin/node', '/opt/index.js', '--allow-origin', 'https://a?b&c=d'],
|
|
});
|
|
expect(plist).toContain('https://a?b&c=d');
|
|
expect(plist).not.toContain('b&c=d');
|
|
});
|
|
|
|
it('ajoute la clé PATH dans EnvironmentVariables quand fournie (launchd a un PATH minimal)', () => {
|
|
const plist = renderLaunchAgentPlist({ ...base, pathEnv: '/Users/me/.local/bin:/usr/bin' });
|
|
expect(plist).toContain('<key>PATH</key>');
|
|
expect(plist).toContain('<string>/Users/me/.local/bin:/usr/bin</string>');
|
|
// Reste dans le dict EnvironmentVariables, juste après NODE_ENV.
|
|
expect(plist.indexOf('<key>PATH</key>')).toBeGreaterThan(plist.indexOf('<key>NODE_ENV</key>'));
|
|
});
|
|
|
|
it("n'ajoute aucune clé PATH sans pathEnv (rétrocompat)", () => {
|
|
const plist = renderLaunchAgentPlist(base);
|
|
expect(plist).not.toContain('<key>PATH</key>');
|
|
});
|
|
});
|
|
|
|
describe('cli install · xmlEscape', () => {
|
|
it('échappe &, < et >', () => {
|
|
expect(xmlEscape('a & b < c > d')).toBe('a & b < c > d');
|
|
});
|
|
});
|
|
|
|
describe('cli install · chemins', () => {
|
|
const savedXdg = process.env.XDG_CONFIG_HOME;
|
|
afterEach(() => {
|
|
if (savedXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
else process.env.XDG_CONFIG_HOME = savedXdg;
|
|
});
|
|
|
|
it('systemdUnitPath respecte XDG_CONFIG_HOME', () => {
|
|
process.env.XDG_CONFIG_HOME = '/tmp/xdg';
|
|
expect(systemdUnitPath()).toBe('/tmp/xdg/systemd/user/arboretum.service');
|
|
});
|
|
|
|
it('systemdUnitPath retombe sur ~/.config sans XDG_CONFIG_HOME', () => {
|
|
delete process.env.XDG_CONFIG_HOME;
|
|
expect(systemdUnitPath()).toBe(join(homedir(), '.config', 'systemd', 'user', 'arboretum.service'));
|
|
});
|
|
|
|
it('launchAgentPlistPath place le plist dans ~/Library/LaunchAgents', () => {
|
|
expect(launchAgentPlistPath('fr.lidge.arboretum')).toBe(
|
|
join(homedir(), 'Library', 'LaunchAgents', 'fr.lidge.arboretum.plist'),
|
|
);
|
|
});
|
|
});
|