返回 DeepSeek-Reasonix
session_restore_conflict_test.go
根目录 / desktop / session_restore_conflict_test.go
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8 "time"
9
10 "reasonix/internal/agent"
11 "reasonix/internal/config"
12 "reasonix/internal/sessioninbox"
13 "reasonix/internal/store"
14 )
15
16 func TestRestoreTrashedSessionFileRejectsActiveInboxConflict(t *testing.T) {
17 dir := t.TempDir()
18 sessionPath := filepath.Join(dir, "session.jsonl")
19 if err := os.WriteFile(sessionPath, []byte("trash data"), 0o644); err != nil {
20 t.Fatal(err)
21 }
22 oldReceipt := enqueueSessionInboxForRestoreTest(t, sessionPath, "old queued work")
23 if err := deleteSessionFile(dir, sessionPath); err != nil {
24 t.Fatalf("trash: %v", err)
25 }
26
27 trashPath := filepath.Join(dir, sessionTrashDir, filepath.Base(sessionPath), filepath.Base(sessionPath))
28 if err := os.WriteFile(sessionPath, nil, 0o644); err != nil {
29 t.Fatalf("write live stub: %v", err)
30 }
31 activeReceipt := enqueueSessionInboxForRestoreTest(t, sessionPath, "active queued work")
32
33 err := restoreTrashedSessionFile(dir, trashPath)
34 if err == nil || !strings.Contains(err.Error(), "session artifact already exists: session.inbox") {
35 t.Fatalf("restore conflict error = %v, want active inbox conflict", err)
36 }
37 if info, err := os.Stat(sessionPath); err != nil || info.Size() != 0 {
38 t.Fatalf("active transcript stub should remain empty, info=%v err=%v", info, err)
39 }
40 assertSessionInboxItemForRestoreTest(t, sessionPath, activeReceipt.ItemID, "active queued work")
41 if _, err := os.Stat(trashPath); err != nil {
42 t.Fatalf("trash transcript should remain after rejected restore: %v", err)
43 }
44 assertSessionInboxItemForRestoreTest(t, trashPath, oldReceipt.ItemID, "old queued work")
45 }
46
47 func TestRestoreTrashedSessionFileRejectsOrphanSidecarConflict(t *testing.T) {
48 dir := t.TempDir()
49 sessionPath := filepath.Join(dir, "orphan-sidecar.jsonl")
50 if err := os.WriteFile(sessionPath, []byte("trash data"), 0o644); err != nil {
51 t.Fatal(err)
52 }
53 if err := deleteSessionFile(dir, sessionPath); err != nil {
54 t.Fatalf("trash: %v", err)
55 }
56 trashPath := filepath.Join(dir, sessionTrashDir, filepath.Base(sessionPath), filepath.Base(sessionPath))
57 if err := os.WriteFile(sessionPath, nil, 0o644); err != nil {
58 t.Fatalf("write live stub: %v", err)
59 }
60 metaPath := store.SessionMeta(sessionPath)
61 if err := os.WriteFile(metaPath, []byte("active metadata"), 0o600); err != nil {
62 t.Fatalf("write active sidecar: %v", err)
63 }
64
65 err := restoreTrashedSessionFile(dir, trashPath)
66 if err == nil || !strings.Contains(err.Error(), filepath.Base(metaPath)) {
67 t.Fatalf("restore conflict error = %v, want orphan sidecar conflict", err)
68 }
69 if got, err := os.ReadFile(metaPath); err != nil || string(got) != "active metadata" {
70 t.Fatalf("active sidecar = %q, %v; want preserved metadata", got, err)
71 }
72 if _, err := os.Stat(sessionPath); err != nil {
73 t.Fatalf("active transcript stub should remain: %v", err)
74 }
75 if _, err := os.Stat(trashPath); err != nil {
76 t.Fatalf("trash transcript should remain after rejected restore: %v", err)
77 }
78 }
79
80 func TestRestoreTrashedSessionFilePreservesStubOnSubagentConflict(t *testing.T) {
81 dir := t.TempDir()
82 sessionPath := filepath.Join(dir, "subagent-conflict.jsonl")
83 if err := os.WriteFile(sessionPath, []byte("trash data"), 0o644); err != nil {
84 t.Fatal(err)
85 }
86 ref := "sa_20260102_030405_000000000_aabbccddeeff"
87 writeSubagentArtifact(t, dir, ref, agent.BranchID(sessionPath))
88 if err := deleteSessionFile(dir, sessionPath); err != nil {
89 t.Fatalf("trash: %v", err)
90 }
91 trashPath := filepath.Join(dir, sessionTrashDir, filepath.Base(sessionPath), filepath.Base(sessionPath))
92 if err := os.WriteFile(sessionPath, nil, 0o644); err != nil {
93 t.Fatalf("write live stub: %v", err)
94 }
95 activeSubagent := filepath.Join(dir, "subagents", ref+".jsonl")
96 if err := os.MkdirAll(filepath.Dir(activeSubagent), 0o755); err != nil {
97 t.Fatal(err)
98 }
99 if err := os.WriteFile(activeSubagent, []byte("active subagent"), 0o600); err != nil {
100 t.Fatal(err)
101 }
102
103 if err := restoreTrashedSessionFile(dir, trashPath); err == nil {
104 t.Fatal("restore should reject an active subagent conflict")
105 }
106 if _, err := os.Stat(sessionPath); err != nil {
107 t.Fatalf("active transcript stub should survive preflight failure: %v", err)
108 }
109 if got, err := os.ReadFile(activeSubagent); err != nil || string(got) != "active subagent" {
110 t.Fatalf("active subagent = %q, %v; want preserved data", got, err)
111 }
112 if _, err := os.Stat(trashPath); err != nil {
113 t.Fatalf("trash transcript should remain after rejected restore: %v", err)
114 }
115 }
116
117 func TestLegacyRestoreSessionFinishesCommittedPartialTrashMove(t *testing.T) {
118 isolateDesktopUserDirs(t)
119
120 dir := config.SessionDir()
121 if err := os.MkdirAll(dir, 0o755); err != nil {
122 t.Fatalf("mkdir sessions: %v", err)
123 }
124 sessionPath := filepath.Join(dir, "partial-restore.jsonl")
125 itemDir := filepath.Join(sessionTrashPath(dir), filepath.Base(sessionPath))
126 if err := os.MkdirAll(itemDir, 0o755); err != nil {
127 t.Fatalf("mkdir trash item: %v", err)
128 }
129 trashPath := filepath.Join(itemDir, filepath.Base(sessionPath))
130 const transcript = `{"role":"user","content":"trashed transcript"}` + "\n"
131 if err := os.WriteFile(trashPath, []byte(transcript), 0o600); err != nil {
132 t.Fatalf("write trashed transcript: %v", err)
133 }
134 metaPath := store.SessionMeta(sessionPath)
135 if err := os.WriteFile(metaPath, []byte(`{"scope":"global","topic_id":"topic_partial","topic_title":"Partial"}`), 0o600); err != nil {
136 t.Fatalf("write leftover live metadata: %v", err)
137 }
138 if err := agent.MarkCleanupPending(sessionPath, "delete"); err != nil {
139 t.Fatalf("mark cleanup pending: %v", err)
140 }
141
142 app := NewApp()
143 t.Cleanup(func() { app.stopSessionCatalog(time.Second) })
144 if err := app.restoreSession(trashPath); err != nil {
145 t.Fatalf("restore partial trash move: %v", err)
146 }
147 if got, err := os.ReadFile(sessionPath); err != nil || string(got) != transcript {
148 t.Fatalf("restored transcript = %q, %v", got, err)
149 }
150 if meta, ok, err := agent.LoadBranchMeta(sessionPath); err != nil || !ok || meta.TopicID != "topic_partial" {
151 t.Fatalf("restored metadata = %+v, %v, %v", meta, ok, err)
152 }
153 if agent.IsCleanupPending(sessionPath) {
154 t.Fatal("restore retained cleanup-pending marker")
155 }
156 if _, err := os.Stat(itemDir); !os.IsNotExist(err) {
157 t.Fatalf("trash item survived restore: %v", err)
158 }
159 }
160
161 func enqueueSessionInboxForRestoreTest(t *testing.T, sessionPath, text string) sessioninbox.InboxReceipt {
162 t.Helper()
163 inbox, err := sessioninbox.Open(sessionPath, sessioninbox.Limits{})
164 if err != nil {
165 t.Fatalf("open inbox: %v", err)
166 }
167 receipt, err := inbox.Enqueue(sessioninbox.EnqueueRequest{
168 Envelope: sessioninbox.PromptEnvelope{DisplayText: text, SubmitText: text},
169 })
170 inbox.Close()
171 if err != nil {
172 t.Fatalf("enqueue inbox item: %v", err)
173 }
174 return receipt
175 }
176
177 func assertSessionInboxItemForRestoreTest(t *testing.T, sessionPath, itemID, want string) {
178 t.Helper()
179 inbox, err := sessioninbox.Open(sessionPath, sessioninbox.Limits{})
180 if err != nil {
181 t.Fatalf("reopen inbox: %v", err)
182 }
183 defer inbox.Close()
184 _, envelope, err := inbox.ReadItem(itemID)
185 if err != nil {
186 t.Fatalf("read inbox item %s: %v", itemID, err)
187 }
188 if envelope.SubmitText != want {
189 t.Fatalf("inbox item %s = %q, want %q", itemID, envelope.SubmitText, want)
190 }
191 }
192
192 lines GO