167 lines
4.7 KiB
TypeScript
167 lines
4.7 KiB
TypeScript
import fs from 'fs';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
STACK_RESTART_UNIT_NAME,
|
|
restartStackServices,
|
|
} from './restart-stack.js';
|
|
|
|
describe('restartStackServices', () => {
|
|
const tempRoots: string[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const root of tempRoots.splice(0)) {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('dispatches stack restart through the oneshot unit by default', () => {
|
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-restart-'));
|
|
tempRoots.push(tempRoot);
|
|
|
|
const execFileSyncImpl = vi.fn();
|
|
|
|
const services = restartStackServices(tempRoot, {
|
|
execFileSyncImpl,
|
|
runningAsRoot: false,
|
|
serviceManager: 'systemd',
|
|
serviceId: null,
|
|
});
|
|
|
|
expect(services).toEqual(['ejclaw']);
|
|
expect(execFileSyncImpl).toHaveBeenNthCalledWith(
|
|
1,
|
|
'systemctl',
|
|
['--user', 'start', '--wait', STACK_RESTART_UNIT_NAME],
|
|
{ stdio: 'ignore' },
|
|
);
|
|
expect(execFileSyncImpl).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('restarts and verifies the unified service in direct mode', () => {
|
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-restart-'));
|
|
tempRoots.push(tempRoot);
|
|
|
|
const execFileSyncImpl = vi.fn();
|
|
|
|
const services = restartStackServices(tempRoot, {
|
|
direct: true,
|
|
execFileSyncImpl,
|
|
runningAsRoot: false,
|
|
serviceManager: 'systemd',
|
|
serviceId: null,
|
|
});
|
|
|
|
expect(services).toEqual(['ejclaw']);
|
|
expect(execFileSyncImpl).toHaveBeenNthCalledWith(
|
|
1,
|
|
'systemctl',
|
|
['--user', 'restart', 'ejclaw'],
|
|
{ stdio: 'ignore' },
|
|
);
|
|
expect(execFileSyncImpl).toHaveBeenNthCalledWith(
|
|
2,
|
|
'systemctl',
|
|
['--user', 'is-active', '--quiet', 'ejclaw'],
|
|
{ stdio: 'ignore' },
|
|
);
|
|
expect(execFileSyncImpl).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('rejects non-systemd environments', () => {
|
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-restart-'));
|
|
tempRoots.push(tempRoot);
|
|
|
|
expect(() =>
|
|
restartStackServices(tempRoot, {
|
|
serviceManager: 'none',
|
|
}),
|
|
).toThrow(
|
|
'restart:stack only supports Linux systemd services in this repo',
|
|
);
|
|
});
|
|
|
|
it('falls back to direct restart when the oneshot unit is not installed for an external caller', () => {
|
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-restart-'));
|
|
tempRoots.push(tempRoot);
|
|
|
|
const execFileSyncImpl = vi.fn().mockImplementationOnce(() => {
|
|
const error = new Error('unit missing');
|
|
Object.assign(error, {
|
|
stderr: 'Unit ejclaw-stack-restart.service not found.',
|
|
});
|
|
throw error;
|
|
});
|
|
|
|
const services = restartStackServices(tempRoot, {
|
|
execFileSyncImpl,
|
|
runningAsRoot: false,
|
|
serviceManager: 'systemd',
|
|
serviceId: null,
|
|
});
|
|
|
|
expect(services).toEqual(['ejclaw']);
|
|
expect(execFileSyncImpl).toHaveBeenNthCalledWith(
|
|
1,
|
|
'systemctl',
|
|
['--user', 'start', '--wait', STACK_RESTART_UNIT_NAME],
|
|
{ stdio: 'ignore' },
|
|
);
|
|
expect(execFileSyncImpl).toHaveBeenNthCalledWith(
|
|
2,
|
|
'systemctl',
|
|
['--user', 'restart', 'ejclaw'],
|
|
{ stdio: 'ignore' },
|
|
);
|
|
});
|
|
|
|
it('rejects direct fallback when the unit is missing for a managed service caller', () => {
|
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-restart-'));
|
|
tempRoots.push(tempRoot);
|
|
|
|
const execFileSyncImpl = vi.fn().mockImplementation(() => {
|
|
const error = new Error('unit missing');
|
|
Object.assign(error, {
|
|
stderr: 'Unit ejclaw-stack-restart.service not found.',
|
|
});
|
|
throw error;
|
|
});
|
|
|
|
expect(() =>
|
|
restartStackServices(tempRoot, {
|
|
execFileSyncImpl,
|
|
runningAsRoot: false,
|
|
serviceManager: 'systemd',
|
|
serviceId: 'codex-main',
|
|
}),
|
|
).toThrow('Run `bun run setup -- --step service`');
|
|
});
|
|
|
|
it('does not hide a general start failure behind direct fallback', () => {
|
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-restart-'));
|
|
tempRoots.push(tempRoot);
|
|
|
|
const execFileSyncImpl = vi.fn().mockImplementation(() => {
|
|
const error = new Error('job failed');
|
|
Object.assign(error, {
|
|
stderr:
|
|
'Job for ejclaw-stack-restart.service failed because the control process exited with error code.',
|
|
});
|
|
throw error;
|
|
});
|
|
|
|
expect(() =>
|
|
restartStackServices(tempRoot, {
|
|
execFileSyncImpl,
|
|
runningAsRoot: false,
|
|
serviceManager: 'systemd',
|
|
serviceId: null,
|
|
}),
|
|
).toThrow('job failed');
|
|
expect(execFileSyncImpl).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|