返回 DeepSeek-Reasonix
protocol_test.go
根目录 / internal / persistentshell / protocol_test.go
1 package persistentshell
2
3 import (
4 "bytes"
5 "os/exec"
6 "strings"
7 "testing"
8 )
9
10 func TestExtractOutputIgnoresEchoedScript(t *testing.T) {
11 start := "REASONIX_START_abc"
12 end := "REASONIX_END_abc:"
13 raw := posixCommandScript("pwd", start, end) +
14 start + "\n" +
15 "/tmp/work\n" +
16 end + "0\n"
17 body, code, ok := extractOutput(raw, start, end)
18 if !ok {
19 t.Fatal("expected completed extraction")
20 }
21 if code != 0 {
22 t.Fatalf("code=%d", code)
23 }
24 if body != "/tmp/work" {
25 t.Fatalf("body=%q", body)
26 }
27 }
28
29 func TestAnsiCQuoteASCIIByteRoundTrip(t *testing.T) {
30 skipNonPOSIX(t)
31 // NUL cannot occur in a shell argument. Include every other byte followed
32 // by octal digits to catch escapes that accidentally consume the suffix.
33 var input []byte
34 for c := 1; c <= 255; c++ {
35 input = append(input, byte(c), '7', '0')
36 }
37 quoted := ansiCQuote(string(input))
38 for _, c := range []byte(quoted) {
39 if c < 0x20 || c >= 0x7f {
40 t.Fatalf("unsafe terminal input byte: %02x", c)
41 }
42 }
43 for _, name := range []string{"bash", "zsh"} {
44 t.Run(name, func(t *testing.T) {
45 path, err := exec.LookPath(name)
46 if err != nil {
47 t.Skipf("%s not installed", name)
48 }
49 cmd := exec.Command(path, "-c", "printf '%s' "+quoted)
50 cmd.Env = []string{"LC_ALL=C"}
51 got, err := cmd.Output()
52 if err != nil {
53 t.Fatal(err)
54 }
55 if !bytes.Equal(got, input) {
56 t.Fatalf("shell decoded %x, want %x", got, input)
57 }
58 })
59 }
60 }
61
62 // The echoed wrapper source contains both markers. Completion must require the
63 // status digits that only the executed printf can produce.
64 func TestEchoedScriptAloneIsNotCompletion(t *testing.T) {
65 start := "REASONIX_START_abc"
66 end := "REASONIX_END_abc:"
67 if _, _, ok := extractOutput(posixCommandScript("pwd", start, end), start, end); ok {
68 t.Fatal("echoed wrapper source must not complete a command")
69 }
70 if readyLine(posixSetupScript()) {
71 t.Fatal("echoed setup source must not report readiness")
72 }
73 }
74
75 func TestExtractOutputCRLF(t *testing.T) {
76 start := "REASONIX_START_x"
77 end := "REASONIX_END_x:"
78 raw := start + "\r\nhello\r\n" + end + "7\r\n"
79 body, code, ok := extractOutput(raw, start, end)
80 if !ok || code != 7 || body != "hello" {
81 t.Fatalf("body=%q code=%d ok=%v", body, code, ok)
82 }
83 }
84
85 // A command whose output has no trailing newline leaves the status marker
86 // mid-line. Requiring a line start there hung every such command until its
87 // deadline (printf without \n, echo -n, cat of a file with no final newline).
88 func TestExtractOutputWithoutTrailingNewline(t *testing.T) {
89 start := "REASONIX_START_y"
90 end := "REASONIX_END_y:"
91 raw := start + "\nhi" + end + "0\n"
92 body, code, ok := extractOutput(raw, start, end)
93 if !ok || code != 0 || body != "hi" {
94 t.Fatalf("body=%q code=%d ok=%v", body, code, ok)
95 }
96 }
97
98 func TestReadyLine(t *testing.T) {
99 if !readyLine("noise\n" + readyToken + "\nmore") {
100 t.Fatal("ready token not detected")
101 }
102 if readyLine("printf '%s\\n' '" + readyToken + "'\n") {
103 t.Fatal("quoted token in echoed source must not count as ready")
104 }
105 }
106
107 func TestPosixQuote(t *testing.T) {
108 if got := posixQuote("it's"); got != `'it'\''s'` {
109 t.Fatalf("got %q", got)
110 }
111 }
112
113 // A multi-line command has to reach the shell as one physical input line, or an
114 // interactive shell prints PS2 and the wrapper's own source leaks into output.
115 func TestAnsiCQuoteKeepsOnePhysicalLine(t *testing.T) {
116 script := posixCommandScript("cat <<'EOF'\nline\nEOF", "S", "E:")
117 if strings.Count(script, "\n") != 1 || !strings.HasSuffix(script, "\n") {
118 t.Fatalf("wrapper must be one line, got %q", script)
119 }
120 if got := ansiCQuote("a'b\nc\\d\te"); got != `$'a\'b\nc\\d\te'` {
121 t.Fatalf("quote=%q", got)
122 }
123 if got := ansiCQuote("\x01"); got != `$'\001'` {
124 t.Fatalf("control quote=%q", got)
125 }
126 }
127
128 // The command's stdin is /dev/null, matching one-shot execution: a command that
129 // prompts fails immediately instead of blocking the session shell.
130 func TestCommandScriptClosesStdin(t *testing.T) {
131 if !strings.Contains(posixCommandScript("read x", "S", "E:"), "</dev/null") {
132 t.Fatal("wrapper must detach stdin")
133 }
134 }
135
136 func TestLongCommandScriptBoundsPhysicalLinesAndPreservesState(t *testing.T) {
137 skipNonPOSIX(t)
138 text := strings.Repeat("中文😀'\\\n", 2000)
139 stages := commandStages("value="+posixQuote(text)+"; printf '%s' \"$value\"; false", "S", "E:")
140 var script, acknowledgements string
141 for _, stage := range stages {
142 script += stage.script
143 if stage.ack != "" {
144 acknowledgements += stage.ack + "\n"
145 }
146 }
147 for line := range strings.SplitSeq(script, "\n") {
148 if len(line) > 768 {
149 t.Fatalf("physical input line has %d bytes; canonical PTYs can discard excess bytes", len(line))
150 }
151 }
152 if _, _, ok := extractOutput(script, "S", "E:"); ok {
153 t.Fatal("echoed multi-line source fabricated completion")
154 }
155 for _, name := range []string{"bash", "zsh"} {
156 t.Run(name, func(t *testing.T) {
157 path, err := exec.LookPath(name)
158 if err != nil {
159 t.Skipf("%s not installed", name)
160 }
161 cmd := exec.CommandContext(t.Context(), path, "-c", script+"printf '%s' \"$value\"")
162 cmd.Env = []string{"LC_ALL=C"}
163 got, err := cmd.Output()
164 want := acknowledgements + "S\n" + text + "E:1\n" + text
165 if err != nil || string(got) != want {
166 t.Fatalf("wrapper changed output, state or status: err=%v bytes=%d want=%d", err, len(got), len(want))
167 }
168 })
169 }
170 }
171
172 // The line discipline can emit \r\r\n under output pressure. Mapping every \r
173 // to \n injected blank lines into model-visible output.
174 func TestNormalizePTYCollapsesCarriageReturnRuns(t *testing.T) {
175 cases := map[string]string{
176 "a\r\nb": "a\nb",
177 "a\r\r\nb": "a\nb",
178 "a\r\r\r\nb": "a\nb",
179 "a\rb": "a\nb",
180 "plain": "plain",
181 }
182 for in, want := range cases {
183 if got := normalizePTY(in); got != want {
184 t.Fatalf("normalizePTY(%q)=%q want %q", in, got, want)
185 }
186 }
187 }
188
188 lines GO