Skip to content

Commit 131188b

Browse files
arul28claude
andcommitted
test: add automated tests for Windows port foundations
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eac37cf commit 131188b

2 files changed

Lines changed: 219 additions & 1 deletion

File tree

apps/desktop/src/main/services/shared/processExecution.test.ts

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
1-
import { describe, expect, it } from "vitest";
1+
import type * as childProcess from "node:child_process";
2+
import { afterEach, describe, expect, it, vi } from "vitest";
3+
4+
const spawnSyncMock = vi.fn();
5+
6+
vi.mock("node:child_process", async () => {
7+
const actual = await vi.importActual<typeof childProcess>("node:child_process");
8+
return {
9+
...actual,
10+
spawnSync: (...args: unknown[]) => spawnSyncMock(...args),
11+
};
12+
});
13+
214
import {
15+
killWindowsProcessTree,
316
quoteWindowsCmdArg,
417
resolveCliSpawnInvocation,
518
resolveWindowsCmdInvocation,
619
shouldUseWindowsCmdWrapper,
20+
terminateProcessTree,
721
} from "./processExecution";
822

923
describe("processExecution", () => {
@@ -49,3 +63,171 @@ describe("processExecution", () => {
4963
});
5064
});
5165
});
66+
67+
describe("killWindowsProcessTree", () => {
68+
afterEach(() => {
69+
spawnSyncMock.mockReset();
70+
});
71+
72+
it("shells out to taskkill /T /F and returns true on exit 0", () => {
73+
spawnSyncMock.mockReturnValueOnce({ status: 0, stdout: "", stderr: "", error: null });
74+
const failure = vi.fn();
75+
76+
expect(killWindowsProcessTree(4321, failure)).toBe(true);
77+
expect(failure).not.toHaveBeenCalled();
78+
expect(spawnSyncMock).toHaveBeenCalledTimes(1);
79+
const [command, args, options] = spawnSyncMock.mock.calls[0]!;
80+
expect(command).toBe("taskkill.exe");
81+
expect(args).toEqual(["/T", "/F", "/PID", "4321"]);
82+
expect(options).toMatchObject({ windowsHide: true });
83+
});
84+
85+
it("invokes the failure callback with taskkill stderr when exit is non-zero", () => {
86+
spawnSyncMock.mockReturnValueOnce({
87+
status: 128,
88+
stdout: Buffer.from("out", "utf8"),
89+
stderr: Buffer.from("Access denied.", "utf8"),
90+
error: null,
91+
});
92+
const failure = vi.fn();
93+
94+
expect(killWindowsProcessTree(1234, failure)).toBe(false);
95+
expect(failure).toHaveBeenCalledTimes(1);
96+
expect(failure).toHaveBeenCalledWith({
97+
pid: 1234,
98+
status: 128,
99+
stdout: "out",
100+
stderr: "Access denied.",
101+
error: null,
102+
});
103+
});
104+
105+
it("invokes the failure callback when spawnSync throws", () => {
106+
const thrown = new Error("spawn failed");
107+
spawnSyncMock.mockImplementationOnce(() => {
108+
throw thrown;
109+
});
110+
const failure = vi.fn();
111+
112+
expect(killWindowsProcessTree(55, failure)).toBe(false);
113+
expect(failure).toHaveBeenCalledWith({
114+
pid: 55,
115+
status: null,
116+
stdout: "",
117+
stderr: "",
118+
error: thrown,
119+
});
120+
});
121+
122+
it("rejects non-positive or non-integer pids without shelling out", () => {
123+
const failure = vi.fn();
124+
125+
expect(killWindowsProcessTree(0, failure)).toBe(false);
126+
expect(killWindowsProcessTree(-7, failure)).toBe(false);
127+
expect(killWindowsProcessTree(3.14, failure)).toBe(false);
128+
expect(spawnSyncMock).not.toHaveBeenCalled();
129+
expect(failure).not.toHaveBeenCalled();
130+
});
131+
});
132+
133+
type FakeChildShape = Pick<childProcess.ChildProcess, "kill" | "pid" | "exitCode" | "signalCode">;
134+
135+
function fakeChild(overrides: Partial<FakeChildShape> = {}): FakeChildShape & { kill: ReturnType<typeof vi.fn> } {
136+
return {
137+
pid: 4321,
138+
exitCode: null,
139+
signalCode: null,
140+
kill: vi.fn(() => true),
141+
...overrides,
142+
} as FakeChildShape & { kill: ReturnType<typeof vi.fn> };
143+
}
144+
145+
const originalPlatform = process.platform;
146+
147+
function setPlatform(value: NodeJS.Platform): void {
148+
Object.defineProperty(process, "platform", {
149+
value,
150+
configurable: true,
151+
});
152+
}
153+
154+
describe("terminateProcessTree", () => {
155+
afterEach(() => {
156+
spawnSyncMock.mockReset();
157+
setPlatform(originalPlatform);
158+
});
159+
160+
it("on non-Windows, forwards the signal to child.kill and returns its result", () => {
161+
setPlatform("linux");
162+
const child = fakeChild();
163+
164+
expect(terminateProcessTree(child, "SIGTERM")).toBe(true);
165+
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
166+
expect(spawnSyncMock).not.toHaveBeenCalled();
167+
});
168+
169+
it("on non-Windows, returns false and does not throw when child.kill throws", () => {
170+
setPlatform("linux");
171+
const child = fakeChild({
172+
kill: vi.fn(() => {
173+
throw new Error("ESRCH");
174+
}),
175+
});
176+
177+
expect(terminateProcessTree(child, "SIGKILL")).toBe(false);
178+
});
179+
180+
it("on Windows, calls taskkill for a live child and skips child.kill on success", () => {
181+
setPlatform("win32");
182+
spawnSyncMock.mockReturnValueOnce({ status: 0, stdout: "", stderr: "", error: null });
183+
const child = fakeChild();
184+
185+
expect(terminateProcessTree(child)).toBe(true);
186+
expect(spawnSyncMock).toHaveBeenCalledTimes(1);
187+
expect(spawnSyncMock.mock.calls[0]![1]).toEqual(["/T", "/F", "/PID", "4321"]);
188+
expect(child.kill).not.toHaveBeenCalled();
189+
});
190+
191+
it("on Windows, falls back to child.kill when taskkill fails", () => {
192+
setPlatform("win32");
193+
spawnSyncMock.mockReturnValueOnce({
194+
status: 1,
195+
stdout: "",
196+
stderr: Buffer.from("err", "utf8"),
197+
error: null,
198+
});
199+
const child = fakeChild();
200+
const failure = vi.fn();
201+
202+
expect(terminateProcessTree(child, "SIGTERM", failure)).toBe(true);
203+
expect(failure).toHaveBeenCalledTimes(1);
204+
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
205+
});
206+
207+
it("on Windows, skips taskkill entirely when the child has already exited", () => {
208+
setPlatform("win32");
209+
const child = fakeChild({ exitCode: 0 });
210+
211+
expect(terminateProcessTree(child)).toBe(false);
212+
expect(spawnSyncMock).not.toHaveBeenCalled();
213+
expect(child.kill).not.toHaveBeenCalled();
214+
});
215+
216+
it("on Windows, skips taskkill entirely when the child already received a signal", () => {
217+
setPlatform("win32");
218+
const child = fakeChild({ signalCode: "SIGTERM" });
219+
220+
expect(terminateProcessTree(child)).toBe(false);
221+
expect(spawnSyncMock).not.toHaveBeenCalled();
222+
expect(child.kill).not.toHaveBeenCalled();
223+
});
224+
225+
it("on Windows, falls back to child.kill when pid is missing", () => {
226+
setPlatform("win32");
227+
const child = fakeChild({ pid: undefined });
228+
229+
expect(terminateProcessTree(child, "SIGKILL")).toBe(true);
230+
expect(spawnSyncMock).not.toHaveBeenCalled();
231+
expect(child.kill).toHaveBeenCalledWith("SIGKILL");
232+
});
233+
});

