summaryrefslogtreecommitdiff
path: root/internal/tui/eventstream/export_test.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-13 10:23:00 +0300
committerPaul Buetow <paul@buetow.org>2026-05-13 10:23:00 +0300
commitd799f3f04da8067669c90a755e90cd723f9746e7 (patch)
tree02c0c5dda7aaa0d79a230e043297ad822cf2b9f0 /internal/tui/eventstream/export_test.go
parent5856360a696b958a4c5c57cf512c5a04f0cfd66f (diff)
fix: use shell-aware tokenizer in resolveEditorCommand to handle EDITOR paths with spaces
Replace strings.Fields with a new shellSplit function that implements POSIX-like quoting rules (single-quotes, double-quotes, backslash escapes), so EDITOR values such as '/My Editor/hx' or "/path/with spaces/hx" --wait are correctly parsed rather than mangled into multiple path components. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/tui/eventstream/export_test.go')
-rw-r--r--internal/tui/eventstream/export_test.go80
1 files changed, 80 insertions, 0 deletions
diff --git a/internal/tui/eventstream/export_test.go b/internal/tui/eventstream/export_test.go
index 9bdb2d2..6bebe6b 100644
--- a/internal/tui/eventstream/export_test.go
+++ b/internal/tui/eventstream/export_test.go
@@ -3,6 +3,7 @@ package eventstream
import (
"os"
"path/filepath"
+ "reflect"
"testing"
)
@@ -81,3 +82,82 @@ func TestResolveEditorCommandFallsBackToViWhenHxMissing(t *testing.T) {
t.Fatalf("expected vi fallback, got %#v", parts)
}
}
+
+// TestResolveEditorCommandSingleQuotedPath verifies that an EDITOR value with
+// a single-quoted path containing spaces is not broken into multiple tokens.
+func TestResolveEditorCommandSingleQuotedPath(t *testing.T) {
+ t.Setenv("SUDO_EDITOR", "")
+ t.Setenv("VISUAL", "")
+ t.Setenv("EDITOR", "'/My Editor/hx'")
+
+ parts, source, err := resolveEditorCommand()
+ if err != nil {
+ t.Fatalf("resolve editor: %v", err)
+ }
+ if source != "EDITOR" {
+ t.Fatalf("expected EDITOR source, got %q", source)
+ }
+ want := []string{"/My Editor/hx"}
+ if !reflect.DeepEqual(parts, want) {
+ t.Fatalf("expected %#v, got %#v", want, parts)
+ }
+}
+
+// TestResolveEditorCommandDoubleQuotedPathWithArgs verifies that double-quoted
+// paths with trailing arguments are all preserved correctly.
+func TestResolveEditorCommandDoubleQuotedPathWithArgs(t *testing.T) {
+ t.Setenv("SUDO_EDITOR", "")
+ t.Setenv("VISUAL", "")
+ t.Setenv("EDITOR", `"/path/with spaces/hx" --wait`)
+
+ parts, source, err := resolveEditorCommand()
+ if err != nil {
+ t.Fatalf("resolve editor: %v", err)
+ }
+ if source != "EDITOR" {
+ t.Fatalf("expected EDITOR source, got %q", source)
+ }
+ want := []string{"/path/with spaces/hx", "--wait"}
+ if !reflect.DeepEqual(parts, want) {
+ t.Fatalf("expected %#v, got %#v", want, parts)
+ }
+}
+
+// TestShellSplitVariousCases covers the tokenizer with a table-driven approach.
+func TestShellSplitVariousCases(t *testing.T) {
+ cases := []struct {
+ input string
+ want []string
+ }{
+ // Plain tokens — behaviour identical to strings.Fields.
+ {"vi", []string{"vi"}},
+ {"nvim --wait", []string{"nvim", "--wait"}},
+ // Single-quoted path with spaces — the whole quoted span is one token.
+ {"'/path/with spaces/hx'", []string{"/path/with spaces/hx"}},
+ // Double-quoted path.
+ {`"/path/with spaces/hx"`, []string{"/path/with spaces/hx"}},
+ // Double-quoted path with escaped double quote inside.
+ {`"/path/\"hx\""`, []string{`/path/"hx"`}},
+ // Mixed: quoted binary + unquoted flag.
+ {`"/My Editor/hx" --wait`, []string{"/My Editor/hx", "--wait"}},
+ // Backslash escaping a space outside quotes.
+ {`/path/with\ spaces/hx`, []string{"/path/with spaces/hx"}},
+ // Empty string returns no tokens.
+ {"", nil},
+ // Whitespace-only returns no tokens.
+ {" ", nil},
+ // Unterminated single quote: treated as implicit close at end.
+ {"'unterminated", []string{"unterminated"}},
+ }
+
+ for _, tc := range cases {
+ got := shellSplit(tc.input)
+ // Treat nil and empty slice as equivalent for comparison purposes.
+ if len(got) == 0 && len(tc.want) == 0 {
+ continue
+ }
+ if !reflect.DeepEqual(got, tc.want) {
+ t.Errorf("shellSplit(%q): got %#v, want %#v", tc.input, got, tc.want)
+ }
+ }
+}