返回 DeepSeek-Reasonix
lifecycle_diagnostics_test.go
根目录 / desktop / lifecycle_diagnostics_test.go
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "strconv"
7 "sync"
8 "sync/atomic"
9 "testing"
10 "time"
11 )
12
13 func lifecycleTrackerForTest(t *testing.T, root string, pid int, runID string) *desktopLifecycleTracker {
14 t.Helper()
15 tracker := newDesktopLifecycleTracker(root, "v1.23.0", "stable")
16 tracker.state.PID = pid
17 tracker.state.RunID = runID
18 tracker.path = filepath.Join(tracker.dir, runID+".json")
19 tracker.processAlive = func(int) bool { return false }
20 return tracker
21 }
22
23 func TestDesktopLifecycleDeadRecordIsConsumedOnce(t *testing.T) {
24 root := t.TempDir()
25 dead := lifecycleTrackerForTest(t, root, 4242, "dead")
26 if err := dead.start(); err != nil {
27 t.Fatal(err)
28 }
29 dead.mark("healthy")
30 dead.stopWriter()
31
32 reader := lifecycleTrackerForTest(t, root, os.Getpid(), "reader")
33 got := reader.consumePrevious(true)
34 if len(got) != 1 || got[0].Phase != "healthy" || got[0].Version != "v1.23.0" {
35 t.Fatalf("observations = %+v", got)
36 }
37 if replay := reader.consumePrevious(true); len(replay) != 0 {
38 t.Fatalf("lifecycle record replayed: %+v", replay)
39 }
40 }
41
42 func TestDesktopLifecycleClaimIsRestoredWhenPendingQueueWriteFails(t *testing.T) {
43 root := t.TempDir()
44 dead := lifecycleTrackerForTest(t, root, 4242, "dead-retry")
45 if err := dead.start(); err != nil {
46 t.Fatal(err)
47 }
48 dead.stopWriter()
49 reader := lifecycleTrackerForTest(t, root, os.Getpid(), "reader")
50 first := reader.consumePrevious(true)
51 if len(first) != 1 {
52 t.Fatalf("first claim = %+v", first)
53 }
54 reader.finalizeObservation(first[0], false)
55 second := reader.consumePrevious(true)
56 if len(second) != 1 || second[0].RunID != "dead-retry" {
57 t.Fatalf("restored claim was not retryable: %+v", second)
58 }
59 reader.finalizeObservation(second[0], true)
60 if _, err := os.Stat(dead.path); !os.IsNotExist(err) {
61 t.Fatalf("persisted evidence was not acknowledged: %v", err)
62 }
63 }
64
65 func TestDesktopLifecycleConcurrentConsumersClaimOnce(t *testing.T) {
66 root := t.TempDir()
67 dead := lifecycleTrackerForTest(t, root, 4242, "dead-concurrent")
68 if err := dead.start(); err != nil {
69 t.Fatal(err)
70 }
71 dead.stopWriter()
72
73 const observers = 16
74 start := make(chan struct{})
75 var ready sync.WaitGroup
76 var group sync.WaitGroup
77 var reports atomic.Int32
78 for observer := range observers {
79 ready.Add(1)
80 group.Go(func() {
81 reader := lifecycleTrackerForTest(t, root, os.Getpid(), "reader-"+strconv.Itoa(observer))
82 ready.Done()
83 <-start
84 if len(reader.consumePrevious(true)) == 1 {
85 reports.Add(1)
86 }
87 })
88 }
89 ready.Wait()
90 close(start)
91 group.Wait()
92 if got := reports.Load(); got != 1 {
93 t.Fatalf("concurrent reports = %d, want 1", got)
94 }
95 }
96
97 func TestDesktopDiagnosticsOwnershipIsNonBlockingAndExclusive(t *testing.T) {
98 oldVersion := version
99 version = "v1.23.0"
100 t.Cleanup(func() { version = oldVersion })
101
102 first := NewApp()
103 prepareDesktopDiagnostics(first)
104 t.Cleanup(func() {
105 first.lifecycle.tracker.clean()
106 first.releaseDesktopDiagnosticsOwnership()
107 })
108 if !first.diagnosticsOwner {
109 t.Fatal("first process did not claim diagnostics ownership")
110 }
111
112 second := NewApp()
113 prepareDesktopDiagnostics(second)
114 if second.diagnosticsOwner {
115 second.releaseDesktopDiagnosticsOwnership()
116 t.Fatal("second process unexpectedly claimed diagnostics ownership")
117 }
118
119 first.lifecycle.tracker.clean()
120 first.releaseDesktopDiagnosticsOwnership()
121 prepareDesktopDiagnostics(second)
122 t.Cleanup(func() {
123 second.lifecycle.tracker.clean()
124 second.releaseDesktopDiagnosticsOwnership()
125 })
126 if !second.diagnosticsOwner {
127 t.Fatal("ownership was not released for the next process")
128 }
129 }
130
131 func TestDesktopDiagnosticsSkipsNonPrimaryLaunchModes(t *testing.T) {
132 oldVersion := version
133 t.Cleanup(func() {
134 version = oldVersion
135 })
136
137 version = "dev"
138 dev := NewApp()
139 prepareDesktopDiagnostics(dev)
140 if dev.diagnosticsOwner {
141 t.Fatal("dev build claimed diagnostics ownership")
142 }
143
144 }
145
146 func TestDesktopLifecycleLiveRecordIsPreserved(t *testing.T) {
147 root := t.TempDir()
148 live := lifecycleTrackerForTest(t, root, 4242, "live")
149 if err := live.start(); err != nil {
150 t.Fatal(err)
151 }
152 t.Cleanup(live.stopWriter)
153
154 reader := lifecycleTrackerForTest(t, root, os.Getpid(), "reader")
155 reader.processAlive = func(pid int) bool { return pid == 4242 }
156 if got := reader.consumePrevious(true); len(got) != 0 {
157 t.Fatalf("live record observed: %+v", got)
158 }
159 if _, err := os.Stat(live.path); err != nil {
160 t.Fatalf("live record was removed: %v", err)
161 }
162 }
163
164 func TestDesktopLifecycleOptOutConsumesWithoutReporting(t *testing.T) {
165 root := t.TempDir()
166 dead := lifecycleTrackerForTest(t, root, 4242, "dead")
167 if err := dead.start(); err != nil {
168 t.Fatal(err)
169 }
170 dead.stopWriter()
171
172 reader := lifecycleTrackerForTest(t, root, os.Getpid(), "reader")
173 if got := reader.consumePrevious(false); len(got) != 0 {
174 t.Fatalf("opt-out returned observations: %+v", got)
175 }
176 if _, err := os.Stat(dead.path); !os.IsNotExist(err) {
177 t.Fatalf("opt-out did not consume dead record: %v", err)
178 }
179 }
180
181 func TestDesktopLifecycleUnknownSchemaIsPreserved(t *testing.T) {
182 root := t.TempDir()
183 future := lifecycleTrackerForTest(t, root, 4242, "future")
184 future.state.SchemaVersion = desktopLifecycleSchemaVersion + 1
185 if err := future.start(); err != nil {
186 t.Fatal(err)
187 }
188 future.stopWriter()
189
190 reader := lifecycleTrackerForTest(t, root, os.Getpid(), "reader")
191 if got := reader.consumePrevious(true); len(got) != 0 {
192 t.Fatalf("future lifecycle record observed: %+v", got)
193 }
194 state, err := readDesktopLifecycleState(future.path)
195 if err != nil || state.SchemaVersion != desktopLifecycleSchemaVersion+1 {
196 t.Fatalf("future lifecycle record was not preserved: state=%+v err=%v", state, err)
197 }
198 }
199
200 func TestDesktopLifecycleUnknownSchemaWithoutCurrentFieldsIsNeverPruned(t *testing.T) {
201 root := t.TempDir()
202 future := lifecycleTrackerForTest(t, root, 4242, "future-fields")
203 future.state.SchemaVersion = desktopLifecycleSchemaVersion + 1
204 future.state.PID = 0
205 future.state.Phase = ""
206 if err := future.start(); err != nil {
207 t.Fatal(err)
208 }
209 future.stopWriter()
210 old := time.Now().UTC().Add(-2 * desktopLifecycleRetention)
211 if err := os.Chtimes(future.path, old, old); err != nil {
212 t.Fatal(err)
213 }
214
215 reader := lifecycleTrackerForTest(t, root, os.Getpid(), "reader")
216 reader.now = func() time.Time { return time.Now().UTC() }
217 if got := reader.consumePrevious(true); len(got) != 0 {
218 t.Fatalf("future lifecycle record observed: %+v", got)
219 }
220 if _, err := os.Stat(future.path); err != nil {
221 t.Fatalf("future lifecycle record without v2 fields was pruned: %v", err)
222 }
223 }
224
225 func TestDesktopLifecycleCleanRemovesCurrentRecord(t *testing.T) {
226 tracker := lifecycleTrackerForTest(t, t.TempDir(), os.Getpid(), "current")
227 base := time.Date(2026, 8, 10, 1, 0, 0, 0, time.UTC)
228 tracker.now = func() time.Time { return base }
229 if err := tracker.start(); err != nil {
230 t.Fatal(err)
231 }
232 tracker.mark("shutting_down")
233 state, err := readDesktopLifecycleState(tracker.path)
234 if err != nil || state.Phase != "shutting_down" || state.UpdatedAt != base.Format(time.RFC3339Nano) {
235 t.Fatalf("state = %+v err=%v", state, err)
236 }
237 tracker.clean()
238 if _, err := os.Stat(tracker.path); !os.IsNotExist(err) {
239 t.Fatalf("clean lifecycle record remains: %v", err)
240 }
241 }
242
243 func TestDesktopLifecycleAsyncWriterKeepsLatestPhase(t *testing.T) {
244 tracker := lifecycleTrackerForTest(t, t.TempDir(), os.Getpid(), "async")
245 if err := tracker.start(); err != nil {
246 t.Fatal(err)
247 }
248 tracker.markAsync("ready")
249 tracker.markAsync("healthy")
250 tracker.stopWriter()
251 // stopWriter leaves after 250ms so quitting never blocks on diagnostic I/O.
252 // That budget is not a flush guarantee — one atomic replace on a loaded
253 // Windows runner has taken 500ms — so wait for the drain before reading.
254 select {
255 case <-tracker.writerDone:
256 case <-time.After(30 * time.Second):
257 t.Fatal("lifecycle writer never drained")
258 }
259
260 state, err := readDesktopLifecycleState(tracker.path)
261 if err != nil || state.Phase != "healthy" {
262 t.Fatalf("async lifecycle state = %+v err=%v", state, err)
263 }
264 }
265
266 func TestDesktopShutdownPanicPreservesLifecycleRecord(t *testing.T) {
267 tracker := lifecycleTrackerForTest(t, t.TempDir(), os.Getpid(), "shutdown-panic")
268 if err := tracker.start(); err != nil {
269 t.Fatal(err)
270 }
271
272 func() {
273 defer func() { _ = recover() }()
274 completeDesktopShutdown(tracker, func() { panic("teardown failed") })
275 }()
276 state, err := readDesktopLifecycleState(tracker.path)
277 if err != nil || state.Phase != "shutting_down" {
278 t.Fatalf("panic lifecycle state = %+v err=%v", state, err)
279 }
280 }
281
281 lines GO