From dd860e76aa38254f7eac1f321cb23f01f538e299 Mon Sep 17 00:00:00 2001 From: Kevin ONeill Date: Sun, 22 Mar 2026 17:27:16 -0500 Subject: [PATCH] fix: normalize env var keys and isolate tests from real .env - Apply normalizeEnvVarKey({ portable: true }) before security filtering, matching the established pattern in env-vars.ts. Rejects non-portable key names (spaces, special chars) that would produce invalid plist/systemd syntax. - Isolate existing tests from the developer's real ~/.openclaw/.env by providing a temp HOME directory, preventing flaky failures when the test machine has a populated .env file. --- src/commands/daemon-install-helpers.test.ts | 16 +++++++++++++--- src/commands/daemon-install-helpers.ts | 9 +++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/commands/daemon-install-helpers.test.ts b/src/commands/daemon-install-helpers.test.ts index a05176aee9f..b564c33055b 100644 --- a/src/commands/daemon-install-helpers.test.ts +++ b/src/commands/daemon-install-helpers.test.ts @@ -86,11 +86,21 @@ function mockNodeGatewayPlanFixture( } describe("buildGatewayInstallPlan", () => { + // Prevent tests from reading the developer's real ~/.openclaw/.env when + // passing `env: {}` (which falls back to os.homedir for state-dir resolution). + let isolatedHome: string; + beforeEach(() => { + isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), "oc-plan-test-")); + }); + afterEach(() => { + fs.rmSync(isolatedHome, { recursive: true, force: true }); + }); + it("uses provided nodePath and returns plan", async () => { mockNodeGatewayPlanFixture(); const plan = await buildGatewayInstallPlan({ - env: {}, + env: { HOME: isolatedHome }, port: 3000, runtime: "node", nodePath: "/custom/node", @@ -102,7 +112,7 @@ describe("buildGatewayInstallPlan", () => { expect(mocks.resolvePreferredNodePath).not.toHaveBeenCalled(); expect(mocks.buildServiceEnvironment).toHaveBeenCalledWith( expect.objectContaining({ - env: {}, + env: { HOME: isolatedHome }, port: 3000, extraPathDirs: ["/custom"], }), @@ -113,7 +123,7 @@ describe("buildGatewayInstallPlan", () => { mockNodeGatewayPlanFixture(); await buildGatewayInstallPlan({ - env: {}, + env: { HOME: isolatedHome }, port: 3000, runtime: "node", nodePath: "node", diff --git a/src/commands/daemon-install-helpers.ts b/src/commands/daemon-install-helpers.ts index e3ad0b491ee..3797ab8ee09 100644 --- a/src/commands/daemon-install-helpers.ts +++ b/src/commands/daemon-install-helpers.ts @@ -15,6 +15,7 @@ import { buildServiceEnvironment } from "../daemon/service-env.js"; import { isDangerousHostEnvOverrideVarName, isDangerousHostEnvVarName, + normalizeEnvVarKey, } from "../infra/host-env-security.js"; import { emitDaemonInstallRuntimeWarning, @@ -49,8 +50,12 @@ export function readStateDirDotEnvVars( const parsed = dotenv.parse(content); const entries: Record = {}; - for (const [key, value] of Object.entries(parsed)) { - if (!key || !value?.trim()) { + for (const [rawKey, value] of Object.entries(parsed)) { + if (!value?.trim()) { + continue; + } + const key = normalizeEnvVarKey(rawKey, { portable: true }); + if (!key) { continue; } if (isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)) {