Files
nanoclaw/setup/systemd-user-env.test.ts
T
glifocat 1512e3f19e fix(setup): re-probe systemd user session with derived env on su- entry
`setup/service.ts` decides between a real systemd user unit and the
nohup fallback by running `systemctl --user daemon-reload` and watching
for the call to succeed. In contexts that bypass pam_systemd —
`su -`, `pct enter`, headless containers — `XDG_RUNTIME_DIR` and
`DBUS_SESSION_BUS_ADDRESS` are not exported, the probe fails with
`Failed to connect to bus: No medium found`, and we install nohup
despite the user manager being healthy on disk.

Before the probe, check whether linger is enabled for the current user
and `/run/user/<uid>` exists; if so, re-derive the env vars from disk
and let them propagate to subsequent `systemctl --user` calls via
`process.env` (execSync inherits by default). If the probe still fails
after that, the existing nohup fallback runs unchanged — and the
warning log now records *which* precondition failed so the cause is
visible without grepping setup.log.

The pure decision function lives in `setup/systemd-user-env.ts` so it
can be tested without execSync. New regression test in
`setup/systemd-user-env.test.ts` covers the #2482 repro plus the
already_set / no_linger / no_runtime_dir / no_user / no_uid branches.

Closes #1981
Closes #2482
2026-05-15 17:16:12 +02:00

91 lines
2.6 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { computeUserSystemdEnv } from './systemd-user-env.js';
function existsFrom(paths: Set<string>) {
return (p: string) => paths.has(p);
}
describe('computeUserSystemdEnv', () => {
it('populates env when linger is on and the runtime dir exists (#2482 repro)', () => {
const result = computeUserSystemdEnv({
uid: 1000,
user: 'nanoclaw',
env: {},
exists: existsFrom(
new Set(['/var/lib/systemd/linger/nanoclaw', '/run/user/1000']),
),
});
expect(result).toEqual({
reason: 'populated',
XDG_RUNTIME_DIR: '/run/user/1000',
DBUS_SESSION_BUS_ADDRESS: 'unix:path=/run/user/1000/bus',
});
});
it('no-ops when XDG_RUNTIME_DIR is already set (SSH login path)', () => {
const result = computeUserSystemdEnv({
uid: 1000,
user: 'nanoclaw',
env: { XDG_RUNTIME_DIR: '/run/user/1000' },
exists: existsFrom(
new Set(['/var/lib/systemd/linger/nanoclaw', '/run/user/1000']),
),
});
expect(result.reason).toBe('already_set');
expect(result.XDG_RUNTIME_DIR).toBeUndefined();
expect(result.DBUS_SESSION_BUS_ADDRESS).toBeUndefined();
});
it('returns no_linger when the linger marker is absent', () => {
const result = computeUserSystemdEnv({
uid: 1000,
user: 'nanoclaw',
env: {},
exists: existsFrom(new Set(['/run/user/1000'])),
});
expect(result.reason).toBe('no_linger');
expect(result.XDG_RUNTIME_DIR).toBeUndefined();
});
it('returns no_runtime_dir when linger is on but /run/user/<uid> is missing', () => {
// The defensive guard from #2482: without this we would point env vars
// at a non-existent socket and the daemon-reload probe would fail with a
// less recoverable error than the bare "No medium found".
const result = computeUserSystemdEnv({
uid: 1000,
user: 'nanoclaw',
env: {},
exists: existsFrom(new Set(['/var/lib/systemd/linger/nanoclaw'])),
});
expect(result.reason).toBe('no_runtime_dir');
expect(result.XDG_RUNTIME_DIR).toBeUndefined();
});
it('returns no_user when USER and LOGNAME are both missing', () => {
const result = computeUserSystemdEnv({
uid: 1000,
user: undefined,
env: {},
exists: existsFrom(new Set()),
});
expect(result.reason).toBe('no_user');
});
it('returns no_uid when process.getuid is unavailable', () => {
const result = computeUserSystemdEnv({
uid: undefined,
user: 'nanoclaw',
env: {},
exists: existsFrom(new Set()),
});
expect(result.reason).toBe('no_uid');
});
});