fix(cli): warn when systemd user lingering is disabled (#1855)

This commit is contained in:
Ibrahim Khan
2026-09-05 18:12:43 +03:00
committed by GitHub
parent 391ea57731
commit 3a96c7ce64
3 changed files with 183 additions and 6 deletions
+92
View File
@@ -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 });
});
});