fix(cli): warn when systemd user lingering is disabled (#1855)
This commit is contained in:
@@ -11,6 +11,7 @@ import { isModuleCliExecution, normalizeCliEntryPath } from './cli-entry.js';
|
||||
import { requestJson } from './lib/cli-http.js';
|
||||
import { requestControlAction } from './lib/cli-control.js';
|
||||
import { inspectTunnelAttachability } from './lib/cli-lifecycle.js';
|
||||
import { startupCommand } from './lib/commands-startup.js';
|
||||
import { formatGoal } from './lib/commands-schedule.js';
|
||||
import {
|
||||
buildSessionCreatePayload,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
discoverRunningInstances,
|
||||
discoverUnconfirmedRegistryInstanceOnPort,
|
||||
ensureTunnelProfilesMigrated,
|
||||
EXIT_CODE,
|
||||
generateUiPassword,
|
||||
getInstanceFilePath,
|
||||
getPidFilePath,
|
||||
@@ -1456,3 +1458,93 @@ describe('Windows startup task command builder', () => {
|
||||
expect(cmd).not.toContain('-Command ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('startup command lingering output', () => {
|
||||
const linuxStatus = (lingerEnabled, lingerUser = 'alice') => ({
|
||||
supported: true,
|
||||
platform: 'linux',
|
||||
enabled: true,
|
||||
active: true,
|
||||
activeState: 'active',
|
||||
servicePath: '/home/alice/.config/systemd/user/openchamber.service',
|
||||
lingerEnabled,
|
||||
lingerUser,
|
||||
});
|
||||
|
||||
const dependenciesFor = (status) => ({
|
||||
getStartupStatus: () => status,
|
||||
enableStartupService: () => status,
|
||||
disableStartupService: () => status,
|
||||
});
|
||||
|
||||
const runCommand = (status, options, action) => startupCommand(options, action, dependenciesFor(status));
|
||||
|
||||
it.each([
|
||||
['enable', true, 'ok', undefined],
|
||||
['enable', false, 'warning', 'LINGER_DISABLED'],
|
||||
['enable', null, 'warning', 'LINGER_UNKNOWN'],
|
||||
['status', true, 'ok', undefined],
|
||||
['status', false, 'warning', 'LINGER_DISABLED'],
|
||||
['status', null, 'warning', 'LINGER_UNKNOWN'],
|
||||
])('reports %s with Linux linger=%s as JSON-only output', async (action, lingerEnabled, expectedStatus, warningCode) => {
|
||||
const output = await captureStdout(() => runCommand(linuxStatus(lingerEnabled), { json: true }, action));
|
||||
const payload = JSON.parse(output);
|
||||
|
||||
expect(payload.status).toBe(expectedStatus);
|
||||
expect(payload.action).toBe(action);
|
||||
expect(payload.lingerEnabled).toBe(lingerEnabled);
|
||||
expect(payload.messages?.[0]?.code).toBe(warningCode);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['enable', true, 'yes'],
|
||||
['enable', false, 'no'],
|
||||
['enable', null, 'unknown'],
|
||||
['status', true, 'yes'],
|
||||
['status', false, 'no'],
|
||||
['status', null, 'unknown'],
|
||||
])('reports %s with Linux linger=%s in one quiet result line', async (action, lingerEnabled, label) => {
|
||||
const output = await captureStdout(() => runCommand(linuxStatus(lingerEnabled), { quiet: true }, action));
|
||||
|
||||
expect(output.split('\n')).toHaveLength(2);
|
||||
expect(output).toContain(` linger:${label}\n`);
|
||||
expect(output).not.toContain('loginctl');
|
||||
});
|
||||
|
||||
it.each([true, false])('warns with an actionable command in human TTY=%s output', async (isTTY) => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(process.stdout, 'isTTY');
|
||||
Object.defineProperty(process.stdout, 'isTTY', { configurable: true, value: isTTY });
|
||||
try {
|
||||
const output = await captureStdout(() => runCommand(linuxStatus(false), {}, 'enable'));
|
||||
|
||||
expect(output).toContain('[LINGER_DISABLED]');
|
||||
expect(output).toContain('sudo loginctl enable-linger alice');
|
||||
} finally {
|
||||
if (descriptor) Object.defineProperty(process.stdout, 'isTTY', descriptor);
|
||||
else delete process.stdout.isTTY;
|
||||
}
|
||||
});
|
||||
|
||||
it('reports unknown state without inventing a user when detection is unavailable', async () => {
|
||||
const output = await captureStdout(() => runCommand(linuxStatus(null, null), {}, 'status'));
|
||||
|
||||
expect(output).toContain('[LINGER_UNKNOWN]');
|
||||
expect(output).toContain('loginctl show-user "$USER" -p Linger');
|
||||
});
|
||||
|
||||
it('reports disabled-service linger state without warning or remediation', async () => {
|
||||
const output = await captureStdout(() => runCommand({ ...linuxStatus(false), enabled: false }, {}, 'status'));
|
||||
|
||||
expect(output).toContain('user lingering is disabled');
|
||||
expect(output).not.toContain('[LINGER_DISABLED]');
|
||||
expect(output).not.toContain('loginctl enable-linger');
|
||||
});
|
||||
|
||||
it('does not emit linger guidance for unsupported systems', async () => {
|
||||
await expect(runCommand(
|
||||
{ supported: false, platform: 'freebsd', enabled: false, servicePath: null },
|
||||
{ json: true },
|
||||
'status'
|
||||
)).rejects.toMatchObject({ exitCode: EXIT_CODE.USAGE_ERROR });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user