Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
188 lines
6.1 KiB
Go
188 lines
6.1 KiB
Go
package runtimeprep
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCursorBackgroundTaskGuardAllowsForegroundAndDeniesBackground(t *testing.T) {
|
|
nodePath, err := exec.LookPath("node")
|
|
if err != nil {
|
|
t.Skip("node is required to execute the generated Cursor hook")
|
|
}
|
|
|
|
pluginDir := t.TempDir()
|
|
if err := installCursorBackgroundTaskGuard(filepath.Join(pluginDir, "hooks")); err != nil {
|
|
t.Fatalf("installCursorBackgroundTaskGuard() error = %v", err)
|
|
}
|
|
hookPath := filepath.Join(pluginDir, "hooks", "guard-background-task.mjs")
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantDenied bool
|
|
}{
|
|
{
|
|
name: "foreground omitted",
|
|
input: `{"tool_name":"Task","tool_input":{"prompt":"run_in_background=true is mentioned only as text"}}`,
|
|
wantDenied: false,
|
|
},
|
|
{
|
|
name: "foreground false",
|
|
input: `{"tool_name":"Task","tool_input":{"run_in_background":false}}`,
|
|
wantDenied: false,
|
|
},
|
|
{
|
|
name: "background snake case",
|
|
input: `{"tool_name":"Task","tool_input":{"run_in_background":true}}`,
|
|
wantDenied: true,
|
|
},
|
|
{
|
|
name: "background camel case",
|
|
input: `{"toolName":"Task","toolInput":{"runInBackground":true}}`,
|
|
wantDenied: true,
|
|
},
|
|
{
|
|
name: "invalid payload fails closed",
|
|
input: `{`,
|
|
wantDenied: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
command := exec.Command(nodePath, hookPath)
|
|
command.Stdin = strings.NewReader(tt.input)
|
|
output, err := command.Output()
|
|
if err != nil {
|
|
t.Fatalf("generated hook error = %v", err)
|
|
}
|
|
var response struct {
|
|
Permission string `json:"permission"`
|
|
UserMessage string `json:"user_message"`
|
|
}
|
|
if err := json.Unmarshal(output, &response); err != nil {
|
|
t.Fatalf("generated hook output = %q: %v", output, err)
|
|
}
|
|
if tt.wantDenied {
|
|
if response.Permission != "deny" || response.UserMessage != cursorBackgroundTaskGuardDeniedMessage {
|
|
t.Fatalf("generated hook response = %#v, want background denial", response)
|
|
}
|
|
} else if response.Permission != "" || response.UserMessage != "" {
|
|
t.Fatalf("generated hook response = %#v, want empty allow response", response)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCursorPromptPolicyUsesRelativeSearchRootsOnWindows(t *testing.T) {
|
|
base := "base runtime policy"
|
|
windowsPolicy := cursorPromptPolicyForGOOS(base, "windows")
|
|
for _, expected := range []string{
|
|
"## Windows file-search boundary",
|
|
"drive-qualified absolute Windows paths",
|
|
"workspace-relative path",
|
|
"native terminal",
|
|
"retry through the relative-path or native-terminal route",
|
|
} {
|
|
if !strings.Contains(windowsPolicy, expected) {
|
|
t.Fatalf("Windows Cursor policy missing %q: %s", expected, windowsPolicy)
|
|
}
|
|
}
|
|
if got := cursorPromptPolicyForGOOS(base, "linux"); got != base {
|
|
t.Fatalf("non-Windows Cursor policy = %q, want unchanged base policy", got)
|
|
}
|
|
}
|
|
|
|
func TestCursorBackgroundTaskGuardLauncherUsesCursorBundledNode(t *testing.T) {
|
|
nodePath, err := exec.LookPath("node")
|
|
if err != nil {
|
|
t.Skip("node is required to execute the generated Cursor hook")
|
|
}
|
|
|
|
pluginDir := t.TempDir()
|
|
if err := installCursorBackgroundTaskGuard(filepath.Join(pluginDir, "hooks")); err != nil {
|
|
t.Fatalf("installCursorBackgroundTaskGuard() error = %v", err)
|
|
}
|
|
installDir := filepath.Join(t.TempDir(), "cursor-version")
|
|
binDir := filepath.Join(t.TempDir(), "bin")
|
|
if err := os.MkdirAll(installDir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(binDir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(nodePath, filepath.Join(installDir, "node")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cursorPath := filepath.Join(installDir, "cursor-agent")
|
|
if err := os.WriteFile(cursorPath, []byte("#!/usr/bin/env bash\n"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(cursorPath, filepath.Join(binDir, "cursor-agent")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
launcherPath := filepath.Join(pluginDir, "hooks", "guard-background-task.sh")
|
|
for _, tt := range []struct {
|
|
name string
|
|
input string
|
|
wantDenied bool
|
|
}{
|
|
{name: "foreground", input: `{"tool_name":"Task","tool_input":{"run_in_background":false}}`},
|
|
{name: "background", input: `{"tool_name":"Task","tool_input":{"run_in_background":true}}`, wantDenied: true},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
command := exec.Command(launcherPath)
|
|
command.Stdin = strings.NewReader(tt.input)
|
|
command.Env = append(os.Environ(),
|
|
"CURSOR_INVOKED_AS=cursor-agent",
|
|
"PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"),
|
|
)
|
|
output, err := command.Output()
|
|
if err != nil {
|
|
t.Fatalf("generated launcher error = %v", err)
|
|
}
|
|
denied := strings.Contains(string(output), `"permission":"deny"`)
|
|
if denied != tt.wantDenied {
|
|
t.Fatalf("generated launcher output = %q, denied = %v want %v", output, denied, tt.wantDenied)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCursorBackgroundTaskGuardLauncherFailsClosedWithoutCursorNode(t *testing.T) {
|
|
bashPath, err := exec.LookPath("bash")
|
|
if err != nil {
|
|
t.Skip("bash is required to execute the generated Cursor hook launcher")
|
|
}
|
|
|
|
pluginDir := t.TempDir()
|
|
if err := installCursorBackgroundTaskGuard(filepath.Join(pluginDir, "hooks")); err != nil {
|
|
t.Fatalf("installCursorBackgroundTaskGuard() error = %v", err)
|
|
}
|
|
binDir := t.TempDir()
|
|
if err := os.Symlink(bashPath, filepath.Join(binDir, "bash")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
launcherPath := filepath.Join(pluginDir, "hooks", "guard-background-task.sh")
|
|
command := exec.Command(launcherPath)
|
|
command.Stdin = strings.NewReader(`{"tool_name":"Task","tool_input":{"run_in_background":false}}`)
|
|
command.Env = append(os.Environ(),
|
|
"CURSOR_INVOKED_AS=missing-cursor-agent",
|
|
"PATH="+strings.Join([]string{binDir, "/usr/bin", "/bin"}, string(os.PathListSeparator)),
|
|
)
|
|
output, err := command.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("generated launcher error = %v output = %q", err, output)
|
|
}
|
|
if !strings.Contains(string(output), `"permission":"deny"`) ||
|
|
!strings.Contains(string(output), cursorBackgroundTaskGuardDeniedMessage) {
|
|
t.Fatalf("generated launcher output = %q, want fail-closed denial", output)
|
|
}
|
|
}
|