mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
On Windows the official cursor-agent installer ships cursor-agent.cmd whose body is `powershell ... -File cursor-agent.ps1 %*`. CreateProcess for a .cmd file goes through cmd.exe, and `%*` in a batch file is expanded by re-tokenising the original command line, which mangles arguments containing newlines or other whitespace - most notably a long, multi-line `-p <prompt>`. The agent then only sees a truncated prompt and fails with "Workspace Trust Required" or exits 1 immediately. When LookPath resolves cursor-agent to a .cmd/.bat launcher and a sibling cursor-agent.ps1 exists, invoke PowerShell directly with `-File <ps1>` so Go's os/exec passes each argv as a discrete token. This is exactly what the .cmd does internally; we just skip the cmd.exe re-tokenisation step. PowerShell host resolution prefers pwsh.exe (PS 7) on PATH, then powershell.exe on PATH, and finally falls back to %SystemRoot%\System32\WindowsPowerShell\v1.0. Platform-specific code is split via build tags (cursor_invocation_windows.go / cursor_invocation_other.go) so non-Windows builds carry no Windows-only dependencies. The lookup is exposed as a package variable to make the Windows path fully unit-testable without spawning real PowerShell. Five unit tests cover: passthrough on non-launcher targets, successful rewrite with a multi-line prompt, .exe direct launch (skip), missing .ps1 (skip), and missing PowerShell host (skip). The change leaves macOS / Linux behaviour entirely untouched and stays on the official cursor-agent launch chain - no node.exe direct invocation, no prompt mutation, no extra flags. Closes #1297 Made-with: Cursor
126 lines
4.3 KiB
Go
126 lines
4.3 KiB
Go
//go:build windows
|
|
|
|
package agent
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
// stubPowerShell installs a deterministic PowerShell lookup for the duration
|
|
// of a test and restores the original on cleanup.
|
|
func stubPowerShell(t *testing.T, path string, ok bool) {
|
|
t.Helper()
|
|
prev := powerShellLookup
|
|
powerShellLookup = func() (string, bool) { return path, ok }
|
|
t.Cleanup(func() { powerShellLookup = prev })
|
|
}
|
|
|
|
func writeFile(t *testing.T, path, body string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
|
t.Fatalf("write %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
// TestPlatformCursorInvocation_RewritesCmdLauncherToPowerShellFile is the core
|
|
// Windows test: when LookPath resolves cursor-agent to the official .cmd
|
|
// launcher and a sibling cursor-agent.ps1 exists, we should invoke
|
|
// PowerShell with -File <ps1> and forward every original arg unchanged
|
|
// (including a multi-line -p prompt that would otherwise be mangled by the
|
|
// cmd.exe %* re-expansion in the .cmd launcher).
|
|
func TestPlatformCursorInvocation_RewritesCmdLauncherToPowerShellFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cmdPath := filepath.Join(dir, "cursor-agent.cmd")
|
|
ps1Path := filepath.Join(dir, "cursor-agent.ps1")
|
|
writeFile(t, cmdPath, "@echo off\r\npowershell -NoProfile -ExecutionPolicy Bypass -File \"%~dp0cursor-agent.ps1\" %*\r\n")
|
|
writeFile(t, ps1Path, "# fake cursor-agent.ps1\r\n")
|
|
|
|
fakePS := filepath.Join(dir, "powershell.exe")
|
|
writeFile(t, fakePS, "")
|
|
stubPowerShell(t, fakePS, true)
|
|
|
|
args := []string{
|
|
"chat",
|
|
"-p", "line1\nline2\nline3",
|
|
"--output-format", "stream-json",
|
|
"--yolo",
|
|
"--workspace", `C:\some\workspace`,
|
|
}
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
|
|
gotExec, gotArgs, ok := platformCursorInvocation(cmdPath, args, logger)
|
|
if !ok {
|
|
t.Fatalf("expected platform rewrite to be applied, got ok=false")
|
|
}
|
|
if gotExec != fakePS {
|
|
t.Errorf("argv0: got %q want %q", gotExec, fakePS)
|
|
}
|
|
|
|
wantArgs := append([]string{
|
|
"-NoProfile",
|
|
"-ExecutionPolicy", "Bypass",
|
|
"-File", ps1Path,
|
|
}, args...)
|
|
if !reflect.DeepEqual(gotArgs, wantArgs) {
|
|
t.Errorf("argv mismatch:\n got %#v\n want %#v", gotArgs, wantArgs)
|
|
}
|
|
}
|
|
|
|
// TestPlatformCursorInvocation_SkipsWhenNotCmdOrBat ensures we leave argv
|
|
// alone when the user explicitly resolved cursor-agent to something that
|
|
// isn't a batch launcher (e.g. a real binary or a node script).
|
|
func TestPlatformCursorInvocation_SkipsWhenNotCmdOrBat(t *testing.T) {
|
|
dir := t.TempDir()
|
|
exePath := filepath.Join(dir, "cursor-agent.exe")
|
|
writeFile(t, exePath, "")
|
|
// A sibling .ps1 must not trick us into rewriting a non-launcher exec.
|
|
writeFile(t, filepath.Join(dir, "cursor-agent.ps1"), "")
|
|
|
|
stubPowerShell(t, filepath.Join(dir, "powershell.exe"), true)
|
|
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
if _, _, ok := platformCursorInvocation(exePath, []string{"chat"}, logger); ok {
|
|
t.Fatalf("expected ok=false for non-.cmd/.bat launcher")
|
|
}
|
|
}
|
|
|
|
// TestPlatformCursorInvocation_SkipsWhenPS1Missing covers the rare case where
|
|
// a .cmd was found but its companion .ps1 is missing (e.g. a partial install).
|
|
// We must fall back to the original launcher rather than synthesising an
|
|
// invalid powershell -File invocation.
|
|
func TestPlatformCursorInvocation_SkipsWhenPS1Missing(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cmdPath := filepath.Join(dir, "cursor-agent.cmd")
|
|
writeFile(t, cmdPath, "@echo off\r\n")
|
|
|
|
stubPowerShell(t, filepath.Join(dir, "powershell.exe"), true)
|
|
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
if _, _, ok := platformCursorInvocation(cmdPath, []string{"chat"}, logger); ok {
|
|
t.Fatalf("expected ok=false when cursor-agent.ps1 is missing")
|
|
}
|
|
}
|
|
|
|
// TestPlatformCursorInvocation_SkipsWhenPowerShellMissing covers a stripped
|
|
// down environment in which neither pwsh.exe nor powershell.exe can be
|
|
// resolved. We must not fabricate an empty-string argv[0].
|
|
func TestPlatformCursorInvocation_SkipsWhenPowerShellMissing(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cmdPath := filepath.Join(dir, "cursor-agent.cmd")
|
|
ps1Path := filepath.Join(dir, "cursor-agent.ps1")
|
|
writeFile(t, cmdPath, "@echo off\r\n")
|
|
writeFile(t, ps1Path, "# fake\r\n")
|
|
|
|
stubPowerShell(t, "", false)
|
|
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
if _, _, ok := platformCursorInvocation(cmdPath, []string{"chat"}, logger); ok {
|
|
t.Fatalf("expected ok=false when no powershell host is available")
|
|
}
|
|
}
|