返回 DeepSeek-Reasonix
session_sidecar_durability_test.go
根目录 / desktop / session_sidecar_durability_test.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "runtime"
11 "strings"
12 "sync"
13 "testing"
14 "time"
15
16 "reasonix/internal/filelock"
17 "reasonix/internal/fileutil"
18 )
19
20 func TestSessionSidecarSavesUseDurableAtomicWrite(t *testing.T) {
21 tests := []struct {
22 name string
23 path func(string) string
24 save func(string) error
25 }{
26 {
27 name: "titles",
28 path: sessionTitlesPath,
29 save: func(dir string) error {
30 return saveSessionTitles(dir, map[string]string{"session.jsonl": "Durable title"})
31 },
32 },
33 {
34 name: "displays",
35 path: sessionDisplayPath,
36 save: func(dir string) error {
37 return saveSessionDisplays(dir, sessionDisplayMap{
38 "session.jsonl": {messageDisplayKey("expanded prompt"): "visible prompt"},
39 })
40 },
41 },
42 {
43 name: "planner displays",
44 path: sessionPlannerDisplayPath,
45 save: func(dir string) error {
46 return saveSessionPlannerDisplays(dir, sessionPlannerDisplayMap{
47 "session.jsonl": {{UserHash: "prompt-digest", Messages: []HistoryMessage{}}},
48 })
49 },
50 },
51 }
52
53 for _, tt := range tests {
54 t.Run(tt.name, func(t *testing.T) {
55 dir := t.TempDir()
56 path := tt.path(dir)
57 previousCrashPoint := fileutil.CrashPoint
58 t.Cleanup(func() { fileutil.CrashPoint = previousCrashPoint })
59 usedDurableWrite := false
60 fileutil.CrashPoint = func(op, gotPath string) {
61 if op == "atomic-write" && gotPath == path {
62 usedDurableWrite = true
63 }
64 }
65
66 if err := tt.save(dir); err != nil {
67 t.Fatalf("save %s sidecar: %v", tt.name, err)
68 }
69 if !usedDurableWrite {
70 t.Fatalf("%s sidecar bypassed fileutil.AtomicWriteFile", tt.name)
71 }
72 if runtime.GOOS != "windows" {
73 info, err := os.Stat(path)
74 if err != nil {
75 t.Fatalf("stat %s sidecar: %v", tt.name, err)
76 }
77 if got := info.Mode().Perm(); got != 0o600 {
78 t.Fatalf("%s sidecar mode = %o, want 600", tt.name, got)
79 }
80 }
81 })
82 }
83 }
84
85 func TestSetSessionTitleHonorsSidecarLock(t *testing.T) {
86 dir := t.TempDir()
87 release, err := filelock.Acquire(context.Background(), sessionTitlesPath(dir)+".lock")
88 if err != nil {
89 t.Fatalf("acquire title sidecar lock: %v", err)
90 }
91 previousTimeout := sessionTitlesQueueTimeout
92 sessionTitlesQueueTimeout = 40 * time.Millisecond
93 t.Cleanup(func() { sessionTitlesQueueTimeout = previousTimeout })
94
95 err = setSessionTitle(dir, filepath.Join(dir, "locked.jsonl"), "Locked")
96 if err == nil {
97 release()
98 t.Fatal("setSessionTitle succeeded while the title sidecar lock was held")
99 }
100 release()
101 if err := setSessionTitle(dir, filepath.Join(dir, "locked.jsonl"), "Saved"); err != nil {
102 t.Fatalf("setSessionTitle after release: %v", err)
103 }
104 }
105
106 func TestRecordSessionDisplayExternalLockUsesShortBudget(t *testing.T) {
107 if os.Getenv("REASONIX_DISPLAY_LOCK_HELPER") != "" {
108 dir := os.Getenv("REASONIX_DISPLAY_LOCK_DIR")
109 release, err := filelock.Acquire(context.Background(), sessionDisplayPath(dir)+".lock")
110 if err != nil {
111 t.Fatalf("acquire external display lock: %v", err)
112 }
113 defer release()
114 if err := os.WriteFile(filepath.Join(dir, "display-lock.ready"), []byte("ready"), 0o600); err != nil {
115 t.Fatal(err)
116 }
117 if !waitForPlannerDisplayTestFile(filepath.Join(dir, "display-lock.release"), 10*time.Second) {
118 t.Fatal("timed out waiting to release external display lock")
119 }
120 return
121 }
122
123 dir := t.TempDir()
124 var output strings.Builder
125 cmd := exec.Command(os.Args[0], "-test.run=^TestRecordSessionDisplayExternalLockUsesShortBudget$")
126 cmd.Env = append(os.Environ(),
127 "REASONIX_DISPLAY_LOCK_HELPER=1",
128 "REASONIX_DISPLAY_LOCK_DIR="+dir,
129 )
130 cmd.Stdout = &output
131 cmd.Stderr = &output
132 if err := cmd.Start(); err != nil {
133 t.Fatalf("start external display-lock helper: %v", err)
134 }
135 releasePath := filepath.Join(dir, "display-lock.release")
136 releaseHelper := func() {
137 _ = os.WriteFile(releasePath, []byte("release"), 0o600)
138 }
139 if !waitForPlannerDisplayTestFile(filepath.Join(dir, "display-lock.ready"), 5*time.Second) {
140 releaseHelper()
141 _ = cmd.Wait()
142 t.Fatalf("external display-lock helper did not start: %s", output.String())
143 }
144
145 previousTimeout := sessionDisplayExternalLockTimeout
146 sessionDisplayExternalLockTimeout = 100 * time.Millisecond
147 t.Cleanup(func() { sessionDisplayExternalLockTimeout = previousTimeout })
148 started := time.Now()
149 err := recordSessionDisplay(dir, filepath.Join(dir, "session.jsonl"), "expanded prompt", "visible prompt")
150 elapsed := time.Since(started)
151 releaseHelper()
152 if waitErr := cmd.Wait(); waitErr != nil {
153 t.Fatalf("external display-lock helper failed: %v\n%s", waitErr, output.String())
154 }
155 if !errors.Is(err, context.DeadlineExceeded) {
156 t.Fatalf("record display error = %v, want external lock deadline exceeded", err)
157 }
158 if elapsed >= 2*time.Second {
159 t.Fatalf("record display waited %v, want the short external lock budget", elapsed)
160 }
161 }
162
163 func TestSetSessionTitleSerializesConcurrentUpdates(t *testing.T) {
164 dir := t.TempDir()
165 const sessions = 32
166 errs := make(chan error, sessions)
167 var wg sync.WaitGroup
168 for i := range sessions {
169 wg.Add(1)
170 go func(i int) {
171 defer wg.Done()
172 path := filepath.Join(dir, fmt.Sprintf("session-%02d.jsonl", i))
173 errs <- setSessionTitle(dir, path, fmt.Sprintf("Title %02d", i))
174 }(i)
175 }
176 wg.Wait()
177 close(errs)
178 for err := range errs {
179 if err != nil {
180 t.Fatalf("setSessionTitle: %v", err)
181 }
182 }
183
184 titles := loadSessionTitles(dir)
185 if len(titles) != sessions {
186 t.Fatalf("title count = %d, want %d: %#v", len(titles), sessions, titles)
187 }
188 for i := range sessions {
189 key := fmt.Sprintf("session-%02d.jsonl", i)
190 if got := titles[key]; got != fmt.Sprintf("Title %02d", i) {
191 t.Fatalf("%s title = %q, want retained concurrent value", key, got)
192 }
193 }
194 }
195
195 lines GO