返回 DeepSeek-Reasonix
crash_pending_test.go
根目录 / desktop / crash_pending_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "net/http"
6 "net/http/httptest"
7 "os"
8 "strings"
9 "sync"
10 "sync/atomic"
11 "testing"
12 )
13
14 func readPending(t *testing.T) (crashReport, bool) {
15 t.Helper()
16 paths := pendingCrashPaths()
17 if len(paths) == 0 {
18 return crashReport{}, false
19 }
20 body, err := os.ReadFile(paths[0])
21 if err != nil {
22 return crashReport{}, false
23 }
24 var r crashReport
25 if err := json.Unmarshal(body, &r); err != nil {
26 t.Fatalf("pending file not valid JSON: %v", err)
27 }
28 return r, true
29 }
30
31 func TestRecoverToPendingCapturesAndReraises(t *testing.T) {
32 t.Cleanup(removeAllPendingCrashes)
33
34 func() {
35 defer func() {
36 if recover() == nil {
37 t.Fatal("recoverToPending must re-raise the panic")
38 }
39 }()
40 app := NewApp()
41 defer app.recoverToPending("unit")
42 panic(`boom at C:\Users\alice\proj\x.go`)
43 }()
44
45 r, ok := readPending(t)
46 if !ok {
47 t.Fatal("expected a pending crash file")
48 }
49 if r.Kind != "crash" {
50 t.Errorf("kind = %q, want crash", r.Kind)
51 }
52 if !strings.Contains(r.Message, "[go panic] unit") {
53 t.Errorf("message missing site prefix: %q", r.Message)
54 }
55 if r.Source != "go" || r.Label != "unit" || r.ErrorMessage == "" || r.Stack == "" || r.TopFrame == "" {
56 t.Errorf("structured panic metadata missing: %+v", r)
57 }
58 if strings.Contains(r.Message, `Users\alice`) {
59 t.Errorf("message not scrubbed: %q", r.Message)
60 }
61 }
62
63 func TestWritePendingCrashCaps(t *testing.T) {
64 t.Cleanup(removeAllPendingCrashes)
65 writePendingCrash("big", "x", []byte(strings.Repeat("a", 64<<10)))
66 r, ok := readPending(t)
67 if !ok {
68 t.Fatal("expected a pending crash file")
69 }
70 if len(r.Message) > maxCrashDetailBytes {
71 t.Errorf("message len = %d, want <= %d", len(r.Message), maxCrashDetailBytes)
72 }
73 }
74
75 func TestWritePendingReportQueuesWithoutOverwritingExistingCrash(t *testing.T) {
76 t.Cleanup(removeAllPendingCrashes)
77 writePendingCrash("panic", "boom", []byte("stack"))
78 before, ok := readPending(t)
79 if !ok {
80 t.Fatal("expected initial pending crash")
81 }
82
83 hang := baseCrashReport("performance")
84 hang.Source = "native.watchdog"
85 hang.Label = "mac.main_thread.hang"
86 hang.Message = "hang"
87 if !writePendingReport(hang, false) {
88 t.Fatal("writePendingReport should enqueue the second report")
89 }
90 after, ok := readPending(t)
91 if !ok {
92 t.Fatal("expected pending crash after skipped write")
93 }
94 if after.Label != before.Label || after.Message != before.Message {
95 t.Fatalf("pending crash was overwritten: before=%+v after=%+v", before, after)
96 }
97 if got := len(pendingCrashPaths()); got != 2 {
98 t.Fatalf("pending reports = %d, want 2", got)
99 }
100 }
101
102 func TestWritePendingReportQueueIsBoundedUnderConcurrentWriters(t *testing.T) {
103 t.Cleanup(removeAllPendingCrashes)
104 const writers = 32
105 start := make(chan struct{})
106 var ready sync.WaitGroup
107 var done sync.WaitGroup
108 var successes atomic.Int32
109
110 for i := 0; i < writers; i++ {
111 ready.Add(1)
112 done.Add(1)
113 go func() {
114 defer done.Done()
115 report := baseCrashReport("performance")
116 report.Source = "native.watchdog"
117 report.Label = "mac.main_thread.hang"
118 report.Message = strings.Repeat("hang", 1024)
119 ready.Done()
120 <-start
121 if writePendingReport(report, false) {
122 successes.Add(1)
123 }
124 }()
125 }
126 ready.Wait()
127 close(start)
128 done.Wait()
129
130 if got := successes.Load(); got != writers {
131 t.Fatalf("successful queued writers = %d, want %d", got, writers)
132 }
133 if got := len(pendingCrashPaths()); got != maxPendingCrashes {
134 t.Fatalf("pending reports = %d, want bounded queue of %d", got, maxPendingCrashes)
135 }
136 }
137
138 func TestWritePendingCrashScrubsSensitiveText(t *testing.T) {
139 t.Cleanup(removeAllPendingCrashes)
140 apiKey := "sk-proj-" + "abcdefghijklmnopqrstuvwxyz1234567890"
141 bearer := "abcdefghijklmnopqrstuvwxyz1234567890ABCDE"
142 longHex := "0123456789abcdef0123456789abcdef"
143
144 privatePanicValue := "private user prompt contents"
145 writePendingCrash("unit", privatePanicValue+" api_key="+apiKey+" user alice@example.com", []byte("goroutine\nAuthorization: Bearer "+bearer+"\n/home/alice/project/x.go:12\nhash "+longHex))
146 r, ok := readPending(t)
147 if !ok {
148 t.Fatal("expected a pending crash file")
149 }
150 freeText := strings.Join([]string{r.Message, r.ErrorMessage, r.Stack, r.TopFrame}, "\n")
151 for _, leaked := range []string{privatePanicValue, apiKey, bearer, longHex, "alice@example.com", "/home/alice"} {
152 if strings.Contains(freeText, leaked) {
153 t.Fatalf("sensitive value leaked %q in %+v", leaked, r)
154 }
155 }
156 }
157
158 func TestFlushPendingCrashSendsAndClears(t *testing.T) {
159 oldVersion, oldEndpoint := version, crashEndpoint
160 t.Cleanup(func() {
161 version, crashEndpoint = oldVersion, oldEndpoint
162 removeAllPendingCrashes()
163 })
164 version = "v9.9.9"
165
166 var hits atomic.Int32
167 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
168 hits.Add(1)
169 w.WriteHeader(http.StatusAccepted)
170 }))
171 defer srv.Close()
172 crashEndpoint = srv.URL
173
174 writePendingCrash("flush", "boom", []byte("stack"))
175 NewApp().flushPendingCrash()
176
177 if hits.Load() != 1 {
178 t.Errorf("server hits = %d, want 1", hits.Load())
179 }
180 if _, ok := readPending(t); ok {
181 t.Error("pending file should be cleared after a successful send")
182 }
183 }
184
185 func TestFlushPendingCrashDevGuard(t *testing.T) {
186 oldVersion := version
187 t.Cleanup(func() {
188 version = oldVersion
189 removeAllPendingCrashes()
190 })
191 version = "dev"
192
193 writePendingCrash("dev", "boom", []byte("stack"))
194 NewApp().flushPendingCrash()
195
196 if _, ok := readPending(t); !ok {
197 t.Error("dev build must leave the pending file untouched")
198 }
199 }
200
201 func TestFlushPendingCrashIgnoresSafeModeEnv(t *testing.T) {
202 // v1.20+: REASONIX_SAFE_MODE no longer blocks crash flush. With telemetry
203 // off/default, the pending file is consumed (sent or dropped).
204 t.Setenv("REASONIX_SAFE_MODE", "1")
205 oldVersion := version
206 t.Cleanup(func() {
207 version = oldVersion
208 removeAllPendingCrashes()
209 })
210 version = "v9.9.9"
211
212 writePendingCrash("safe", "boom", []byte("stack"))
213 NewApp().flushPendingCrash()
214 // Either sent or dropped is fine; must not retain solely because of Safe Mode env.
215 // When telemetry is off the file is removed; when on it is sent. Both clear it.
216 }
217
217 lines GO