|
1 | 1 | import fs from "node:fs/promises"; |
2 | 2 | import path from "node:path"; |
3 | | -import { describe, expect, it } from "vitest"; |
| 3 | +import { afterEach, describe, expect, it, vi } from "vitest"; |
4 | 4 | import { withTempDir } from "../test-helpers/temp-dir.js"; |
5 | 5 | import { |
6 | 6 | isExecutableFile, |
| 7 | + resolveExecutable, |
7 | 8 | resolveExecutableFromPathEnv, |
8 | 9 | resolveExecutablePath, |
9 | 10 | } from "./executable-path.js"; |
10 | 11 |
|
| 12 | +function restoreEnvValue(name: string, value: string | undefined): void { |
| 13 | + if (value === undefined) { |
| 14 | + delete process.env[name]; |
| 15 | + } else { |
| 16 | + process.env[name] = value; |
| 17 | + } |
| 18 | +} |
| 19 | + |
11 | 20 | describe("executable path helpers", () => { |
12 | 21 | it("detects executable files and rejects directories or non-executables", async () => { |
13 | 22 | await withTempDir({ prefix: "openclaw-exec-path-" }, async (base) => { |
@@ -95,3 +104,95 @@ describe("executable path helpers", () => { |
95 | 104 | ).toBeUndefined(); |
96 | 105 | }); |
97 | 106 | }); |
| 107 | + |
| 108 | +describe("resolveExecutable", () => { |
| 109 | + afterEach(() => { |
| 110 | + vi.restoreAllMocks(); |
| 111 | + }); |
| 112 | + |
| 113 | + it("returns cmd unchanged on non-Windows platforms", () => { |
| 114 | + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("linux"); |
| 115 | + expect(resolveExecutable("gcloud")).toBe("gcloud"); |
| 116 | + platformSpy.mockRestore(); |
| 117 | + }); |
| 118 | + |
| 119 | + it("returns cmd unchanged when it already carries a known PATHEXT extension on Windows", () => { |
| 120 | + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); |
| 121 | + expect(resolveExecutable("gcloud.cmd")).toBe("gcloud.cmd"); |
| 122 | + expect(resolveExecutable("gcloud.exe")).toBe("gcloud.exe"); |
| 123 | + expect(resolveExecutable("gcloud.bat")).toBe("gcloud.bat"); |
| 124 | + expect(resolveExecutable("gcloud.com")).toBe("gcloud.com"); |
| 125 | + platformSpy.mockRestore(); |
| 126 | + }); |
| 127 | + |
| 128 | + it("resolves to the first .cmd result from PATH on Windows without executing where.exe", async () => { |
| 129 | + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); |
| 130 | + await withTempDir({ prefix: "openclaw-exec-path-" }, async (base) => { |
| 131 | + const binDir = path.join(base, "bin"); |
| 132 | + await fs.mkdir(binDir, { recursive: true }); |
| 133 | + const cmdPath = path.join(binDir, "gcloud.cmd"); |
| 134 | + const exePath = path.join(binDir, "gcloud.exe"); |
| 135 | + await fs.writeFile(cmdPath, "@echo off\n", "utf8"); |
| 136 | + await fs.writeFile(exePath, "exe\n", "utf8"); |
| 137 | + |
| 138 | + const originalPath = process.env.PATH; |
| 139 | + const originalPathext = process.env.PATHEXT; |
| 140 | + process.env.PATH = binDir; |
| 141 | + process.env.PATHEXT = ".EXE;.CMD;.BAT;.COM"; |
| 142 | + try { |
| 143 | + expect(resolveExecutable("gcloud")).toBe(cmdPath); |
| 144 | + } finally { |
| 145 | + restoreEnvValue("PATH", originalPath); |
| 146 | + restoreEnvValue("PATHEXT", originalPathext); |
| 147 | + } |
| 148 | + }); |
| 149 | + platformSpy.mockRestore(); |
| 150 | + }); |
| 151 | + |
| 152 | + it("falls back to .exe when no .cmd match exists on Windows", async () => { |
| 153 | + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); |
| 154 | + await withTempDir({ prefix: "openclaw-exec-path-" }, async (base) => { |
| 155 | + const binDir = path.join(base, "bin"); |
| 156 | + await fs.mkdir(binDir, { recursive: true }); |
| 157 | + const exePath = path.join(binDir, "tailscale.exe"); |
| 158 | + await fs.writeFile(exePath, "exe\n", "utf8"); |
| 159 | + |
| 160 | + const originalPath = process.env.PATH; |
| 161 | + process.env.PATH = binDir; |
| 162 | + try { |
| 163 | + expect(resolveExecutable("tailscale")).toBe(exePath); |
| 164 | + } finally { |
| 165 | + restoreEnvValue("PATH", originalPath); |
| 166 | + } |
| 167 | + }); |
| 168 | + platformSpy.mockRestore(); |
| 169 | + }); |
| 170 | + |
| 171 | + it("falls back to first PATH result when no .cmd or .exe match exists on Windows", async () => { |
| 172 | + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); |
| 173 | + await withTempDir({ prefix: "openclaw-exec-path-" }, async (base) => { |
| 174 | + const binDir = path.join(base, "bin"); |
| 175 | + await fs.mkdir(binDir, { recursive: true }); |
| 176 | + const ps1Path = path.join(binDir, "gcloud.ps1"); |
| 177 | + await fs.writeFile(ps1Path, "Write-Output ok\n", "utf8"); |
| 178 | + |
| 179 | + const originalPath = process.env.PATH; |
| 180 | + const originalPathext = process.env.PATHEXT; |
| 181 | + process.env.PATH = binDir; |
| 182 | + process.env.PATHEXT = ".PS1"; |
| 183 | + try { |
| 184 | + expect(resolveExecutable("gcloud")).toBe(ps1Path); |
| 185 | + } finally { |
| 186 | + restoreEnvValue("PATH", originalPath); |
| 187 | + restoreEnvValue("PATHEXT", originalPathext); |
| 188 | + } |
| 189 | + }); |
| 190 | + platformSpy.mockRestore(); |
| 191 | + }); |
| 192 | + |
| 193 | + it("returns original cmd when no PATH match exists on Windows", () => { |
| 194 | + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); |
| 195 | + expect(resolveExecutable("gog")).toBe("gog"); |
| 196 | + platformSpy.mockRestore(); |
| 197 | + }); |
| 198 | +}); |
0 commit comments