返回 DeepSeek-Reasonix
unicode_test.go
根目录 / internal / persistentshell / unicode_test.go
1 package persistentshell
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9 "time"
10 )
11
12 // Desktop launches can omit all locale variables. The command must reach
13 // interactive Bash unchanged even when Readline treats high bytes as meta keys.
14 func TestPersistentShellUnicodeTransport(t *testing.T) {
15 skipNonPOSIX(t)
16 sh := posixShell(t)
17 for _, locale := range []string{"unset", "C", "en_US.UTF-8"} {
18 t.Run(locale, func(t *testing.T) {
19 dir := t.TempDir()
20 m := testManager(t)
21 env := []string{"PATH=" + os.Getenv("PATH"), "HOME=" + dir, "TERM=dumb", "INPUTRC=/dev/null"}
22 if locale != "unset" {
23 env = append(env, "LC_ALL="+locale)
24 }
25 req := Request{Argv: InteractiveArgv(sh), Dir: dir, Env: env, Shell: sh, Timeout: 10 * time.Second}
26 run := func(command, want string) {
27 t.Helper()
28 req.Command = command
29 res := m.Run(context.Background(), req)
30 if res.Err != nil || res.ExitCode != 0 || res.Output != want {
31 t.Fatalf("command (%d bytes): err=%v exit=%d output=%q, want %q", len(command), res.Err, res.ExitCode, res.Output, want)
32 }
33 }
34 run("printf '%s' '起点是空白格😀7'", "起点是空白格😀7")
35 // Exercise the incident's grep shape using a disposable Unicode path.
36 if err := os.WriteFile(filepath.Join(dir, "中文 '文件.txt"), []byte("起点是空白格\nother\n"), 0o600); err != nil {
37 t.Fatal(err)
38 }
39 run("grep -n '起点是空白格' "+posixQuote("中文 '文件.txt"), "1:起点是空白格\n")
40 text := "中文 'quote' \\ $(false) 😀"
41 run("value="+posixQuote(text)+"; printf '%s' \"$value\"", text)
42 run("printf '%s' \"$value\"", text)
43 run("cat <<'EOF'\n中文😀\nEOF", "中文😀\n")
44 // The ASCII encoding expands non-ASCII input fourfold. Exercise a
45 // wrapper well beyond a canonical terminal line buffer and read chunk.
46 long := strings.Repeat("中文😀", 2000)
47 run("printf '%s' "+posixQuote(long), long)
48 // Locale is user command state, not something transport may replace.
49 run("export LC_ALL=C", "")
50 run("printf '%s' \"$LC_ALL\"; printf '%s' '中文'", "C中文")
51
52 ctx, cancel := context.WithCancel(context.Background())
53 defer cancel()
54 // Exceed the capture's marker overlap so progress is delivered
55 // while the command is running, not only when partial output flushes.
56 req.Command = "printf 'started\\n%0128d\\n' 0; sleep 30"
57 req.Progress = &cancelOnStarted{cancel: cancel}
58 res := m.Run(ctx, req)
59 if !res.Canceled || !res.Reset {
60 t.Fatalf("cancel must retire the running shell: %+v", res)
61 }
62 req.Progress = nil
63 run("printf '%s' '恢复😀'", "恢复😀")
64 })
65 }
66 }
67
68 type cancelOnStarted struct {
69 output strings.Builder
70 cancel context.CancelFunc
71 }
72
73 // Between prompts the PTY can be in canonical mode. Disabling line editing
74 // keeps that state deterministic and must not truncate an encoded command.
75 func TestPersistentShellLongCommandWithoutLineEditing(t *testing.T) {
76 skipNonPOSIX(t)
77 sh := posixShell(t)
78 dir := t.TempDir()
79 m := testManager(t)
80 argv := append([]string{sh.Path, "--noediting"}, InteractiveArgv(sh)[1:]...)
81 req := Request{Argv: argv, Dir: dir, Shell: sh, Timeout: 10 * time.Second,
82 Env: []string{"PATH=" + os.Getenv("PATH"), "HOME=" + dir, "TERM=dumb", "INPUTRC=/dev/null", "LC_ALL=C"},
83 Command: "PS2=CONTINUATION_PROMPT"}
84 if res := m.Run(t.Context(), req); res.Err != nil {
85 t.Fatal(res.Err)
86 }
87 text := strings.Repeat("中文😀", 2000)
88 req.Command = "value=" + posixQuote(text) + "; printf '%s' \"$value\""
89 res := m.Run(t.Context(), req)
90 if res.Err != nil || res.Output != text {
91 t.Fatalf("long command: err=%v output bytes=%d want=%d", res.Err, len(res.Output), len(text))
92 }
93 req.Command = "printf '%s' \"$value\""
94 if res := m.Run(t.Context(), req); res.Err != nil || res.Output != text {
95 t.Fatalf("retained value: err=%v output bytes=%d want=%d", res.Err, len(res.Output), len(text))
96 }
97 }
98
99 func (w *cancelOnStarted) Write(p []byte) (int, error) {
100 w.output.Write(p)
101 if strings.Contains(w.output.String(), "started\n") {
102 w.cancel()
103 }
104 return len(p), nil
105 }
106
106 lines GO