返回 DeepSeek-Reasonix
crash_diagnostics_test.go
根目录 / desktop / crash_diagnostics_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "runtime"
8 "strings"
9 "testing"
10
11 "reasonix/internal/repair"
12 )
13
14 // Pending reports queued by the retired WebView2/WebKitGTK shell must still
15 // decode and forward after the upgrade removed every producer.
16 func TestPendingReportDecodesLegacyWebRuntimeDiagnostics(t *testing.T) {
17 raw := `{
18 "kind": "performance",
19 "version": "v1.24.0",
20 "os": "linux",
21 "arch": "amd64",
22 "message": "legacy",
23 "webRuntime": {"engine": "webkitgtk", "kind": "web_process", "reason": "crashed", "runtimeVersion": "2.42.5", "gpuMode": "on_demand", "recovery": "reload_succeeded"},
24 "webview2": {"kind": "render_process_unresponsive", "reason": "unresponsive", "runtimeVersion": "132.0.1", "gpuDisabled": true, "recovery": "reload_failed"}
25 }`
26 var report crashReport
27 if err := json.Unmarshal([]byte(raw), &report); err != nil {
28 t.Fatalf("legacy pending report no longer decodes: %v", err)
29 }
30 if report.WebRuntime == nil || report.WebRuntime.Engine != "webkitgtk" || report.WebRuntime.Recovery != "reload_succeeded" {
31 t.Fatalf("webRuntime diagnostic lost: %+v", report.WebRuntime)
32 }
33 if report.WebView2 == nil || report.WebView2.Kind != "render_process_unresponsive" || !report.WebView2.GPUDisabled {
34 t.Fatalf("webview2 diagnostic lost: %+v", report.WebView2)
35 }
36 }
37
38 func TestPreviousRunReportUsesOnlyBoundedLifecycleContext(t *testing.T) {
39 report := previousRunReport(repair.PreviousRunObservation{
40 Abnormal: true,
41 Phase: "healthy",
42 Version: "v2",
43 InstallProfile: "installer",
44 UpdateFrom: "v1",
45 UpdateTo: "v2",
46 UptimeBucket: "m_2_10",
47 })
48 if report.Source != "native.lifecycle.legacy" || report.Label != "desktop.legacy_abnormal_exit" {
49 t.Fatalf("report = %+v", report)
50 }
51 if !strings.Contains(report.Message, "uptime bucket: m_2_10") {
52 t.Fatalf("message missing bounded uptime: %q", report.Message)
53 }
54 }
55
56 func TestDesktopLifecycleReportUsesCurrentLifecycleNamespace(t *testing.T) {
57 report := desktopLifecycleReport(desktopLifecycleObservation{
58 Version: "v1.23.0", Channel: "stable", Phase: "healthy",
59 StartedAt: "2026-08-10T01:00:00Z", UpdatedAt: "2026-08-10T02:00:00Z",
60 })
61 if report.Source != "native.lifecycle" || report.Label != "desktop.abnormal_exit.v2" {
62 t.Fatalf("report = %+v", report)
63 }
64 if report.FingerprintHint != "desktop.abnormal_exit.v2."+runtime.GOOS+".healthy" {
65 t.Fatalf("fingerprint = %q", report.FingerprintHint)
66 }
67 }
68
69 func TestCapturePreviousFatalCrashQueuesAndRemovesRawDump(t *testing.T) {
70 resetFatalCrashArtifacts(t)
71 const crashedPID = 424242
72 raw := "fatal error: concurrent map writes\n\ngoroutine 1 [running]:\nmain.run()\n\t/home/alice/project/main.go:12\n"
73 path := fatalCrashPathForPID(crashedPID)
74 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
75 t.Fatal(err)
76 }
77 if err := os.WriteFile(path, []byte(raw), 0o600); err != nil {
78 t.Fatal(err)
79 }
80 capturePreviousFatalCrash()
81 if _, err := os.Stat(path); !os.IsNotExist(err) {
82 t.Fatalf("raw fatal dump was not removed: %v", err)
83 }
84 report, ok := readPending(t)
85 if !ok || report.Label != "go.fatal" || report.Source != "go.runtime" {
86 t.Fatalf("queued report = %+v ok=%v", report, ok)
87 }
88 if strings.Contains(report.Stack, "/home/alice") {
89 t.Fatalf("fatal stack leaked home path: %q", report.Stack)
90 }
91 }
92
93 func TestSanitizeFatalRuntimeDumpRemovesPanicValue(t *testing.T) {
94 got := sanitizeFatalRuntimeDump("panic: private prompt text\n\ngoroutine 1 [running]:\nmain.run()\n\t/home/alice/project/main.go:12\n")
95 if strings.Contains(got, "private prompt text") || strings.Contains(got, "/home/alice") {
96 t.Fatalf("fatal dump leaked user-controlled text: %q", got)
97 }
98 if !strings.Contains(got, "panic: [redacted panic value]") || !strings.Contains(got, "main.go:12") {
99 t.Fatalf("fatal dump lost diagnostic structure: %q", got)
100 }
101 }
102
103 func TestCapturePreviousFatalCrashSkipsRuntimeDuplicateOfStructuredPanic(t *testing.T) {
104 resetFatalCrashArtifacts(t)
105 const crashedPID = 424243
106 path := fatalCrashPathForPID(crashedPID)
107 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
108 t.Fatal(err)
109 }
110 if err := os.WriteFile(path, []byte("panic: duplicate\n\ngoroutine 1 [running]:\nmain.run()\n"), 0o600); err != nil {
111 t.Fatal(err)
112 }
113 markFatalCrashCoveredForPID(crashedPID)
114 capturePreviousFatalCrash()
115 if _, ok := readPending(t); ok {
116 t.Fatal("runtime duplicate was queued despite structured panic marker")
117 }
118 }
119
120 func TestCapturePreviousFatalCrashDoesNotTouchLiveOwner(t *testing.T) {
121 resetFatalCrashArtifacts(t)
122 const livePID = 424244
123 path := fatalCrashPathForPID(livePID)
124 raw := []byte("fatal error: still being written\n\ngoroutine 1 [running]:\n")
125 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
126 t.Fatal(err)
127 }
128 if err := os.WriteFile(path, raw, 0o600); err != nil {
129 t.Fatal(err)
130 }
131 oldProcessAlive := fatalCrashProcessAlive
132 fatalCrashProcessAlive = func(pid int) bool { return pid == livePID }
133 t.Cleanup(func() { fatalCrashProcessAlive = oldProcessAlive })
134
135 capturePreviousFatalCrash()
136
137 got, err := os.ReadFile(path)
138 if err != nil {
139 t.Fatalf("live owner's fatal file was removed: %v", err)
140 }
141 if string(got) != string(raw) {
142 t.Fatalf("live owner's fatal file changed: got %q want %q", got, raw)
143 }
144 if _, ok := readPending(t); ok {
145 t.Fatal("live owner's partial fatal output was queued")
146 }
147 }
148
149 func TestCapturePreviousFatalCrashKeepsEmptyLegacyFile(t *testing.T) {
150 resetFatalCrashArtifacts(t)
151 if err := os.MkdirAll(filepath.Dir(legacyFatalCrashPath()), 0o700); err != nil {
152 t.Fatal(err)
153 }
154 if err := os.WriteFile(legacyFatalCrashPath(), nil, 0o600); err != nil {
155 t.Fatal(err)
156 }
157
158 capturePreviousFatalCrash()
159
160 if _, err := os.Stat(legacyFatalCrashPath()); err != nil {
161 t.Fatalf("empty legacy fatal file may belong to a live older process: %v", err)
162 }
163 }
164
165 func TestCapturePreviousFatalCrashMigratesLegacyDump(t *testing.T) {
166 resetFatalCrashArtifacts(t)
167 raw := "fatal error: legacy crash\n\ngoroutine 1 [running]:\nmain.run()\n\t/home/alice/project/main.go:12\n"
168 if err := os.MkdirAll(filepath.Dir(legacyFatalCrashPath()), 0o700); err != nil {
169 t.Fatal(err)
170 }
171 if err := os.WriteFile(legacyFatalCrashPath(), []byte(raw), 0o600); err != nil {
172 t.Fatal(err)
173 }
174
175 capturePreviousFatalCrash()
176
177 if _, err := os.Stat(legacyFatalCrashPath()); !os.IsNotExist(err) {
178 t.Fatalf("captured legacy fatal dump was not removed: %v", err)
179 }
180 report, ok := readPending(t)
181 if !ok || report.Label != "go.fatal" || report.Source != "go.runtime" {
182 t.Fatalf("legacy fatal dump was not migrated: report=%+v ok=%v", report, ok)
183 }
184 }
185
186 func resetFatalCrashArtifacts(t *testing.T) {
187 t.Helper()
188 oldProcessAlive := fatalCrashProcessAlive
189 fatalCrashProcessAlive = func(int) bool { return false }
190 removeAllPendingCrashes()
191 _ = os.Remove(legacyFatalCrashPath())
192 _ = os.Remove(legacyFatalCrashCoveredPath())
193 _ = os.RemoveAll(fatalCrashDir())
194 t.Cleanup(func() {
195 removeAllPendingCrashes()
196 _ = os.Remove(legacyFatalCrashPath())
197 _ = os.Remove(legacyFatalCrashCoveredPath())
198 _ = os.RemoveAll(fatalCrashDir())
199 fatalCrashProcessAlive = oldProcessAlive
200 })
201 }
202
202 lines GO