diff --git a/setup/service.test.ts b/setup/service.test.ts index 8aa69f7..8827c2f 100644 --- a/setup/service.test.ts +++ b/setup/service.test.ts @@ -3,8 +3,13 @@ import os from 'os'; import path from 'path'; import { afterEach, describe, expect, it } from 'vitest'; -import { buildRuntimePathEnv, buildStackRestartSystemdUnit } from './service.js'; -import { getServiceDefs } from './service-defs.js'; +import { + buildLaunchdPlist, + buildRuntimePathEnv, + buildStackRestartSystemdUnit, + buildSystemdUnit, +} from './service.js'; +import { getServiceDefs, type ServiceDef } from './service-defs.js'; /** * Tests for service configuration generation. @@ -13,105 +18,49 @@ import { getServiceDefs } from './service-defs.js'; * without actually loading services. */ -// Helper: generate a plist string the same way service.ts does -function generatePlist( - nodePath: string, - projectRoot: string, - homeDir: string, -): string { - const runtimePath = buildRuntimePathEnv(nodePath, homeDir); - - return ` - - - - Label - com.ejclaw - ProgramArguments - - ${nodePath} - ${projectRoot}/dist/index.js - - WorkingDirectory - ${projectRoot} - RunAtLoad - - KeepAlive - - EnvironmentVariables - - PATH - ${runtimePath} - HOME - ${homeDir} - - StandardOutPath - ${projectRoot}/logs/ejclaw.log - StandardErrorPath - ${projectRoot}/logs/ejclaw.error.log - -`; -} - -function generateSystemdUnit( - nodePath: string, - projectRoot: string, - homeDir: string, - isSystem: boolean, -): string { - const runtimePath = buildRuntimePathEnv(nodePath, homeDir); - - return `[Unit] -Description=EJClaw Personal Assistant -After=network.target - -[Service] -Type=simple -ExecStart=${nodePath} ${projectRoot}/dist/index.js -WorkingDirectory=${projectRoot} -Restart=always -RestartSec=5 -Environment=HOME=${homeDir} -Environment=PATH=${runtimePath} -StandardOutput=append:${projectRoot}/logs/ejclaw.log -StandardError=append:${projectRoot}/logs/ejclaw.error.log - -[Install] -WantedBy=${isSystem ? 'multi-user.target' : 'default.target'}`; -} +const baseServiceDef: ServiceDef = { + description: 'EJClaw Personal Assistant', + launchdLabel: 'com.ejclaw', + logName: 'ejclaw', + name: 'ejclaw', +}; describe('plist generation', () => { it('contains the correct label', () => { - const plist = generatePlist( - '/usr/local/bin/node', + const plist = buildLaunchdPlist( + baseServiceDef, '/home/user/ejclaw', + '/usr/local/bin/node', '/home/user', ); expect(plist).toContain('com.ejclaw'); }); it('uses the correct node path', () => { - const plist = generatePlist( - '/opt/node/bin/node', + const plist = buildLaunchdPlist( + baseServiceDef, '/home/user/ejclaw', + '/opt/node/bin/node', '/home/user', ); expect(plist).toContain('/opt/node/bin/node'); }); it('points to dist/index.js', () => { - const plist = generatePlist( - '/usr/local/bin/node', + const plist = buildLaunchdPlist( + baseServiceDef, '/home/user/ejclaw', + '/usr/local/bin/node', '/home/user', ); expect(plist).toContain('/home/user/ejclaw/dist/index.js'); }); it('sets log paths', () => { - const plist = generatePlist( - '/usr/local/bin/node', + const plist = buildLaunchdPlist( + baseServiceDef, '/home/user/ejclaw', + '/usr/local/bin/node', '/home/user', ); expect(plist).toContain('ejclaw.log'); @@ -127,9 +76,10 @@ describe('systemd unit generation', () => { }); it('user unit uses default.target', () => { - const unit = generateSystemdUnit( - '/usr/bin/node', + const unit = buildSystemdUnit( + baseServiceDef, '/home/user/ejclaw', + '/usr/bin/node', '/home/user', false, ); @@ -137,9 +87,10 @@ describe('systemd unit generation', () => { }); it('system unit uses multi-user.target', () => { - const unit = generateSystemdUnit( - '/usr/bin/node', + const unit = buildSystemdUnit( + baseServiceDef, '/home/user/ejclaw', + '/usr/bin/node', '/home/user', true, ); @@ -147,9 +98,10 @@ describe('systemd unit generation', () => { }); it('contains restart policy', () => { - const unit = generateSystemdUnit( - '/usr/bin/node', + const unit = buildSystemdUnit( + baseServiceDef, '/home/user/ejclaw', + '/usr/bin/node', '/home/user', false, ); @@ -158,9 +110,10 @@ describe('systemd unit generation', () => { }); it('sets correct ExecStart', () => { - const unit = generateSystemdUnit( - '/usr/bin/bun', + const unit = buildSystemdUnit( + baseServiceDef, '/srv/ejclaw', + '/usr/bin/bun', '/home/user', false, ); @@ -168,6 +121,25 @@ describe('systemd unit generation', () => { 'ExecStart=/usr/bin/bun /srv/ejclaw/dist/index.js', ); }); + + it('preserves EnvironmentFile and extraEnv in the actual builder', () => { + const unit = buildSystemdUnit( + { + ...baseServiceDef, + environmentFile: '/srv/ejclaw/.env.codex', + extraEnv: { ASSISTANT_NAME: 'codex' }, + logName: 'ejclaw-codex', + name: 'ejclaw-codex', + }, + '/srv/ejclaw', + '/usr/bin/bun', + '/home/user', + false, + ); + + expect(unit).toContain('EnvironmentFile=/srv/ejclaw/.env.codex'); + expect(unit).toContain('Environment=ASSISTANT_NAME=codex'); + }); }); describe('WSL nohup fallback', () => { diff --git a/setup/service.ts b/setup/service.ts index 4ae9b0b..e3a3588 100644 --- a/setup/service.ts +++ b/setup/service.ts @@ -148,39 +148,7 @@ function setupLaunchd( ); fs.mkdirSync(path.dirname(plistPath), { recursive: true }); - const envEntries = buildLaunchdEnvironmentEntries( - nodePath, - homeDir, - def.extraEnv, - ); - - const plist = ` - - - - Label - ${def.launchdLabel} - ProgramArguments - - ${nodePath} - ${projectRoot}/dist/index.js - - WorkingDirectory - ${projectRoot} - RunAtLoad - - KeepAlive - - EnvironmentVariables - -${envEntries.join('\n')} - - StandardOutPath - ${projectRoot}/logs/${def.logName}.log - StandardErrorPath - ${projectRoot}/logs/${def.logName}.error.log - -`; + const plist = buildLaunchdPlist(def, projectRoot, nodePath, homeDir); fs.writeFileSync(plistPath, plist); logger.info({ plistPath, service: def.name }, 'Wrote launchd plist'); @@ -218,6 +186,47 @@ ${envEntries.join('\n')} }); } +export function buildLaunchdPlist( + def: ServiceDef, + projectRoot: string, + nodePath: string, + homeDir: string, +): string { + const envEntries = buildLaunchdEnvironmentEntries( + nodePath, + homeDir, + def.extraEnv, + ); + + return ` + + + + Label + ${def.launchdLabel} + ProgramArguments + + ${nodePath} + ${projectRoot}/dist/index.js + + WorkingDirectory + ${projectRoot} + RunAtLoad + + KeepAlive + + EnvironmentVariables + +${envEntries.join('\n')} + + StandardOutPath + ${projectRoot}/logs/${def.logName}.log + StandardErrorPath + ${projectRoot}/logs/${def.logName}.error.log + +`; +} + /* ------------------------------------------------------------------ */ /* Linux */ /* ------------------------------------------------------------------ */ @@ -364,6 +373,25 @@ function setupSystemdUnit( const unitPath = getUnitPath(def.name, homeDir, runningAsRoot); fs.mkdirSync(path.dirname(unitPath), { recursive: true }); + const unit = buildSystemdUnit( + def, + projectRoot, + nodePath, + homeDir, + runningAsRoot, + ); + + fs.writeFileSync(unitPath, unit); + logger.info({ unitPath, service: def.name }, 'Wrote systemd unit'); +} + +export function buildSystemdUnit( + def: ServiceDef, + projectRoot: string, + nodePath: string, + homeDir: string, + runningAsRoot: boolean, +): string { const envLines = buildSystemdEnvironmentLines( nodePath, homeDir, @@ -375,7 +403,7 @@ function setupSystemdUnit( ? `EnvironmentFile=${def.environmentFile}\n` : ''; - const unit = `[Unit] + return `[Unit] Description=${def.description} After=network.target @@ -391,9 +419,6 @@ StandardError=append:${projectRoot}/logs/${def.logName}.error.log [Install] WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`; - - fs.writeFileSync(unitPath, unit); - logger.info({ unitPath, service: def.name }, 'Wrote systemd unit'); } export function buildStackRestartSystemdUnit(