|
1 | 1 | import { EventEmitter } from "node:events"; |
| 2 | +import fs from "node:fs"; |
| 3 | +import os from "node:os"; |
| 4 | +import path from "node:path"; |
2 | 5 | import { afterEach, describe, expect, it, vi } from "vitest"; |
3 | 6 | import { testing } from "./invoke.js"; |
4 | 7 |
|
@@ -124,4 +127,93 @@ describe("runCommand", () => { |
124 | 127 | error: null, |
125 | 128 | }); |
126 | 129 | }); |
| 130 | + |
| 131 | + describe("working directory failures", () => { |
| 132 | + const enoent = (message: string) => |
| 133 | + Object.assign(new Error(message), { code: "ENOENT" }) as NodeJS.ErrnoException; |
| 134 | + |
| 135 | + it("blames a missing working directory instead of the shell", () => { |
| 136 | + const cwd = path.join(os.tmpdir(), `node-exec-missing-${process.pid}-${Date.now()}`); |
| 137 | + expect(testing.clarifyNodeExecCwdSpawnError(enoent("spawn /bin/sh ENOENT"), cwd)).toBe( |
| 138 | + `node exec working directory does not exist on the node host: ${cwd} (os reported: spawn /bin/sh ENOENT)`, |
| 139 | + ); |
| 140 | + }); |
| 141 | + |
| 142 | + it("flags a cwd that exists but is not a directory", () => { |
| 143 | + const file = path.join(os.tmpdir(), `node-exec-file-${process.pid}-${Date.now()}.txt`); |
| 144 | + fs.writeFileSync(file, "x"); |
| 145 | + try { |
| 146 | + const error = Object.assign(new Error("spawn ENOTDIR"), { |
| 147 | + code: "ENOTDIR", |
| 148 | + }) as NodeJS.ErrnoException; |
| 149 | + expect(testing.clarifyNodeExecCwdSpawnError(error, file)).toBe( |
| 150 | + `node exec working directory is not a directory on the node host: ${file} (os reported: spawn ENOTDIR)`, |
| 151 | + ); |
| 152 | + } finally { |
| 153 | + fs.rmSync(file, { force: true }); |
| 154 | + } |
| 155 | + }); |
| 156 | + |
| 157 | + it("clarifies an asynchronous spawn error for a missing cwd", async () => { |
| 158 | + const child = createMockChild(); |
| 159 | + mockNextSpawn(child); |
| 160 | + const cwd = path.join(os.tmpdir(), `node-exec-run-missing-${process.pid}-${Date.now()}`); |
| 161 | + |
| 162 | + const resultPromise = testing.runCommand(["/bin/sh"], cwd, undefined, undefined); |
| 163 | + child.emit("error", enoent("spawn /bin/sh ENOENT")); |
| 164 | + |
| 165 | + await expect(resultPromise).resolves.toMatchObject({ |
| 166 | + success: false, |
| 167 | + error: expect.stringContaining( |
| 168 | + `node exec working directory does not exist on the node host: ${cwd}`, |
| 169 | + ), |
| 170 | + }); |
| 171 | + }); |
| 172 | + |
| 173 | + it("clarifies a synchronous spawn throw for a cwd that is a file", async () => { |
| 174 | + const file = path.join(os.tmpdir(), `node-exec-run-file-${process.pid}-${Date.now()}.txt`); |
| 175 | + fs.writeFileSync(file, "x"); |
| 176 | + vi.mocked(spawn).mockImplementationOnce(() => { |
| 177 | + throw Object.assign(new Error("spawn ENOTDIR"), { code: "ENOTDIR" }); |
| 178 | + }); |
| 179 | + try { |
| 180 | + await expect( |
| 181 | + testing.runCommand(["/bin/sh"], file, undefined, undefined), |
| 182 | + ).resolves.toMatchObject({ |
| 183 | + success: false, |
| 184 | + error: expect.stringContaining( |
| 185 | + `node exec working directory is not a directory on the node host: ${file}`, |
| 186 | + ), |
| 187 | + }); |
| 188 | + } finally { |
| 189 | + fs.rmSync(file, { force: true }); |
| 190 | + } |
| 191 | + }); |
| 192 | + |
| 193 | + it("preserves genuine executable and unrelated spawn errors", () => { |
| 194 | + const missingExecutable = "spawn /usr/bin/does-not-exist ENOENT"; |
| 195 | + expect(testing.clarifyNodeExecCwdSpawnError(enoent(missingExecutable), os.tmpdir())).toBe( |
| 196 | + missingExecutable, |
| 197 | + ); |
| 198 | + expect(testing.clarifyNodeExecCwdSpawnError(enoent("spawn /bin/sh ENOENT"), undefined)).toBe( |
| 199 | + "spawn /bin/sh ENOENT", |
| 200 | + ); |
| 201 | + const denied = Object.assign(new Error("spawn EACCES"), { |
| 202 | + code: "EACCES", |
| 203 | + }) as NodeJS.ErrnoException; |
| 204 | + expect(testing.clarifyNodeExecCwdSpawnError(denied, "/missing")).toBe("spawn EACCES"); |
| 205 | + }); |
| 206 | + |
| 207 | + it("preserves the spawn error when the cwd cannot be inspected", () => { |
| 208 | + const message = "spawn /bin/sh ENOENT"; |
| 209 | + const stat = vi.spyOn(fs, "statSync").mockImplementationOnce(() => { |
| 210 | + throw Object.assign(new Error("permission denied"), { code: "EACCES" }); |
| 211 | + }); |
| 212 | + try { |
| 213 | + expect(testing.clarifyNodeExecCwdSpawnError(enoent(message), "/unreadable")).toBe(message); |
| 214 | + } finally { |
| 215 | + stat.mockRestore(); |
| 216 | + } |
| 217 | + }); |
| 218 | + }); |
127 | 219 | }); |
0 commit comments