From 0a783315360cb17774364f6ec5dee51900a905f4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 18 Feb 2026 17:22:26 +0000 Subject: [PATCH] refactor(infra): share shell env timeout normalization --- src/infra/shell-env.test.ts | 40 +++++++++++++++++++++++++++++++++++++ src/infra/shell-env.ts | 17 ++++++++-------- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/infra/shell-env.test.ts b/src/infra/shell-env.test.ts index c2391fb963..98485b2e5a 100644 --- a/src/infra/shell-env.test.ts +++ b/src/infra/shell-env.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it, vi } from "vitest"; import { + getShellPathFromLoginShell, loadShellEnvFallback, + resetShellPathCacheForTests, resolveShellEnvFallbackTimeoutMs, shouldEnableShellEnvFallback, } from "./shell-env.js"; @@ -71,4 +73,42 @@ describe("shell env fallback", () => { expect(env.DISCORD_BOT_TOKEN).toBe("discord"); expect(exec2).not.toHaveBeenCalled(); }); + + it("resolves PATH via login shell and caches it", () => { + resetShellPathCacheForTests(); + const exec = vi.fn(() => Buffer.from("PATH=/usr/local/bin:/usr/bin\0HOME=/tmp\0")); + + const first = getShellPathFromLoginShell({ + env: {} as NodeJS.ProcessEnv, + exec: exec as unknown as Parameters[0]["exec"], + }); + const second = getShellPathFromLoginShell({ + env: {} as NodeJS.ProcessEnv, + exec: exec as unknown as Parameters[0]["exec"], + }); + + expect(first).toBe("/usr/local/bin:/usr/bin"); + expect(second).toBe("/usr/local/bin:/usr/bin"); + expect(exec).toHaveBeenCalledOnce(); + }); + + it("returns null on shell env read failure and caches null", () => { + resetShellPathCacheForTests(); + const exec = vi.fn(() => { + throw new Error("exec failed"); + }); + + const first = getShellPathFromLoginShell({ + env: {} as NodeJS.ProcessEnv, + exec: exec as unknown as Parameters[0]["exec"], + }); + const second = getShellPathFromLoginShell({ + env: {} as NodeJS.ProcessEnv, + exec: exec as unknown as Parameters[0]["exec"], + }); + + expect(first).toBeNull(); + expect(second).toBeNull(); + expect(exec).toHaveBeenCalledOnce(); + }); }); diff --git a/src/infra/shell-env.ts b/src/infra/shell-env.ts index ce86a50337..d1fa53fe01 100644 --- a/src/infra/shell-env.ts +++ b/src/infra/shell-env.ts @@ -6,6 +6,13 @@ const DEFAULT_MAX_BUFFER_BYTES = 2 * 1024 * 1024; let lastAppliedKeys: string[] = []; let cachedShellPath: string | null | undefined; +function resolveTimeoutMs(timeoutMs: number | undefined): number { + if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) { + return DEFAULT_TIMEOUT_MS; + } + return Math.max(0, timeoutMs); +} + function resolveShell(env: NodeJS.ProcessEnv): string { const shell = env.SHELL?.trim(); return shell && shell.length > 0 ? shell : "/bin/sh"; @@ -76,10 +83,7 @@ export function loadShellEnvFallback(opts: ShellEnvFallbackOptions): ShellEnvFal return { ok: true, applied: [], skippedReason: "already-has-keys" }; } - const timeoutMs = - typeof opts.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) - ? Math.max(0, opts.timeoutMs) - : DEFAULT_TIMEOUT_MS; + const timeoutMs = resolveTimeoutMs(opts.timeoutMs); const shell = resolveShell(opts.env); @@ -146,10 +150,7 @@ export function getShellPathFromLoginShell(opts: { } const exec = opts.exec ?? execFileSync; - const timeoutMs = - typeof opts.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) - ? Math.max(0, opts.timeoutMs) - : DEFAULT_TIMEOUT_MS; + const timeoutMs = resolveTimeoutMs(opts.timeoutMs); const shell = resolveShell(opts.env); let stdout: Buffer;