返回 DeepSeek-Reasonix
startup_test.go
根目录 / internal / repair / startup_test.go
1 package repair
2
3 import (
4 "os"
5 "path/filepath"
6 "sync"
7 "sync/atomic"
8 "testing"
9 "time"
10 )
11
12 func writeLegacyStartupState(t *testing.T, path, body string) {
13 t.Helper()
14 if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
15 t.Fatal(err)
16 }
17 }
18
19 func TestStartupTrackerObservesDeadLegacyOwner(t *testing.T) {
20 path := filepath.Join(t.TempDir(), "startup.json")
21 started := time.Now().UTC().Add(-time.Minute)
22 updated := started.Add(45 * time.Second)
23 writeLegacyStartupState(t, path, `{
24 "schemaVersion": 1,
25 "phase": "healthy",
26 "version": "v1.19.1",
27 "installProfile": "installer",
28 "updateFromVersion": "v1.19.0",
29 "updateToVersion": "v1.19.1",
30 "pid": 42,
31 "safeMode": true,
32 "startedAt": "`+started.Format(time.RFC3339Nano)+`",
33 "updatedAt": "`+updated.Format(time.RFC3339Nano)+`"
34 }`)
35
36 tracker := NewStartupTracker(path)
37 tracker.processAlive = func(int) bool { return false }
38 got := tracker.ObservePreviousRun()
39 if !got.Abnormal || got.Phase != "healthy" || got.Version != "v1.19.1" || got.InstallProfile != "installer" {
40 t.Fatalf("observation = %+v", got)
41 }
42 if got.UpdateFrom != "v1.19.0" || got.UpdateTo != "v1.19.1" || got.UptimeBucket != "m_0_2" {
43 t.Fatalf("observation metadata = %+v", got)
44 }
45 if got := tracker.ObservePreviousRun(); got.Abnormal {
46 t.Fatalf("claimed legacy record replayed: %+v", got)
47 }
48 }
49
50 func TestStartupTrackerIgnoresLiveAndCleanLegacyRecords(t *testing.T) {
51 path := filepath.Join(t.TempDir(), "startup.json")
52 tracker := NewStartupTracker(path)
53 tracker.processAlive = func(pid int) bool { return pid == 42 }
54
55 writeLegacyStartupState(t, path, `{"phase":"ready","pid":42}`)
56 if got := tracker.ObservePreviousRun(); got.Abnormal {
57 t.Fatalf("live owner reported abnormal: %+v", got)
58 }
59 writeLegacyStartupState(t, path, `{"phase":"clean-exit","pid":42}`)
60 if got := tracker.ObservePreviousRun(); got.Abnormal {
61 t.Fatalf("clean exit reported abnormal: %+v", got)
62 }
63 if _, err := os.Stat(path); !os.IsNotExist(err) {
64 t.Fatalf("clean legacy record was not consumed: %v", err)
65 }
66 }
67
68 func TestStartupTrackerConcurrentClaimReportsOnce(t *testing.T) {
69 path := filepath.Join(t.TempDir(), "startup.json")
70 writeLegacyStartupState(t, path, `{"phase":"healthy","version":"v1.19.1","pid":42}`)
71
72 const observers = 16
73 start := make(chan struct{})
74 var ready sync.WaitGroup
75 var done sync.WaitGroup
76 var reports atomic.Int32
77 for range observers {
78 ready.Add(1)
79 done.Go(func() {
80 tracker := NewStartupTracker(path)
81 tracker.processAlive = func(int) bool { return false }
82 ready.Done()
83 <-start
84 if tracker.ObservePreviousRun().Abnormal {
85 reports.Add(1)
86 }
87 })
88 }
89 ready.Wait()
90 close(start)
91 done.Wait()
92 if got := reports.Load(); got != 1 {
93 t.Fatalf("concurrent reports = %d, want 1", got)
94 }
95 }
96
97 func TestStartupTrackerInvalidOrMissingStateIsIgnored(t *testing.T) {
98 path := filepath.Join(t.TempDir(), "startup.json")
99 tracker := NewStartupTracker(path)
100 if got := tracker.ObservePreviousRun(); got.Abnormal {
101 t.Fatalf("missing state reported abnormal: %+v", got)
102 }
103 writeLegacyStartupState(t, path, `{broken`)
104 if got := tracker.ObservePreviousRun(); got.Abnormal {
105 t.Fatalf("invalid state reported abnormal: %+v", got)
106 }
107 }
108
108 lines GO