返回 DeepSeek-Reasonix
crash_diagnostics_test.go
根目录 / desktop / crash_diagnostics_test.go
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "sync/atomic"
8 "testing"
9 "time"
10
11 "reasonix/internal/repair"
12 )
13
14 func TestParseWebView2ProcessFailure(t *testing.T) {
15 kind, ok := parseWebView2ProcessFailure("windows | WebVie2wProcess failed with kind 6")
16 if !ok || kind != 6 {
17 t.Fatalf("kind=%d ok=%v", kind, ok)
18 }
19 if _, ok := parseWebView2ProcessFailure("unrelated failure"); ok {
20 t.Fatal("unrelated log message matched WebView2 failure")
21 }
22 }
23
24 func TestWebView2ProcessFailureReportIsStructured(t *testing.T) {
25 report := webView2ProcessFailureReportWithContext(2, 3, "132.0.2957.140")
26 if report.Source != "webview2.process" || report.Label != "windows.webview2.process_failed" {
27 t.Fatalf("report = %+v", report)
28 }
29 if report.FingerprintHint != "windows.webview2.render_process_unresponsive" {
30 t.Fatalf("fingerprint hint = %q", report.FingerprintHint)
31 }
32 for _, want := range []string{"runtime version: 132.0.2957.140", "same-process occurrence: 3", "exit code: unavailable"} {
33 if !strings.Contains(report.Message, want) {
34 t.Fatalf("report message missing %q: %s", want, report.Message)
35 }
36 }
37 }
38
39 func TestWebView2FailureTrackerDeduplicatesSessionBursts(t *testing.T) {
40 var tracker webView2FailureTracker
41 base := time.Date(2026, 8, 3, 10, 0, 0, 0, time.UTC)
42 if occurrence, report := tracker.observe(2, base); occurrence != 1 || !report {
43 t.Fatalf("first observation = (%d, %v)", occurrence, report)
44 }
45 if occurrence, report := tracker.observe(2, base.Add(time.Minute)); occurrence != 2 || report {
46 t.Fatalf("burst observation = (%d, %v)", occurrence, report)
47 }
48 if occurrence, report := tracker.observe(2, base.Add(webView2FailureReportCooldown)); occurrence != 3 || !report {
49 t.Fatalf("post-cooldown observation = (%d, %v)", occurrence, report)
50 }
51 if occurrence, report := tracker.observe(6, base.Add(time.Minute)); occurrence != 1 || !report {
52 t.Fatalf("different kind observation = (%d, %v)", occurrence, report)
53 }
54 }
55
56 func TestPreviousRunReportUsesOnlyBoundedLifecycleContext(t *testing.T) {
57 report := previousRunReport(repair.PreviousRunObservation{
58 Abnormal: true,
59 Phase: "healthy",
60 Version: "v2",
61 InstallProfile: "installer",
62 UpdateFrom: "v1",
63 UpdateTo: "v2",
64 UptimeBucket: "m_2_10",
65 })
66 if report.Source != "native.lifecycle" || report.Label != "desktop.abnormal_exit" {
67 t.Fatalf("report = %+v", report)
68 }
69 if !strings.Contains(report.Message, "uptime bucket: m_2_10") {
70 t.Fatalf("message missing bounded uptime: %q", report.Message)
71 }
72 }
73
74 func TestCapturePreviousFatalCrashQueuesAndRemovesRawDump(t *testing.T) {
75 resetFatalCrashArtifacts(t)
76 const crashedPID = 424242
77 raw := "fatal error: concurrent map writes\n\ngoroutine 1 [running]:\nmain.run()\n\t/home/alice/project/main.go:12\n"
78 path := fatalCrashPathForPID(crashedPID)
79 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
80 t.Fatal(err)
81 }
82 if err := os.WriteFile(path, []byte(raw), 0o600); err != nil {
83 t.Fatal(err)
84 }
85 capturePreviousFatalCrash()
86 if _, err := os.Stat(path); !os.IsNotExist(err) {
87 t.Fatalf("raw fatal dump was not removed: %v", err)
88 }
89 report, ok := readPending(t)
90 if !ok || report.Label != "go.fatal" || report.Source != "go.runtime" {
91 t.Fatalf("queued report = %+v ok=%v", report, ok)
92 }
93 if strings.Contains(report.Stack, "/home/alice") {
94 t.Fatalf("fatal stack leaked home path: %q", report.Stack)
95 }
96 }
97
98 func TestSanitizeFatalRuntimeDumpRemovesPanicValue(t *testing.T) {
99 got := sanitizeFatalRuntimeDump("panic: private prompt text\n\ngoroutine 1 [running]:\nmain.run()\n\t/home/alice/project/main.go:12\n")
100 if strings.Contains(got, "private prompt text") || strings.Contains(got, "/home/alice") {
101 t.Fatalf("fatal dump leaked user-controlled text: %q", got)
102 }
103 if !strings.Contains(got, "panic: [redacted panic value]") || !strings.Contains(got, "main.go:12") {
104 t.Fatalf("fatal dump lost diagnostic structure: %q", got)
105 }
106 }
107
108 func TestCapturePreviousFatalCrashSkipsRuntimeDuplicateOfStructuredPanic(t *testing.T) {
109 resetFatalCrashArtifacts(t)
110 const crashedPID = 424243
111 path := fatalCrashPathForPID(crashedPID)
112 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
113 t.Fatal(err)
114 }
115 if err := os.WriteFile(path, []byte("panic: duplicate\n\ngoroutine 1 [running]:\nmain.run()\n"), 0o600); err != nil {
116 t.Fatal(err)
117 }
118 markFatalCrashCoveredForPID(crashedPID)
119 capturePreviousFatalCrash()
120 if _, ok := readPending(t); ok {
121 t.Fatal("runtime duplicate was queued despite structured panic marker")
122 }
123 }
124
125 func TestCapturePreviousFatalCrashDoesNotTouchLiveOwner(t *testing.T) {
126 resetFatalCrashArtifacts(t)
127 const livePID = 424244
128 path := fatalCrashPathForPID(livePID)
129 raw := []byte("fatal error: still being written\n\ngoroutine 1 [running]:\n")
130 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
131 t.Fatal(err)
132 }
133 if err := os.WriteFile(path, raw, 0o600); err != nil {
134 t.Fatal(err)
135 }
136 oldProcessAlive := fatalCrashProcessAlive
137 fatalCrashProcessAlive = func(pid int) bool { return pid == livePID }
138 t.Cleanup(func() { fatalCrashProcessAlive = oldProcessAlive })
139
140 capturePreviousFatalCrash()
141
142 got, err := os.ReadFile(path)
143 if err != nil {
144 t.Fatalf("live owner's fatal file was removed: %v", err)
145 }
146 if string(got) != string(raw) {
147 t.Fatalf("live owner's fatal file changed: got %q want %q", got, raw)
148 }
149 if _, ok := readPending(t); ok {
150 t.Fatal("live owner's partial fatal output was queued")
151 }
152 }
153
154 func TestCapturePreviousFatalCrashKeepsEmptyLegacyFile(t *testing.T) {
155 resetFatalCrashArtifacts(t)
156 if err := os.MkdirAll(filepath.Dir(legacyFatalCrashPath()), 0o700); err != nil {
157 t.Fatal(err)
158 }
159 if err := os.WriteFile(legacyFatalCrashPath(), nil, 0o600); err != nil {
160 t.Fatal(err)
161 }
162
163 capturePreviousFatalCrash()
164
165 if _, err := os.Stat(legacyFatalCrashPath()); err != nil {
166 t.Fatalf("empty legacy fatal file may belong to a live older process: %v", err)
167 }
168 }
169
170 func TestCapturePreviousFatalCrashMigratesLegacyDump(t *testing.T) {
171 resetFatalCrashArtifacts(t)
172 raw := "fatal error: legacy crash\n\ngoroutine 1 [running]:\nmain.run()\n\t/home/alice/project/main.go:12\n"
173 if err := os.MkdirAll(filepath.Dir(legacyFatalCrashPath()), 0o700); err != nil {
174 t.Fatal(err)
175 }
176 if err := os.WriteFile(legacyFatalCrashPath(), []byte(raw), 0o600); err != nil {
177 t.Fatal(err)
178 }
179
180 capturePreviousFatalCrash()
181
182 if _, err := os.Stat(legacyFatalCrashPath()); !os.IsNotExist(err) {
183 t.Fatalf("captured legacy fatal dump was not removed: %v", err)
184 }
185 report, ok := readPending(t)
186 if !ok || report.Label != "go.fatal" || report.Source != "go.runtime" {
187 t.Fatalf("legacy fatal dump was not migrated: report=%+v ok=%v", report, ok)
188 }
189 }
190
191 func TestAwaitWindowRestoreRequiresNativeConfirmation(t *testing.T) {
192 ticks := make(chan time.Time)
193 deadline := make(chan time.Time, 1)
194 deadline <- time.Now()
195
196 if awaitWindowRestoreConfirmation(func() bool { return false }, ticks, deadline) {
197 t.Fatal("an enqueued show request without native confirmation was treated as restored")
198 }
199 }
200
201 func TestAwaitWindowRestoreAcceptsConfirmationAfterTick(t *testing.T) {
202 ticks := make(chan time.Time, 1)
203 deadline := make(chan time.Time)
204 var confirmed atomic.Bool
205 result := make(chan bool, 1)
206 go func() {
207 result <- awaitWindowRestoreConfirmation(confirmed.Load, ticks, deadline)
208 }()
209
210 confirmed.Store(true)
211 ticks <- time.Now()
212 if !<-result {
213 t.Fatal("native window confirmation was not accepted")
214 }
215 }
216
217 func TestSupersededWindowRestoreDoesNotRemoveLatestJournal(t *testing.T) {
218 path := windowRestoreStatePath()
219 _ = os.Remove(path)
220 t.Cleanup(func() { _ = os.Remove(path) })
221 oldSequence := windowRestoreSequence.Load()
222 t.Cleanup(func() { windowRestoreSequence.Store(oldSequence) })
223
224 latest := windowRestoreState{
225 SchemaVersion: windowRestoreStateVersion,
226 PID: os.Getpid(),
227 AttemptID: 2,
228 Source: "tray",
229 StartedAt: "2026-07-24T08:01:00Z",
230 }
231 if !writeWindowRestoreState(latest) {
232 t.Fatal("write latest window restore state")
233 }
234 windowRestoreSequence.Store(latest.AttemptID)
235
236 NewApp().completeWindowRestoreAttempt(1, windowRestoreState{AttemptID: 1}, true)
237
238 got, err := readWindowRestoreState()
239 if err != nil {
240 t.Fatalf("latest journal was removed by superseded attempt: %v", err)
241 }
242 if got != latest {
243 t.Fatalf("latest journal changed: got %+v want %+v", got, latest)
244 }
245 }
246
247 func resetFatalCrashArtifacts(t *testing.T) {
248 t.Helper()
249 oldProcessAlive := fatalCrashProcessAlive
250 fatalCrashProcessAlive = func(int) bool { return false }
251 removeAllPendingCrashes()
252 _ = os.Remove(legacyFatalCrashPath())
253 _ = os.Remove(legacyFatalCrashCoveredPath())
254 _ = os.RemoveAll(fatalCrashDir())
255 t.Cleanup(func() {
256 removeAllPendingCrashes()
257 _ = os.Remove(legacyFatalCrashPath())
258 _ = os.Remove(legacyFatalCrashCoveredPath())
259 _ = os.RemoveAll(fatalCrashDir())
260 fatalCrashProcessAlive = oldProcessAlive
261 })
262 }
263
264 func TestWindowRestoreFailureReportSeparatesTimeoutAndSource(t *testing.T) {
265 report := windowRestoreFailureReport("timeout", "second_instance", "2026-07-24T08:00:00Z")
266 if report.Label != "windows.window_restore.timeout" || report.TopFrame != "windows.window_restore.second_instance" {
267 t.Fatalf("report = %+v", report)
268 }
269 }
270
271 func TestWriteWindowRestoreStateCreatesReadableJournal(t *testing.T) {
272 path := windowRestoreStatePath()
273 _ = os.Remove(path)
274 t.Cleanup(func() { _ = os.Remove(path) })
275
276 want := windowRestoreState{
277 SchemaVersion: windowRestoreStateVersion,
278 PID: os.Getpid(),
279 Source: "tray",
280 StartedAt: "2026-07-24T08:00:00Z",
281 }
282 if !writeWindowRestoreState(want) {
283 t.Fatal("writeWindowRestoreState returned false")
284 }
285 got, err := readWindowRestoreState()
286 if err != nil {
287 t.Fatalf("readWindowRestoreState: %v", err)
288 }
289 if got != want {
290 t.Fatalf("journal = %+v, want %+v", got, want)
291 }
292 }
293
293 lines GO