返回 DeepSeek-Reasonix
e2e_encoding_test.go
根目录 / internal / tool / builtin / e2e_encoding_test.go
1 package builtin
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11 "unicode/utf8"
12
13 "golang.org/x/text/encoding/simplifiedchinese"
14 "golang.org/x/text/transform"
15
16 "reasonix/internal/tool"
17 )
18
19 func gbkBytes(t *testing.T, s string) []byte {
20 t.Helper()
21 b, _, err := transform.Bytes(simplifiedchinese.GB18030.NewEncoder(), []byte(s))
22 if err != nil {
23 t.Fatalf("encode GBK: %v", err)
24 }
25 return b
26 }
27
28 func TestE2EGBKRoundTrip(t *testing.T) {
29 path := filepath.Join(t.TempDir(), "test_gbk.txt")
30 if err := os.WriteFile(path, gbkBytes(t, "你好世界\n这是第二行\n包含函数的测试\n"), 0o644); err != nil {
31 t.Fatal(err)
32 }
33
34 readTL, _ := tool.LookupBuiltin("read_file")
35 editTL, _ := tool.LookupBuiltin("edit_file")
36 grepTL, _ := tool.LookupBuiltin("grep")
37
38 args := func(m map[string]any) json.RawMessage {
39 b, _ := json.Marshal(m)
40 return json.RawMessage(b)
41 }
42
43 out, err := readTL.Execute(context.Background(), args(map[string]any{"path": path}))
44 if err != nil {
45 t.Fatalf("read_file: %v", err)
46 }
47 if !strings.Contains(out, "你好世界") || !strings.Contains(out, "这是第二行") {
48 t.Errorf("read_file did not decode GBK to readable Chinese:\n%s", out)
49 }
50
51 if raw, _ := os.ReadFile(path); utf8.Valid(raw) {
52 t.Error("read_file rewrote GBK file as UTF-8 on disk")
53 }
54
55 if _, err := editTL.Execute(context.Background(), args(map[string]any{
56 "path": path,
57 "old_string": "这是第二行",
58 "new_string": "这是新的行",
59 })); err != nil {
60 t.Fatalf("edit_file: %v", err)
61 }
62
63 raw2, _ := os.ReadFile(path)
64 if utf8.Valid(raw2) {
65 t.Error("edit_file rewrote GBK file as UTF-8 on disk")
66 }
67 decoded, _, _ := transform.Bytes(simplifiedchinese.GB18030.NewDecoder(), raw2)
68 if s := string(decoded); !strings.Contains(s, "这是新的行") || strings.Contains(s, "这是第二行") {
69 t.Errorf("edit not applied to GBK file on disk: %q", s)
70 }
71
72 out2, err := readTL.Execute(context.Background(), args(map[string]any{"path": path}))
73 if err != nil {
74 t.Fatalf("read_file after edit: %v", err)
75 }
76 if !strings.Contains(out2, "这是新的行") {
77 t.Errorf("read_file after edit missing new text:\n%s", out2)
78 }
79
80 grepOut, err := grepTL.Execute(context.Background(), args(map[string]any{
81 "pattern": "函数",
82 "path": path,
83 }))
84 if err != nil {
85 t.Fatalf("grep: %v", err)
86 }
87 if !strings.Contains(grepOut, "函数") {
88 t.Errorf("grep did not match decoded GBK content:\n%s", grepOut)
89 }
90 }
91
92 func e2eArgs(m map[string]any) json.RawMessage {
93 b, _ := json.Marshal(m)
94 return json.RawMessage(b)
95 }
96
97 // write_file must preserve the encoding of a file it overwrites rather than
98 // always writing UTF-8, which would silently corrupt a GBK file.
99 func TestE2EWriteFilePreservesGBKOnOverwrite(t *testing.T) {
100 path := filepath.Join(t.TempDir(), "gbk.txt")
101 if err := os.WriteFile(path, gbkBytes(t, "原始内容\n"), 0o644); err != nil {
102 t.Fatal(err)
103 }
104 writeTL, _ := tool.LookupBuiltin("write_file")
105 if _, err := writeTL.Execute(context.Background(), e2eArgs(map[string]any{
106 "path": path,
107 "content": "全新中文内容\n第二行\n",
108 })); err != nil {
109 t.Fatalf("write_file: %v", err)
110 }
111 raw, _ := os.ReadFile(path)
112 if utf8.Valid(raw) {
113 t.Error("write_file rewrote an existing GBK file as UTF-8 on disk")
114 }
115 decoded, _, _ := transform.Bytes(simplifiedchinese.GB18030.NewDecoder(), raw)
116 if s := string(decoded); !strings.Contains(s, "全新中文内容") || !strings.Contains(s, "第二行") {
117 t.Errorf("write_file content wrong after GBK round-trip: %q", s)
118 }
119 }
120
121 // A newly created file (no existing encoding to preserve) defaults to UTF-8.
122 func TestE2EWriteFileNewFileIsUTF8(t *testing.T) {
123 path := filepath.Join(t.TempDir(), "new.txt")
124 writeTL, _ := tool.LookupBuiltin("write_file")
125 if _, err := writeTL.Execute(context.Background(), e2eArgs(map[string]any{
126 "path": path,
127 "content": "新文件中文\n",
128 })); err != nil {
129 t.Fatalf("write_file: %v", err)
130 }
131 raw, _ := os.ReadFile(path)
132 if !utf8.Valid(raw) {
133 t.Error("a newly created file should be written as UTF-8")
134 }
135 if string(raw) != "新文件中文\n" {
136 t.Errorf("new file content = %q, want UTF-8 Chinese", raw)
137 }
138 }
139
140 // delete_range must decode a GBK file before matching anchors (a UTF-8 anchor
141 // would never match the GBK bytes otherwise) and re-encode it on write.
142 func TestE2EDeleteRangePreservesGBK(t *testing.T) {
143 path := filepath.Join(t.TempDir(), "gbk.txt")
144 if err := os.WriteFile(path, gbkBytes(t, "第一行\n第二行\n第三行\n"), 0o644); err != nil {
145 t.Fatal(err)
146 }
147 delTL, _ := tool.LookupBuiltin("delete_range")
148 if _, err := delTL.Execute(context.Background(), e2eArgs(map[string]any{
149 "path": path,
150 "start_anchor": "第二行",
151 "end_anchor": "第二行",
152 })); err != nil {
153 t.Fatalf("delete_range on GBK file: %v", err)
154 }
155 raw, _ := os.ReadFile(path)
156 if utf8.Valid(raw) {
157 t.Error("delete_range rewrote a GBK file as UTF-8 on disk")
158 }
159 decoded, _, _ := transform.Bytes(simplifiedchinese.GB18030.NewDecoder(), raw)
160 s := string(decoded)
161 if strings.Contains(s, "第二行") {
162 t.Errorf("delete_range did not remove the target line: %q", s)
163 }
164 if !strings.Contains(s, "第一行") || !strings.Contains(s, "第三行") {
165 t.Errorf("delete_range removed the wrong lines: %q", s)
166 }
167 }
168
169 // A UTF-8 CJK file carrying one stray invalid byte is not GB18030. Decoding it
170 // as GB18030 turns the orphaned continuation bytes into U+FFFD, and an edit
171 // anywhere in the file then rewrites every one of them.
172 func TestE2EEditKeepsBytesOfUTF8FileWithStrayByte(t *testing.T) {
173 path := filepath.Join(t.TempDir(), "mixed.go")
174 before := []byte("// 中文注释:修改前的内容\nvalue := 1\n// 结尾\xff\n")
175 if err := os.WriteFile(path, before, 0o644); err != nil {
176 t.Fatal(err)
177 }
178 editTL, _ := tool.LookupBuiltin("edit_file")
179 if _, err := editTL.Execute(context.Background(), e2eArgs(map[string]any{
180 "path": path,
181 "old_string": "value := 1",
182 "new_string": "value := 2",
183 })); err != nil {
184 t.Fatalf("edit_file: %v", err)
185 }
186 got, _ := os.ReadFile(path)
187 want := []byte("// 中文注释:修改前的内容\nvalue := 2\n// 结尾\xff\n")
188 if !bytes.Equal(got, want) {
189 t.Fatalf("edit_file changed bytes outside the edit:\n got % x\nwant % x", got, want)
190 }
191 }
192
192 lines GO