返回 DeepSeek-Reasonix
cli_test.go
根目录 / internal / crashreport / cli_test.go
1 package crashreport
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "io"
8 "net/http"
9 "os"
10 "path/filepath"
11 "runtime"
12 "strings"
13 "sync"
14 "testing"
15 "unicode/utf8"
16 )
17
18 type roundTripFunc func(*http.Request) (*http.Response, error)
19
20 func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }
21
22 func TestCapturePanicWritesBoundedSanitizedReport(t *testing.T) {
23 home := t.TempDir()
24 secret := "private prompt contents"
25 apiKey := "sk-proj-abcdefghijklmnopqrstuvwxyz1234567890"
26 stack := "goroutine 7 [running]:\n" +
27 "reasonix/internal/agent.run(" + secret + ")\n" +
28 "\t/Users/alice/private-project/internal/agent/run.go:42 +0x123\n" +
29 "Authorization: Bearer abcdefghijklmnopqrstuvwxyz1234567890\n" +
30 "api_key=" + apiKey
31
32 if err := CapturePanic(home, "v1.20.0", secret+" api_key="+apiKey, []byte(stack)); err != nil {
33 t.Fatal(err)
34 }
35 reports, err := List(home)
36 if err != nil || len(reports) != 1 {
37 t.Fatalf("reports=%d err=%v", len(reports), err)
38 }
39 report := reports[0].Report
40 if report.Kind != "crash" || report.Source != "cli.go" || report.Label != "panic" || report.SchemaVersion != 2 {
41 t.Fatalf("report metadata = %+v", report)
42 }
43 if !strings.Contains(report.Stack, "reasonix/internal/agent.run(...)") || !strings.Contains(report.Stack, "<path>/run.go:42") {
44 t.Fatalf("sanitized stack = %q", report.Stack)
45 }
46 if report.TopFrame != "reasonix/internal/agent.run <path>/run.go:42" {
47 t.Fatalf("top frame = %q", report.TopFrame)
48 }
49 preview, err := Preview(report)
50 if err != nil {
51 t.Fatal(err)
52 }
53 for _, leaked := range []string{secret, apiKey, "alice", "private-project", "Bearer abcdefghijklmnopqrstuvwxyz1234567890"} {
54 if strings.Contains(string(preview), leaked) {
55 t.Fatalf("report leaked %q:\n%s", leaked, preview)
56 }
57 }
58 report.ErrorType = "api_key=" + apiKey
59 preview, err = Preview(report)
60 if err != nil {
61 t.Fatal(err)
62 }
63 if strings.Contains(string(preview), apiKey) {
64 t.Fatalf("send-time field sanitization leaked a key:\n%s", preview)
65 }
66 path := filepath.Join(home, dirName, reports[0].ID+".json")
67 info, err := os.Stat(path)
68 if err != nil {
69 t.Fatal(err)
70 }
71 // Windows reports synthesized POSIX permission bits and enforces access
72 // through inherited ACLs, so only Unix-like systems can assert mode 0600.
73 if runtime.GOOS != "windows" && info.Mode().Perm() != 0o600 {
74 t.Fatalf("report mode=%v", info.Mode().Perm())
75 }
76
77 for i := 0; i < maxReports+5; i++ {
78 if err := CapturePanic(home, "v1.20.0", i, []byte(stack)); err != nil {
79 t.Fatal(err)
80 }
81 }
82 reports, err = List(home)
83 if err != nil || len(reports) != maxReports {
84 t.Fatalf("bounded reports=%d err=%v", len(reports), err)
85 }
86 }
87
88 func TestSendUsesSharedProtocolWithoutDeletingLocalReport(t *testing.T) {
89 home := t.TempDir()
90 if err := CapturePanic(home, "v1.20.0", "boom", []byte("goroutine 1 [running]:\nreasonix.run()\n\t/home/alice/reasonix/main.go:12")); err != nil {
91 t.Fatal(err)
92 }
93 pending, err := Load(home, "")
94 if err != nil {
95 t.Fatal(err)
96 }
97 var uploaded Report
98 client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
99 if req.Method != http.MethodPost || req.URL.String() != "https://example.invalid/v1/report" {
100 t.Fatalf("request = %s %s", req.Method, req.URL)
101 }
102 if got := req.Header.Get("Content-Type"); got != "application/json" {
103 t.Fatalf("content type = %q", got)
104 }
105 if err := json.NewDecoder(req.Body).Decode(&uploaded); err != nil {
106 t.Fatal(err)
107 }
108 return &http.Response{StatusCode: http.StatusAccepted, Status: "202 Accepted", Header: make(http.Header), Body: io.NopCloser(strings.NewReader("ok"))}, nil
109 })}
110 if err := sendWithClient(context.Background(), client, "https://example.invalid/v1/report", pending.Report); err != nil {
111 t.Fatal(err)
112 }
113 if uploaded.Source != "cli.go" || uploaded.Stack == "" || uploaded.TopFrame == "" {
114 t.Fatalf("uploaded report = %+v", uploaded)
115 }
116 if _, err := Load(home, pending.ID); err != nil {
117 t.Fatalf("Send removed local report: %v", err)
118 }
119 if err := Remove(home, pending.ID); err != nil {
120 t.Fatal(err)
121 }
122 if _, err := Load(home, ""); !errors.Is(err, ErrNoReports) {
123 t.Fatalf("Load after Remove = %v", err)
124 }
125 }
126
127 func TestLoadRejectsUnknownIDWithoutPathTraversal(t *testing.T) {
128 home := t.TempDir()
129 if err := CapturePanic(home, "v1.20.0", "boom", []byte("stack")); err != nil {
130 t.Fatal(err)
131 }
132 if _, err := Load(home, "../../config.toml"); err == nil {
133 t.Fatal("path traversal ID was accepted")
134 }
135 }
136
137 func TestConcurrentCaptureKeepsQueueBounded(t *testing.T) {
138 home := t.TempDir()
139 const writers = 32
140 var wg sync.WaitGroup
141 start := make(chan struct{})
142 for i := 0; i < writers; i++ {
143 wg.Add(1)
144 go func(value int) {
145 defer wg.Done()
146 <-start
147 if err := CapturePanic(home, "v1.20.0", value, []byte("reasonix.run()\n\t/home/alice/main.go:12")); err != nil {
148 t.Errorf("CapturePanic: %v", err)
149 }
150 }(i)
151 }
152 close(start)
153 wg.Wait()
154 reports, err := List(home)
155 if err != nil || len(reports) != maxReports {
156 t.Fatalf("reports=%d err=%v", len(reports), err)
157 }
158 }
159
160 func TestCapturePanicPrunesOnlyCurrentReportFormat(t *testing.T) {
161 home := t.TempDir()
162 dir := filepath.Join(home, dirName)
163 if err := os.MkdirAll(dir, 0o700); err != nil {
164 t.Fatal(err)
165 }
166 futurePath := filepath.Join(dir, "00000000000000000000-1-0000000000000000.json")
167 futureReport := `{"kind":"crash","version":"v2.0.0","os":"linux","arch":"amd64","message":"future","schemaVersion":3,"futureField":"preserve me"}`
168 if err := os.WriteFile(futurePath, []byte(futureReport), 0o600); err != nil {
169 t.Fatal(err)
170 }
171
172 for i := 0; i < maxReports+1; i++ {
173 if err := CapturePanic(home, "v1.20.0", i, []byte("reasonix.run()\n\t/home/alice/main.go:12")); err != nil {
174 t.Fatal(err)
175 }
176 }
177 if _, err := os.Stat(futurePath); err != nil {
178 t.Fatalf("future report was removed: %v", err)
179 }
180 reports, err := List(home)
181 if err != nil {
182 t.Fatal(err)
183 }
184 if len(reports) != maxReports {
185 t.Fatalf("current reports=%d, want %d", len(reports), maxReports)
186 }
187 }
188
189 func TestSanitizingLimitPreservesUTF8(t *testing.T) {
190 got := sanitizeText(strings.Repeat("界", maxFieldBytes), maxFieldBytes)
191 if len(got) > maxFieldBytes || !utf8.ValidString(got) {
192 t.Fatalf("sanitized text bytes=%d valid=%v", len(got), utf8.ValidString(got))
193 }
194 }
195
195 lines GO