apps/desktop/src/renderer/lib/pathUtils.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { describe, expect, it } from "vitest";
22
import {
33
arePathsEqual,
44
isPathEqualOrDescendant,
5+
isWindowsAbsolutePath,
6+
isWindowsDrivePath,
7+
isWindowsUncPath,
58
normalizePath,
69
normalizePathForComparison,
710
normalizePathForWorkspaceComparison,
@@ -43,3 +46,36 @@ describe("pathUtils", () => {
4346
expect(remapPathForRename("src/Main.ts", "SRC", "Renamed", "C:\\Repo")).toBe("Renamed/Main.ts");
4447
});
4548
});
49+
50+
describe("Windows path predicates", () => {
51+
it("identifies drive-letter paths, including leading-slash and bare-drive forms", () => {
52+
expect(isWindowsDrivePath("C:\\Users\\me")).toBe(true);
53+
expect(isWindowsDrivePath("c:/users/me")).toBe(true);
54+
expect(isWindowsDrivePath("/C:/Users/me")).toBe(true);
55+
expect(isWindowsDrivePath("Z:")).toBe(true);
56+
57+
expect(isWindowsDrivePath("relative/path")).toBe(false);
58+
expect(isWindowsDrivePath("/unix/path")).toBe(false);
59+
expect(isWindowsDrivePath("C:relative")).toBe(false);
60+
});
61+
62+
it("identifies UNC paths in both backslash and forward-slash form", () => {
63+
expect(isWindowsUncPath("\\\\server\\share\\file.txt")).toBe(true);
64+
expect(isWindowsUncPath("//server/share/file.txt")).toBe(true);
65+
66+
expect(isWindowsUncPath("\\\\")).toBe(false);
67+
expect(isWindowsUncPath("//")).toBe(false);
68+
expect(isWindowsUncPath("/unix/path")).toBe(false);
69+
expect(isWindowsUncPath("C:\\path")).toBe(false);
70+
});
71+
72+
it("identifies any Windows absolute path as drive or UNC", () => {
73+
expect(isWindowsAbsolutePath("C:\\Users")).toBe(true);
74+
expect(isWindowsAbsolutePath("\\\\srv\\share")).toBe(true);
75+
expect(isWindowsAbsolutePath("//srv/share")).toBe(true);
76+
77+
expect(isWindowsAbsolutePath("/unix/path")).toBe(false);
78+
expect(isWindowsAbsolutePath("relative/path")).toBe(false);
79+
expect(isWindowsAbsolutePath("")).toBe(false);
80+
});
81+
});

0 commit comments

Comments
 (0)