diff --git a/src/infra/shell-env.test.ts b/src/infra/shell-env.test.ts index 64be7f28f..52d65c9ed 100644 --- a/src/infra/shell-env.test.ts +++ b/src/infra/shell-env.test.ts @@ -2,10 +2,12 @@ import fs from "node:fs"; import os from "node:os"; import { describe, expect, it, vi } from "vitest"; import { + getShellEnvAppliedKeys, getShellPathFromLoginShell, loadShellEnvFallback, resetShellPathCacheForTests, resolveShellEnvFallbackTimeoutMs, + shouldDeferShellEnvFallback, shouldEnableShellEnvFallback, } from "./shell-env.js"; @@ -119,6 +121,12 @@ describe("shell env fallback", () => { expect(shouldEnableShellEnvFallback({ OPENCLAW_LOAD_SHELL_ENV: "1" })).toBe(true); }); + it("uses the same truthy env parsing for deferred fallback", () => { + expect(shouldDeferShellEnvFallback({} as NodeJS.ProcessEnv)).toBe(false); + expect(shouldDeferShellEnvFallback({ OPENCLAW_DEFER_SHELL_ENV_FALLBACK: "false" })).toBe(false); + expect(shouldDeferShellEnvFallback({ OPENCLAW_DEFER_SHELL_ENV_FALLBACK: "yes" })).toBe(true); + }); + it("resolves timeout from env with default fallback", () => { expect(resolveShellEnvFallbackTimeoutMs({} as NodeJS.ProcessEnv)).toBe(15000); expect(resolveShellEnvFallbackTimeoutMs({ OPENCLAW_SHELL_ENV_TIMEOUT_MS: "42" })).toBe(42); @@ -179,6 +187,57 @@ describe("shell env fallback", () => { expect(exec2).not.toHaveBeenCalled(); }); + it("tracks last applied keys across success, skip, and failure paths", () => { + const successEnv: NodeJS.ProcessEnv = {}; + const successExec = vi.fn(() => + Buffer.from("OPENAI_API_KEY=from-shell\0DISCORD_BOT_TOKEN=\0EXTRA=ignored\0"), + ); + expect( + loadShellEnvFallback({ + enabled: true, + env: successEnv, + expectedKeys: ["OPENAI_API_KEY", "DISCORD_BOT_TOKEN"], + exec: successExec as unknown as Parameters[0]["exec"], + }), + ).toEqual({ + ok: true, + applied: ["OPENAI_API_KEY"], + }); + expect(getShellEnvAppliedKeys()).toEqual(["OPENAI_API_KEY"]); + + expect( + loadShellEnvFallback({ + enabled: false, + env: {}, + expectedKeys: ["OPENAI_API_KEY"], + exec: successExec as unknown as Parameters[0]["exec"], + }), + ).toEqual({ + ok: true, + applied: [], + skippedReason: "disabled", + }); + expect(getShellEnvAppliedKeys()).toEqual([]); + + const failureExec = vi.fn(() => { + throw new Error("boom"); + }); + expect( + loadShellEnvFallback({ + enabled: true, + env: {}, + expectedKeys: ["OPENAI_API_KEY"], + exec: failureExec as unknown as Parameters[0]["exec"], + logger: { warn: vi.fn() }, + }), + ).toMatchObject({ + ok: false, + applied: [], + error: "boom", + }); + expect(getShellEnvAppliedKeys()).toEqual([]); + }); + it("resolves PATH via login shell and caches it", () => { const exec = vi.fn(() => Buffer.from("PATH=/usr/local/bin:/usr/bin\0HOME=/tmp\0")); @@ -207,6 +266,19 @@ describe("shell env fallback", () => { expect(exec).toHaveBeenCalledOnce(); }); + it("returns null when login shell PATH is blank", () => { + const exec = vi.fn(() => Buffer.from("PATH= \0HOME=/tmp\0")); + + const { first, second } = probeShellPathWithFreshCache({ + exec, + platform: "linux", + }); + + expect(first).toBeNull(); + expect(second).toBeNull(); + expect(exec).toHaveBeenCalledOnce(); + }); + it("falls back to /bin/sh when SHELL is non-absolute", () => { const { res, exec } = runShellEnvFallbackForShell("zsh");