Fix Cursor runtime probes to execute Windows cmd and bat shims reliably while preserving diagnostics. - Run shared Cursor CLI probe and discovery spawns through the shell only on Windows. - Surface bounded spawn error details in unavailable Cursor probe reasons. - Cover Windows shell spawning, failure diagnostics, probe, and process-manager behavior with tests. - Document the Windows PATH shim invocation contract and add a patch changeset. Files changed: .changeset/fn-7418-cursor-cli-windows-cmd.md | 7 ++ docs/cursor-cli-contract.md | 17 ++++ .../src/__tests__/cli-spawn.test.ts | 103 +++++++++++++++++++++ .../src/__tests__/probe.test.ts | 44 ++++++++- .../src/__tests__/process-manager.test.ts | 24 ++++- .../fusion-plugin-cursor-runtime/src/cli-spawn.ts | 41 ++++++-- plugins/fusion-plugin-cursor-runtime/src/probe.ts | 14 ++- 7 files changed, 236 insertions(+), 14 deletions(-) Fusion-Task-Id: FN-7418 Fusion-Task-Lineage: d689bea7-4676-4190-a60a-f85efab90aba Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
import { EventEmitter } from "node:events";
|
|
import { PassThrough } from "node:stream";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("node:child_process", () => ({ spawn: vi.fn() }));
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { runCursorCommand } from "../cli-spawn.js";
|
|
|
|
function mockPlatform(platform: NodeJS.Platform) {
|
|
return vi.spyOn(process, "platform", "get").mockReturnValue(platform);
|
|
}
|
|
|
|
function createMockChild() {
|
|
const child = new EventEmitter() as EventEmitter & {
|
|
stdout: PassThrough;
|
|
stderr: PassThrough;
|
|
kill: ReturnType<typeof vi.fn>;
|
|
};
|
|
child.stdout = new PassThrough();
|
|
child.stderr = new PassThrough();
|
|
child.kill = vi.fn();
|
|
vi.mocked(spawn).mockReturnValue(child as never);
|
|
return child;
|
|
}
|
|
|
|
describe("runCursorCommand", () => {
|
|
beforeEach(() => {
|
|
vi.useRealTimers();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("uses the Windows shell so PATH .cmd and .bat Cursor shims can run", async () => {
|
|
mockPlatform("win32");
|
|
const child = createMockChild();
|
|
|
|
const resultPromise = runCursorCommand("cursor-agent", ["--version"], 1000);
|
|
|
|
expect(spawn).toHaveBeenCalledWith("cursor-agent", ["--version"], {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
shell: true,
|
|
});
|
|
|
|
child.stdout.write("cursor-agent 1.2.3\n");
|
|
child.stderr.write("diagnostic\n");
|
|
child.emit("close", 0);
|
|
|
|
await expect(resultPromise).resolves.toEqual({
|
|
code: 0,
|
|
stdout: "cursor-agent 1.2.3\n",
|
|
stderr: "diagnostic\n",
|
|
});
|
|
});
|
|
|
|
it("keeps non-Windows Cursor invocations on direct spawn", async () => {
|
|
mockPlatform("darwin");
|
|
const child = createMockChild();
|
|
|
|
const resultPromise = runCursorCommand("cursor-agent", ["--version"], 1000);
|
|
|
|
expect(spawn).toHaveBeenCalledWith("cursor-agent", ["--version"], {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
shell: false,
|
|
});
|
|
|
|
child.emit("close", 0);
|
|
await expect(resultPromise).resolves.toMatchObject({ code: 0 });
|
|
});
|
|
|
|
it("returns spawn errors with diagnostics instead of empty stderr", async () => {
|
|
mockPlatform("win32");
|
|
const child = createMockChild();
|
|
|
|
const resultPromise = runCursorCommand("cursor-agent", ["--version"], 1000);
|
|
child.emit("error", Object.assign(new Error("spawn cursor-agent ENOENT"), { code: "ENOENT" }));
|
|
|
|
const result = await resultPromise;
|
|
expect(result.code).toBe(127);
|
|
expect(result.stderr).toContain("spawn error: ENOENT: spawn cursor-agent ENOENT");
|
|
});
|
|
|
|
it("kills timed-out Cursor commands best-effort and resolves once", async () => {
|
|
vi.useFakeTimers();
|
|
mockPlatform("linux");
|
|
const child = createMockChild();
|
|
|
|
const resultPromise = runCursorCommand("cursor-agent", ["models", "--json"], 25);
|
|
child.stdout.write("partial");
|
|
|
|
await vi.advanceTimersByTimeAsync(25);
|
|
|
|
await expect(resultPromise).resolves.toEqual({ code: 124, stdout: "partial", stderr: "" });
|
|
expect(child.kill).toHaveBeenCalledWith("SIGKILL");
|
|
|
|
child.emit("close", 0);
|
|
await expect(resultPromise).resolves.toMatchObject({ code: 124 });
|
|
});
|
|
});
|