Files
EJClaw/setup/service.test.ts
Eyejoker 35ba7cb5ba chore: replace all node/npm/tsx references with bun across codebase
- Source: setup/service.ts, runners.ts, agent-runner.ts error messages
- Scripts: restart-stack.sh, run-migrations.ts → bun
- Bootstrap: setup.sh check_node→check_bun, npm install→bun install
- Docs: CLAUDE.md, README.md, all SKILL.md files
- Tests: service.test.ts, restart-stack.test.ts expectations updated
2026-03-31 00:10:23 +09:00

221 lines
5.9 KiB
TypeScript

import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
import { buildStackRestartSystemdUnit } from './service.js';
import { getServiceDefs } from './service-defs.js';
/**
* 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 `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.ejclaw</string>
<key>ProgramArguments</key>
<array>
<string>${nodePath}</string>
<string>${projectRoot}/dist/index.js</string>
</array>
<key>WorkingDirectory</key>
<string>${projectRoot}</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin</string>
<key>HOME</key>
<string>${homeDir}</string>
</dict>
<key>StandardOutPath</key>
<string>${projectRoot}/logs/ejclaw.log</string>
<key>StandardErrorPath</key>
<string>${projectRoot}/logs/ejclaw.error.log</string>
</dict>
</plist>`;
}
function generateSystemdUnit(
nodePath: string,
projectRoot: string,
homeDir: string,
isSystem: boolean,
): string {
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=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin
StandardOutput=append:${projectRoot}/logs/ejclaw.log
StandardError=append:${projectRoot}/logs/ejclaw.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/ejclaw',
'/home/user',
);
expect(plist).toContain('<string>com.ejclaw</string>');
});
it('uses the correct node path', () => {
const plist = generatePlist(
'/opt/node/bin/node',
'/home/user/ejclaw',
'/home/user',
);
expect(plist).toContain('<string>/opt/node/bin/node</string>');
});
it('points to dist/index.js', () => {
const plist = generatePlist(
'/usr/local/bin/node',
'/home/user/ejclaw',
'/home/user',
);
expect(plist).toContain('/home/user/ejclaw/dist/index.js');
});
it('sets log paths', () => {
const plist = generatePlist(
'/usr/local/bin/node',
'/home/user/ejclaw',
'/home/user',
);
expect(plist).toContain('ejclaw.log');
expect(plist).toContain('ejclaw.error.log');
});
});
describe('systemd unit generation', () => {
it('user unit uses default.target', () => {
const unit = generateSystemdUnit(
'/usr/bin/node',
'/home/user/ejclaw',
'/home/user',
false,
);
expect(unit).toContain('WantedBy=default.target');
});
it('system unit uses multi-user.target', () => {
const unit = generateSystemdUnit(
'/usr/bin/node',
'/home/user/ejclaw',
'/home/user',
true,
);
expect(unit).toContain('WantedBy=multi-user.target');
});
it('contains restart policy', () => {
const unit = generateSystemdUnit(
'/usr/bin/node',
'/home/user/ejclaw',
'/home/user',
false,
);
expect(unit).toContain('Restart=always');
expect(unit).toContain('RestartSec=5');
});
it('sets correct ExecStart', () => {
const unit = generateSystemdUnit(
'/usr/bin/bun',
'/srv/ejclaw',
'/home/user',
false,
);
expect(unit).toContain(
'ExecStart=/usr/bin/bun /srv/ejclaw/dist/index.js',
);
});
});
describe('WSL nohup fallback', () => {
it('generates a valid wrapper script', () => {
const projectRoot = '/home/user/ejclaw';
const nodePath = '/usr/bin/node';
const pidFile = path.join(projectRoot, 'ejclaw.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/ejclaw.log 2>> ${JSON.stringify(projectRoot)}/logs/ejclaw.error.log &
echo $! > ${JSON.stringify(pidFile)}`;
expect(wrapper).toContain('#!/bin/bash');
expect(wrapper).toContain('nohup');
expect(wrapper).toContain(nodePath);
expect(wrapper).toContain('ejclaw.pid');
});
});
describe('service definitions', () => {
const tempRoots: string[] = [];
afterEach(() => {
for (const root of tempRoots.splice(0)) {
fs.rmSync(root, { recursive: true, force: true });
}
});
it('includes the review service when .env.codex-review exists', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-stack-'));
tempRoots.push(tempRoot);
fs.writeFileSync(path.join(tempRoot, '.env.codex'), 'A=1\n');
fs.writeFileSync(path.join(tempRoot, '.env.codex-review'), 'B=1\n');
const defs = getServiceDefs(tempRoot);
expect(defs.map((def) => def.name)).toEqual([
'ejclaw',
'ejclaw-codex',
'ejclaw-review',
]);
});
it('generates a oneshot stack restart unit', () => {
const unit = buildStackRestartSystemdUnit(
'/srv/ejclaw',
'/usr/bin/bun',
'/home/user',
);
expect(unit).toContain('Description=EJClaw Stack Restart Orchestrator');
expect(unit).toContain('Type=oneshot');
expect(unit).toContain(
'ExecStart=/bin/bash /srv/ejclaw/scripts/restart-ejclaw-stack.sh --direct',
);
});
});