fix(daemon): handle systemctl is-enabled exit 4 (not-found) on Ubuntu (#33634)

Merged via squash.

Prepared head SHA: 67dffc3ee2
Co-authored-by: Yuandiaodiaodiao <33371662+Yuandiaodiaodiao@users.noreply.github.com>
Co-authored-by: shakkernerd <165377636+shakkernerd@users.noreply.github.com>
Reviewed-by: @shakkernerd
This commit is contained in:
a
2026-03-05 00:13:45 +08:00
committed by GitHub
parent 3fa43ec221
commit 4fb40497d4
4 changed files with 22 additions and 3 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Models/custom provider headers: propagate `models.providers.<name>.headers` across inline, fallback, and registry-found model resolution so header-authenticated proxies consistently receive configured request headers. (#27490) thanks @Sid-Qin.
- Daemon/systemd install robustness: treat `systemctl --user is-enabled` exit-code-4 `not-found` responses as not-enabled by combining stderr/stdout detail parsing, so Ubuntu fresh installs no longer fail with `systemctl is-enabled unavailable`. (#33634) Thanks @Yuandiaodiaodiao.
- Slack/system-event session routing: resolve reaction/member/pin/interaction system-event session keys through channel/account bindings (with sender-aware DM routing) so inbound Slack events target the correct agent session in multi-account setups instead of defaulting to `agent:main`. (#34045) Thanks @paulomcg, @daht-mad and @vincentkoc.
- Gateway/HTTP tools invoke media compatibility: preserve raw media payload access for direct `/tools/invoke` clients by allowing media `nodes` invoke commands only in HTTP tool context, while keeping agent-context media invoke blocking to prevent base64 prompt bloat. (#34365) Thanks @obviyus.
- Agents/Nodes media outputs: add dedicated `photos_latest` action handling, block media-returning `nodes invoke` commands, keep metadata-only `camera.list` invoke allowed, and normalize empty `photos_latest` results to a consistent response shape to prevent base64 context bloat. (#34332) Thanks @obviyus.

View File

@@ -90,6 +90,21 @@ describe("isSystemdServiceEnabled", () => {
"systemctl is-enabled unavailable: Failed to connect to bus",
);
});
it("returns false when systemctl is-enabled exits with code 4 (not-found)", async () => {
const { isSystemdServiceEnabled } = await import("./systemd.js");
execFileMock.mockImplementationOnce((_cmd, _args, _opts, cb) => {
// On Ubuntu 24.04, `systemctl --user is-enabled <unit>` exits with
// code 4 and prints "not-found" to stdout when the unit doesn't exist.
const err = new Error(
"Command failed: systemctl --user is-enabled openclaw-gateway.service",
) as Error & { code?: number };
err.code = 4;
cb(err, "not-found\n", "");
});
const result = await isSystemdServiceEnabled({ env: {} });
expect(result).toBe(false);
});
});
describe("systemd runtime parsing", () => {

View File

@@ -143,7 +143,10 @@ async function execSystemctl(
}
function readSystemctlDetail(result: { stdout: string; stderr: string }): string {
return (result.stderr || result.stdout || "").trim();
// Concatenate both streams so pattern matchers (isSystemdUnitNotEnabled,
// isSystemctlMissing) can see the unit status from stdout even when
// execFileUtf8 populates stderr with the Node error message fallback.
return `${result.stderr} ${result.stdout}`.trim();
}
function isSystemctlMissing(detail: string): boolean {

View File

@@ -27,14 +27,14 @@ describe("plugin-sdk root alias", () => {
expect(parsed.success).toBe(false);
});
it("loads legacy root exports lazily through the proxy", () => {
it("loads legacy root exports lazily through the proxy", { timeout: 240_000 }, () => {
expect(typeof rootSdk.resolveControlCommandGate).toBe("function");
expect(typeof rootSdk.default).toBe("object");
expect(rootSdk.default).toBe(rootSdk);
expect(rootSdk.__esModule).toBe(true);
});
it("preserves reflection semantics for lazily resolved exports", () => {
it("preserves reflection semantics for lazily resolved exports", { timeout: 240_000 }, () => {
expect("resolveControlCommandGate" in rootSdk).toBe(true);
const keys = Object.keys(rootSdk);
expect(keys).toContain("resolveControlCommandGate");