Skip to content

Commit f62b1ce

Browse files
committed
fix: resolve Windows CI failures (CRLF, MCP spawn, test skips)
1 parent f1e6968 commit f62b1ce

5 files changed

Lines changed: 129 additions & 108 deletions

File tree

src/mcp/mcp-client.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,10 @@ export class McpClient {
128128
const isWindows = os.platform() === "win32";
129129

130130
if (isWindows) {
131-
// On Windows, .cmd files require shell: true to be spawned.
132-
// Build a single command string so cmd.exe handles quoting correctly.
133-
const cmd = [this.command + ".cmd", ...args].join(" ");
134-
this.process = spawn(cmd, [], {
131+
// On Windows, shell: true lets cmd.exe resolve the command via
132+
// PATHEXT (npx → npx.cmd, etc.) without blindly appending .cmd,
133+
// which would break absolute paths like process.execPath.
134+
this.process = spawn(this.command, args, {
135135
stdio: ["pipe", "pipe", "pipe"],
136136
env: childEnv,
137137
shell: true,

src/tests/clipboard.test.ts

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -36,40 +36,44 @@ test("readClipboardImage returns null when no clipboard helpers are installed",
3636
assert.equal(result, null);
3737
});
3838

39-
test("readClipboardImage uses osascript fallback on macOS when pngpaste is missing", async () => {
40-
const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-clipboard-test-bin-"));
41-
try {
42-
fs.writeFileSync(path.join(binDir, "pngpaste"), "#!/bin/sh\nexit 1\n", { mode: 0o755 });
43-
fs.writeFileSync(
44-
path.join(binDir, "osascript"),
45-
[
46-
"#!/bin/sh",
47-
'for arg in "$@"; do',
48-
' case "$arg" in',
49-
" *'open for access POSIX file " + '"' + "'*)",
50-
' path_part=${arg#*POSIX file \\"}',
51-
' out_path=${path_part%%\\"*}',
52-
' printf fakepng > "$out_path"',
53-
" exit 0",
54-
" ;;",
55-
" esac",
56-
"done",
57-
"exit 1",
58-
"",
59-
].join("\n"),
60-
{ mode: 0o755 }
61-
);
39+
test(
40+
"readClipboardImage uses osascript fallback on macOS when pngpaste is missing",
41+
{ skip: process.platform === "win32" },
42+
async () => {
43+
const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-clipboard-test-bin-"));
44+
try {
45+
fs.writeFileSync(path.join(binDir, "pngpaste"), "#!/bin/sh\nexit 1\n", { mode: 0o755 });
46+
fs.writeFileSync(
47+
path.join(binDir, "osascript"),
48+
[
49+
"#!/bin/sh",
50+
'for arg in "$@"; do',
51+
' case "$arg" in',
52+
" *'open for access POSIX file " + '"' + "'*)",
53+
' path_part=${arg#*POSIX file \\"}',
54+
' out_path=${path_part%%\\"*}',
55+
' printf fakepng > "$out_path"',
56+
" exit 0",
57+
" ;;",
58+
" esac",
59+
"done",
60+
"exit 1",
61+
"",
62+
].join("\n"),
63+
{ mode: 0o755 }
64+
);
6265

63-
const moduleUrl = new URL(`../ui/clipboard.ts?t=${Date.now()}`, import.meta.url).href;
64-
const { readClipboardImage } = (await import(moduleUrl)) as ClipboardModule;
66+
const moduleUrl = new URL(`../ui/clipboard.ts?t=${Date.now()}`, import.meta.url).href;
67+
const { readClipboardImage } = (await import(moduleUrl)) as ClipboardModule;
6568

66-
process.env.PATH = binDir;
67-
const result = withPlatform("darwin", () => readClipboardImage());
68-
assert.equal(result?.mimeType, "image/png");
69-
assert.equal(result?.dataUrl, `data:image/png;base64,${Buffer.from("fakepng").toString("base64")}`);
70-
} finally {
71-
process.env.PATH = ORIGINAL_PATH;
72-
Object.defineProperty(process, "platform", { value: ORIGINAL_PLATFORM });
73-
fs.rmSync(binDir, { recursive: true, force: true });
69+
process.env.PATH = binDir;
70+
const result = withPlatform("darwin", () => readClipboardImage());
71+
assert.equal(result?.mimeType, "image/png");
72+
assert.equal(result?.dataUrl, `data:image/png;base64,${Buffer.from("fakepng").toString("base64")}`);
73+
} finally {
74+
process.env.PATH = ORIGINAL_PATH;
75+
Object.defineProperty(process, "platform", { value: ORIGINAL_PLATFORM });
76+
fs.rmSync(binDir, { recursive: true, force: true });
77+
}
7478
}
75-
});
79+
);

src/tests/settings-and-notify.test.ts

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -364,39 +364,43 @@ test("buildNotifyEnv injects DURATION", () => {
364364
assert.equal(env.DURATION, "2");
365365
});
366366

367-
test("launchNotifyScript passes DURATION and falls back to /bin/sh for non-executable scripts", () => {
368-
const calls: Array<{
369-
command: string;
370-
args: string[];
371-
options: { cwd?: string | URL; env?: NodeJS.ProcessEnv };
372-
}> = [];
373-
374-
const spawnProcess: NotifySpawn = (command, args, options) => {
375-
calls.push({ command, args, options: { cwd: options.cwd, env: options.env } });
376-
377-
return {
378-
once(event, listener) {
379-
if (event === "error" && calls.length === 1) {
380-
listener({ code: "EACCES" } as NodeJS.ErrnoException);
381-
}
382-
return this;
383-
},
384-
unref() {
385-
return undefined;
386-
},
367+
test(
368+
"launchNotifyScript passes DURATION and falls back to /bin/sh for non-executable scripts",
369+
{ skip: process.platform === "win32" },
370+
() => {
371+
const calls: Array<{
372+
command: string;
373+
args: string[];
374+
options: { cwd?: string | URL; env?: NodeJS.ProcessEnv };
375+
}> = [];
376+
377+
const spawnProcess: NotifySpawn = (command, args, options) => {
378+
calls.push({ command, args, options: { cwd: options.cwd, env: options.env } });
379+
380+
return {
381+
once(event, listener) {
382+
if (event === "error" && calls.length === 1) {
383+
listener({ code: "EACCES" } as NodeJS.ErrnoException);
384+
}
385+
return this;
386+
},
387+
unref() {
388+
return undefined;
389+
},
390+
};
387391
};
388-
};
389-
390-
launchNotifyScript("/tmp/notify.sh", 2750, "/tmp/project", spawnProcess, { WEBHOOK: "configured" });
391-
392-
assert.equal(calls.length, 2);
393-
assert.equal(calls[0]?.command, "/tmp/notify.sh");
394-
assert.deepEqual(calls[0]?.args, []);
395-
assert.equal(calls[0]?.options.cwd, "/tmp/project");
396-
assert.equal(calls[0]?.options.env?.DURATION, "2");
397-
assert.equal(calls[0]?.options.env?.WEBHOOK, "configured");
398-
assert.equal(calls[1]?.command, "/bin/sh");
399-
assert.deepEqual(calls[1]?.args, ["/tmp/notify.sh"]);
400-
assert.equal(calls[1]?.options.cwd, "/tmp/project");
401-
assert.equal(calls[1]?.options.env?.DURATION, "2");
402-
});
392+
393+
launchNotifyScript("/tmp/notify.sh", 2750, "/tmp/project", spawnProcess, { WEBHOOK: "configured" });
394+
395+
assert.equal(calls.length, 2);
396+
assert.equal(calls[0]?.command, "/tmp/notify.sh");
397+
assert.deepEqual(calls[0]?.args, []);
398+
assert.equal(calls[0]?.options.cwd, "/tmp/project");
399+
assert.equal(calls[0]?.options.env?.DURATION, "2");
400+
assert.equal(calls[0]?.options.env?.WEBHOOK, "configured");
401+
assert.equal(calls[1]?.command, "/bin/sh");
402+
assert.deepEqual(calls[1]?.args, ["/tmp/notify.sh"]);
403+
assert.equal(calls[1]?.options.cwd, "/tmp/project");
404+
assert.equal(calls[1]?.options.env?.DURATION, "2");
405+
}
406+
);

src/tests/web-search-handler.test.ts

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,40 +20,44 @@ afterEach(() => {
2020
}
2121
});
2222

23-
test("WebSearch executes the configured script with the query as one argument", async () => {
24-
const workspace = createTempWorkspace();
25-
const scriptPath = path.join(workspace, "web-search.sh");
26-
fs.writeFileSync(
27-
scriptPath,
28-
[
29-
"#!/bin/sh",
30-
"printf 'query=%s\\n' \"$1\"",
31-
"printf 'cwd=%s\\n' \"$PWD\"",
32-
"printf 'webhook=%s\\n' \"$WEBHOOK\"",
33-
].join("\n"),
34-
"utf8"
35-
);
36-
fs.chmodSync(scriptPath, 0o755);
23+
test(
24+
"WebSearch executes the configured script with the query as one argument",
25+
{ skip: process.platform === "win32" },
26+
async () => {
27+
const workspace = createTempWorkspace();
28+
const scriptPath = path.join(workspace, "web-search.sh");
29+
fs.writeFileSync(
30+
scriptPath,
31+
[
32+
"#!/bin/sh",
33+
"printf 'query=%s\\n' \"$1\"",
34+
"printf 'cwd=%s\\n' \"$PWD\"",
35+
"printf 'webhook=%s\\n' \"$WEBHOOK\"",
36+
].join("\n"),
37+
"utf8"
38+
);
39+
fs.chmodSync(scriptPath, 0o755);
3740

38-
const starts: Array<{ id: string | number; command: string }> = [];
39-
const exits: Array<string | number> = [];
40-
const result = await handleWebSearchTool(
41-
{ query: "latest node release" },
42-
createContext(workspace, {
43-
webSearchTool: scriptPath,
44-
env: { WEBHOOK: "configured" },
45-
onProcessStart: (id, command) => starts.push({ id, command }),
46-
onProcessExit: (id) => exits.push(id),
47-
})
48-
);
49-
const realWorkspace = fs.realpathSync(workspace);
41+
const starts: Array<{ id: string | number; command: string }> = [];
42+
const exits: Array<string | number> = [];
43+
const result = await handleWebSearchTool(
44+
{ query: "latest node release" },
45+
createContext(workspace, {
46+
webSearchTool: scriptPath,
47+
env: { WEBHOOK: "configured" },
48+
onProcessStart: (id, command) => starts.push({ id, command }),
49+
onProcessExit: (id) => exits.push(id),
50+
})
51+
);
52+
const realWorkspace = fs.realpathSync(workspace);
5053

51-
assert.equal(result.ok, true);
52-
assert.equal(result.output, `query=latest node release\ncwd=${realWorkspace}\nwebhook=configured\n`);
53-
assert.equal(starts.length, 1);
54-
assert.match(starts[0].command, /^WebSearch: latest node release$/);
55-
assert.deepEqual(exits, [starts[0].id]);
56-
});
54+
assert.equal(result.ok, true);
55+
assert.equal(result.output, `query=latest node release\ncwd=${realWorkspace}\nwebhook=configured\n`);
56+
assert.equal(starts.length, 1);
57+
assert.match(starts[0].command, /^WebSearch: latest node release$/);
58+
assert.deepEqual(exits, [starts[0].id]);
59+
}
60+
);
5761

5862
test("WebSearch uses the default API when no script is configured", async () => {
5963
const workspace = createTempWorkspace();

src/tests/welcomeScreen.test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
import { test } from "node:test";
22
import assert from "node:assert/strict";
3+
import * as os from "os";
4+
import * as path from "path";
35
import { buildWelcomeTips, formatHomeRelativePath } from "../ui";
46

57
test("formatHomeRelativePath returns tilde for the home directory", () => {
6-
assert.equal(formatHomeRelativePath("/Users/example", "/Users/example"), "~");
8+
const home = path.resolve("/Users/example");
9+
assert.equal(formatHomeRelativePath(home, home), "~");
710
});
811

912
test("formatHomeRelativePath shortens paths inside the home directory", () => {
10-
assert.equal(formatHomeRelativePath("/Users/example/dev/project", "/Users/example"), "~/dev/project");
13+
const home = path.resolve("/Users/example");
14+
const result = formatHomeRelativePath(path.resolve("/Users/example/dev/project"), home);
15+
assert.equal(result, `~${path.sep}dev${path.sep}project`);
1116
});
1217

1318
test("formatHomeRelativePath keeps paths outside the home directory absolute", () => {
14-
assert.equal(formatHomeRelativePath("/tmp/project", "/Users/example"), "/tmp/project");
19+
const home = path.resolve("/Users/example");
20+
const other = path.resolve("/tmp/project");
21+
// The result should be the absolute path since it's outside home
22+
const result = formatHomeRelativePath(other, home);
23+
assert.equal(result, other);
1524
});
1625

1726
test("buildWelcomeTips includes built-in slash commands and loaded skills", () => {

0 commit comments

Comments
 (0)