import { describe, it, expect } from 'vitest'; import path from 'path'; /** * Tests for service configuration generation. * * These tests verify the generated content of plist/systemd/nohup configs * without actually loading services. */ // Helper: generate a plist string the same way service.ts does function generatePlist( nodePath: string, projectRoot: string, homeDir: string, ): string { return ` Label com.nanoclaw ProgramArguments ${nodePath} ${projectRoot}/dist/index.js WorkingDirectory ${projectRoot} RunAtLoad KeepAlive EnvironmentVariables PATH /usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin HOME ${homeDir} StandardOutPath ${projectRoot}/logs/nanoclaw.log StandardErrorPath ${projectRoot}/logs/nanoclaw.error.log `; } function generateSystemdUnit( nodePath: string, projectRoot: string, homeDir: string, isSystem: boolean, ): string { return `[Unit] Description=NanoClaw Personal Assistant After=network.target [Service] Type=simple ExecStart=${nodePath} ${projectRoot}/dist/index.js WorkingDirectory=${projectRoot} Restart=always RestartSec=5 KillMode=process Environment=HOME=${homeDir} Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin StandardOutput=append:${projectRoot}/logs/nanoclaw.log StandardError=append:${projectRoot}/logs/nanoclaw.error.log [Install] WantedBy=${isSystem ? 'multi-user.target' : 'default.target'}`; } describe('plist generation', () => { it('contains the correct label', () => { const plist = generatePlist( '/usr/local/bin/node', '/home/user/nanoclaw', '/home/user', ); expect(plist).toContain('com.nanoclaw'); }); it('uses the correct node path', () => { const plist = generatePlist( '/opt/node/bin/node', '/home/user/nanoclaw', '/home/user', ); expect(plist).toContain('/opt/node/bin/node'); }); it('points to dist/index.js', () => { const plist = generatePlist( '/usr/local/bin/node', '/home/user/nanoclaw', '/home/user', ); expect(plist).toContain('/home/user/nanoclaw/dist/index.js'); }); it('sets log paths', () => { const plist = generatePlist( '/usr/local/bin/node', '/home/user/nanoclaw', '/home/user', ); expect(plist).toContain('nanoclaw.log'); expect(plist).toContain('nanoclaw.error.log'); }); }); describe('systemd unit generation', () => { it('user unit uses default.target', () => { const unit = generateSystemdUnit( '/usr/bin/node', '/home/user/nanoclaw', '/home/user', false, ); expect(unit).toContain('WantedBy=default.target'); }); it('system unit uses multi-user.target', () => { const unit = generateSystemdUnit( '/usr/bin/node', '/home/user/nanoclaw', '/home/user', true, ); expect(unit).toContain('WantedBy=multi-user.target'); }); it('contains restart policy', () => { const unit = generateSystemdUnit( '/usr/bin/node', '/home/user/nanoclaw', '/home/user', false, ); expect(unit).toContain('Restart=always'); expect(unit).toContain('RestartSec=5'); }); it('uses KillMode=process to preserve detached children', () => { const unit = generateSystemdUnit( '/usr/bin/node', '/home/user/nanoclaw', '/home/user', false, ); expect(unit).toContain('KillMode=process'); }); it('sets correct ExecStart', () => { const unit = generateSystemdUnit( '/usr/bin/node', '/srv/nanoclaw', '/home/user', false, ); expect(unit).toContain( 'ExecStart=/usr/bin/node /srv/nanoclaw/dist/index.js', ); }); }); describe('WSL nohup fallback', () => { it('generates a valid wrapper script', () => { const projectRoot = '/home/user/nanoclaw'; const nodePath = '/usr/bin/node'; const pidFile = path.join(projectRoot, 'nanoclaw.pid'); // Simulate what service.ts generates const wrapper = `#!/bin/bash set -euo pipefail cd ${JSON.stringify(projectRoot)} nohup ${JSON.stringify(nodePath)} ${JSON.stringify(projectRoot)}/dist/index.js >> ${JSON.stringify(projectRoot)}/logs/nanoclaw.log 2>> ${JSON.stringify(projectRoot)}/logs/nanoclaw.error.log & echo $! > ${JSON.stringify(pidFile)}`; expect(wrapper).toContain('#!/bin/bash'); expect(wrapper).toContain('nohup'); expect(wrapper).toContain(nodePath); expect(wrapper).toContain('nanoclaw.pid'); }); });