Files
multica/server/pkg/agent/cursor_execute_unix_test.go
T
88e9170dc1 refactor(redact): drop home-directory masking from Text (#7772)
redact.Text rewrote the local home directory to /Users/****/ on its way
to the database and the WebSocket broadcast. Removing it.

It was never an access-control boundary. Anyone who can read a task
transcript already sees repository paths, file contents, commands, diffs
and error output; masking one path segment out of all that protected
nothing, while it did make paths non-copy-pasteable and harder to line up
when debugging a run.

It was also incoherent about whose home directory it hid. homeDir and
username were resolved from the process running Text, and the main
transcript fields (Content / Output) are only redacted in the server's
ingest handler -- so a hosted deployment matched them against the
*server's* home directory, never the machine actually running the agent.
Only tool inputs and stderr diagnostics went through the daemon-side call
where the resolved home was the right one.

Transcript visibility is an authorization concern and belongs at that
layer. Secret redaction (API keys, tokens, PEM blocks, connection
strings) is a real boundary and is untouched.

Two tests asserted the removed behaviour and now assert its absence, so
nobody reintroduces host-path masking as a side effect of another fix:
redact.TestTextLeavesHomePathIntact (replacing TestRedactHomeDirectory
and TestInputMapRedactsNestedHomePath) and the home-path arm of
agent.TestCursorExecuteReportsSanitizedStderrOnProcessFailure, which
keeps its bearer-token assertion.

Dropping the branch also resolves the Windows failure in
TestRedactHomeDirectory reported in #7744: Windows can return a
DOMAIN\user account name that is not a substring of the home path, so the
strings.Replace never fired. CI only runs Go tests on ubuntu, which is
why main stayed green.

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-31 13:39:39 +08:00

296 lines
9.3 KiB
Go

//go:build unix
package agent
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// A real cursor-agent reads the prompt from stdin to EOF (see buildCursorArgs).
// A fake that exits without reading it leaves the prompt write racing the
// child's exit: win and the ~64 KiB pipe buffer swallows it, lose and the read
// end is already closed and the write fails with EPIPE.
//
// That matters because writeErr outranks both `exitErr` and the generic
// "stream ended without terminal result" when finalizing the error (cursor.go),
// so a lost race replaces the failure the test is asserting on with
// "cursor-agent prompt write failed: broken pipe" — the flake that turned main
// red on 2026-07-30 (MUL-5536).
//
// Draining stdin first makes the fake honour the same contract as the real CLI,
// which removes the race instead of papering over it. Only fixtures whose
// expected error ranks below writeErr need it; the scanner-overflow and
// structured-stream-error cases rank above it and are unaffected.
const drainStdin = "cat > /dev/null"
func TestCursorExecuteStopsAfterTerminalResult(t *testing.T) {
t.Parallel()
script := `#!/bin/sh
printf '%s\n' '{"type":"system","subtype":"init","session_id":"sess-terminal"}'
printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"done","session_id":"sess-terminal"}'
sleep 10
`
result := executeFakeCursor(t, script)
if result.Status != "completed" {
t.Fatalf("status = %q, want completed; error=%q", result.Status, result.Error)
}
if result.Output != "done" {
t.Fatalf("output = %q, want done", result.Output)
}
if result.SessionID != "sess-terminal" {
t.Fatalf("session id = %q, want sess-terminal", result.SessionID)
}
}
func TestCursorExecuteEmitsTerminalResultText(t *testing.T) {
t.Parallel()
script := `#!/bin/sh
printf '%s\n' '{"type":"system","subtype":"init","session_id":"sess-result-text"}'
printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"final-only answer","session_id":"sess-result-text"}'
`
fakePath := filepath.Join(t.TempDir(), "cursor-agent")
writeTestExecutable(t, fakePath, []byte(script))
backend, err := New("cursor", Config{ExecutablePath: fakePath, Logger: slog.Default()})
if err != nil {
t.Fatalf("New(cursor): %v", err)
}
session, err := backend.Execute(t.Context(), "hello", ExecOptions{Timeout: 5 * time.Second})
if err != nil {
t.Fatalf("Execute: %v", err)
}
var messages []Message
done := make(chan struct{})
go func() {
defer close(done)
for msg := range session.Messages {
messages = append(messages, msg)
}
}()
result := <-session.Result
<-done
if result.Status != "completed" {
t.Fatalf("status = %q, want completed; error=%q", result.Status, result.Error)
}
if result.Output != "final-only answer" {
t.Fatalf("output = %q, want final-only answer", result.Output)
}
for _, msg := range messages {
if msg.Type == MessageText && msg.Content == "final-only answer" {
return
}
}
t.Fatalf("expected terminal result text in message stream, got %+v", messages)
}
func TestCursorExecuteStopsAfterTerminalErrorResult(t *testing.T) {
t.Parallel()
script := `#!/bin/sh
printf '%s\n' '{"type":"system","subtype":"init","session_id":"sess-terminal-error"}'
printf '%s\n' '{"type":"result","subtype":"error","is_error":true,"result":"failed hard","session_id":"sess-terminal-error"}'
sleep 10
`
result := executeFakeCursor(t, script)
if result.Status != "failed" {
t.Fatalf("status = %q, want failed; error=%q", result.Status, result.Error)
}
if result.Error != "failed hard" {
t.Fatalf("error = %q, want failed hard", result.Error)
}
if result.Output != "" {
t.Fatalf("output = %q, want empty failed output", result.Output)
}
if result.SessionID != "sess-terminal-error" {
t.Fatalf("session id = %q, want sess-terminal-error", result.SessionID)
}
}
func TestCursorExecuteReportsSanitizedStderrOnProcessFailure(t *testing.T) {
homeDir, err := os.UserHomeDir()
if err != nil {
t.Fatalf("UserHomeDir: %v", err)
}
script := `#!/bin/sh
` + drainStdin + `
dd if=/dev/zero bs=4096 count=1 2>/dev/null | tr '\000' x >&2
printf '\nAuthorization: Bearer cursor-secret-token-value\npath=%s/private\n' "$HOME" >&2
exit 1
`
result := executeFakeCursor(t, script)
if result.Status != "failed" {
t.Fatalf("status = %q, want failed; error=%q", result.Status, result.Error)
}
for _, want := range []string{
"cursor-agent exited with error: exit status 1",
"result_seen=false",
"exit_code=1",
"cursor stderr:",
"Authorization: [REDACTED]",
"actions completed before finalization may already have taken effect",
} {
if !strings.Contains(result.Error, want) {
t.Errorf("error = %q, want substring %q", result.Error, want)
}
}
if strings.Contains(result.Error, "cursor-secret-token-value") {
t.Errorf("error leaked the bearer token: %q", result.Error)
}
// Host paths are deliberately NOT masked: a crash diagnostic is only
// actionable if the path it names is the real one, and masking a path
// segment never was an access-control boundary. See redact.Text.
if !strings.Contains(result.Error, homeDir+"/private") {
t.Errorf("error = %q, want the home path preserved verbatim", result.Error)
}
if result.Output != "" {
t.Fatalf("output = %q, want empty failed output", result.Output)
}
if len(result.Error) > agentStderrTailBytes+1024 {
t.Fatalf("error length = %d, want bounded stderr diagnostic", len(result.Error))
}
}
func TestCursorExecuteReportsMalformedTerminalEvent(t *testing.T) {
script := `#!/bin/sh
printf '%s\n' '{"type":"system","subtype":"init","session_id":"sess-malformed"}'
printf '%s\n' '{"type":"result","subtype":"success","result":"truncated"'
exit 1
`
result := executeFakeCursor(t, script)
if result.Status != "failed" {
t.Fatalf("status = %q, want failed; error=%q", result.Status, result.Error)
}
for _, want := range []string{
"result_seen=false",
"invalid_event_count=1",
"last_event_type=system",
} {
if !strings.Contains(result.Error, want) {
t.Errorf("error = %q, want substring %q", result.Error, want)
}
}
if result.Output != "" {
t.Fatalf("output = %q, want empty failed output", result.Output)
}
}
func TestCursorExecuteReportsScannerOverflow(t *testing.T) {
// The oversized event is sized from agentStreamMaxLineBytes so raising
// the shared cap cannot silently turn this into a plain oversized-line
// pass that never reaches the overflow branch.
script := fmt.Sprintf(`#!/bin/sh
printf '%%s\n' '{"type":"system","subtype":"init","session_id":"sess-overflow"}'
dd if=/dev/zero bs=1048576 count=%d 2>/dev/null | tr '\000' x
printf '\n'
`, agentStreamMaxLineBytes/(1024*1024)+1)
result := executeFakeCursor(t, script)
if result.Status != "failed" {
t.Fatalf("status = %q, want failed; error=%q", result.Status, result.Error)
}
for _, want := range []string{
"cursor-agent stdout read error",
"token too long",
"result_seen=false",
"scanner_error=true",
"last_event_type=system",
} {
if !strings.Contains(result.Error, want) {
t.Errorf("error = %q, want substring %q", result.Error, want)
}
}
if result.Output != "" {
t.Fatalf("output = %q, want empty failed output", result.Output)
}
}
func TestCursorExecuteFailsOnCleanEOFWithoutResult(t *testing.T) {
script := `#!/bin/sh
` + drainStdin + `
printf '%s\n' '{"type":"system","subtype":"init","session_id":"sess-no-result"}'
printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"text","text":"partial answer"}]}}'
`
result := executeFakeCursor(t, script)
if result.Status != "failed" {
t.Fatalf("status = %q, want failed; error=%q", result.Status, result.Error)
}
for _, want := range []string{
"cursor-agent stream ended without terminal result",
"result_seen=false",
"exit_code=0",
"last_event_type=assistant",
} {
if !strings.Contains(result.Error, want) {
t.Errorf("error = %q, want substring %q", result.Error, want)
}
}
if result.Output != "" {
t.Fatalf("output = %q, want partial transcript suppressed", result.Output)
}
}
func TestCursorExecutePreservesStructuredStreamError(t *testing.T) {
script := `#!/bin/sh
printf '%s\n' '{"type":"system","subtype":"init","session_id":"sess-stream-error"}'
printf '%s\n' '{"type":"error","error":"provider rejected request"}'
exit 1
`
result := executeFakeCursor(t, script)
if result.Status != "failed" {
t.Fatalf("status = %q, want failed; error=%q", result.Status, result.Error)
}
for _, want := range []string{
"provider rejected request",
"result_seen=false",
"exit_code=1",
"last_event_type=error",
} {
if !strings.Contains(result.Error, want) {
t.Errorf("error = %q, want substring %q", result.Error, want)
}
}
if result.Output != "" {
t.Fatalf("output = %q, want empty failed output", result.Output)
}
}
func executeFakeCursor(t *testing.T, script string) Result {
t.Helper()
fakePath := filepath.Join(t.TempDir(), "cursor-agent")
writeTestExecutable(t, fakePath, []byte(script))
backend, err := New("cursor", Config{ExecutablePath: fakePath, Logger: slog.Default()})
if err != nil {
t.Fatalf("New(cursor): %v", err)
}
session, err := backend.Execute(t.Context(), "hello", ExecOptions{Timeout: 5 * time.Second})
if err != nil {
t.Fatalf("Execute: %v", err)
}
result := <-session.Result
if result.Status == "timeout" {
t.Fatalf("cursor backend timed out instead of stopping after terminal result; error=%q", result.Error)
}
return result
}