Files
fusion/plugins/fusion-plugin-cursor-runtime/src/__tests__/cli-spawn.test.ts
gsxdsm b6efd89ae5 FN-9097: add Cursor CLI execution runtime
Add a supervised Cursor CLI runtime for routed Cursor model sessions.

- Stream Cursor prompt output into Fusion session callbacks with resume support.
- Support Windows Cursor shims, inactivity-based supervision, and cursor-agent to cursor fallback execution.
- Route Cursor CLI models through the bundled plugin and document the runtime contract.

Files changed:
 .changeset/fn-9097-cursor-cli-runtime.md           |   7 ++
 docs/cursor-cli-contract.md                        |  21 ++++
 docs/settings-reference.md                         |   4 +-
 .../cli-runtime-routing-conformance.test.ts        |  11 +-
 .../engine/src/agents/agent-session-helpers.ts     |   6 +-
 packages/engine/src/agents/cli-provider-routing.ts |  27 ++---
 plugins/fusion-plugin-cursor-runtime/README.md     |  21 ++++
 plugins/fusion-plugin-cursor-runtime/package.json  |   1 +
 .../src/__tests__/cli-spawn.test.ts                |  25 ++++-
 .../src/__tests__/prompt-transport.test.ts         |  58 ++++++++++
 .../src/__tests__/runtime-adapter.test.ts          |  44 +++++---
 .../src/__tests__/stream-parser.test.ts            |  12 +++
 .../fusion-plugin-cursor-runtime/src/cli-spawn.ts  |  57 +++++++++-
 plugins/fusion-plugin-cursor-runtime/src/index.ts  |   4 +-
 .../src/prompt-transport.ts                        | 120 +++++++++++++++++++++
 .../src/runtime-adapter.ts                         |  59 ++++++----
 .../src/stream-parser.ts                           |  43 ++++++++
 plugins/fusion-plugin-cursor-runtime/src/types.ts  |  18 ++--
 pnpm-lock.yaml                                     |   3 +
 19 files changed, 458 insertions(+), 83 deletions(-)

Fusion-Task-Id: FN-9097

Fusion-Task-Lineage: a4eed861-6059-4528-9f80-3426f5ccad58

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-15 08:51:35 -07:00

123 lines
4.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(), spawnSync: vi.fn() }));
import { spawn, spawnSync } from "node:child_process";
import { assertCmdBoundarySafe, quoteCmdArgument, resolvePowerShellExecutable, 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 });
});
});
describe("cmd boundary validation", () => {
it("falls back to Windows PowerShell when pwsh is not resolvable", () => {
vi.mocked(spawnSync).mockReturnValue({ status: 1, stdout: "" } as never);
expect(resolvePowerShellExecutable()).toBe("powershell.exe");
vi.mocked(spawnSync).mockReturnValue({ status: 0, stdout: "C:\\Program Files\\PowerShell\\7\\pwsh.exe\n" } as never);
expect(resolvePowerShellExecutable()).toBe("pwsh.exe");
});
it("allows Cursor bracketed parameterized model identifiers", () => {
const model = "claude-opus-4-8[context=1m,effort=high]";
expect(() => assertCmdBoundarySafe(model)).not.toThrow();
expect(quoteCmdArgument(model)).toBe(`"${model}"`);
});
it("rejects cmd control characters rather than escaping them", () => {
expect(() => assertCmdBoundarySafe("model&payload")).toThrow(/&/);
});
});