返回 DeepSeek-Reasonix
attachment_targets_test.go
根目录 / desktop / attachment_targets_test.go
1 package main
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 "sync"
9 "testing"
10 "time"
11
12 "reasonix/internal/control"
13 )
14
15 func TestAttachmentTargetRejectsReplacementBeforeFileUpload(t *testing.T) {
16 root := t.TempDir()
17 c := control.New(control.Options{WorkspaceRoot: root})
18 t.Cleanup(c.Close)
19 tab := &WorkspaceTab{ID: "a", WorkspaceRoot: root, Ctrl: c, SessionGeneration: 1}
20 a := &App{tabs: map[string]*WorkspaceTab{"a": tab}, activeTabID: "a"}
21 target, err := a.CaptureAttachmentTarget(ComposerTarget{Kind: "session", TabID: "a"})
22 if err != nil {
23 t.Fatal(err)
24 }
25 t.Cleanup(func() { a.ReleaseAttachmentTarget(target.Token) })
26 a.mu.Lock()
27 tab.SessionGeneration++
28 a.mu.Unlock()
29 if _, err := a.StageImageForTarget(target.Token, "paste", "shot.png", "image/png", "data:image/png;base64,"+desktopTinyPNG); err == nil {
30 t.Fatal("stale target accepted")
31 }
32 }
33
34 func TestAttachmentTargetKeepsSourceAcrossFocusSwitch(t *testing.T) {
35 root := t.TempDir()
36 c := control.New(control.Options{WorkspaceRoot: root})
37 t.Cleanup(c.Close)
38 tab := &WorkspaceTab{ID: "a", WorkspaceRoot: root, Ctrl: c, SessionGeneration: 1}
39 a := &App{tabs: map[string]*WorkspaceTab{"a": tab, "b": {ID: "b", WorkspaceRoot: t.TempDir()}}, activeTabID: "a"}
40 before, err := os.Getwd()
41 if err != nil {
42 t.Fatal(err)
43 }
44 target, err := a.CaptureAttachmentTarget(ComposerTarget{Kind: "session", TabID: "a"})
45 if err != nil {
46 t.Fatal(err)
47 }
48 t.Cleanup(func() { a.ReleaseAttachmentTarget(target.Token) })
49 a.attachmentIOHook = func() { a.mu.Lock(); a.activeTabID = "b"; a.mu.Unlock() }
50 draft, err := a.StageImageForTarget(target.Token, "paste", "shot.png", "image/png", "data:image/png;base64,"+desktopTinyPNG)
51 if err != nil {
52 t.Fatal(err)
53 }
54 if _, err := a.ReadDraftImageForTarget(target.Token, draft.DraftID); err != nil {
55 t.Fatal(err)
56 }
57 if after, err := os.Getwd(); err != nil || before != after {
58 t.Fatalf("cwd changed: %q %q %v", before, after, err)
59 }
60 }
61
62 func TestAttachmentDraftSurvivesSameSessionRuntimeReplacement(t *testing.T) {
63 root := t.TempDir()
64 opts := control.Options{WorkspaceRoot: root, SessionPath: filepath.Join(root, "session.jsonl")}
65 old := control.New(opts)
66 t.Cleanup(old.Close)
67 tab := &WorkspaceTab{ID: "a", WorkspaceRoot: root, Ctrl: old, SessionGeneration: 1}
68 a := &App{tabs: map[string]*WorkspaceTab{"a": tab}, activeTabID: "a"}
69 target, err := a.CaptureAttachmentTarget(ComposerTarget{Kind: "session", TabID: "a"})
70 if err != nil {
71 t.Fatal(err)
72 }
73 draft, err := a.StageImageForTarget(target.Token, "paste", "shot.png", "image/png", "data:image/png;base64,"+desktopTinyPNG)
74 if err != nil {
75 t.Fatal(err)
76 }
77 next := control.New(opts)
78 t.Cleanup(next.Close)
79 if err := activateReplacementController(old, next); err != nil {
80 t.Fatal(err)
81 }
82 a.mu.Lock()
83 tab.Ctrl = next
84 a.mu.Unlock()
85 a.ReleaseAttachmentTarget(target.Token)
86 fresh, err := a.CaptureAttachmentTarget(ComposerTarget{Kind: "session", TabID: "a"})
87 if err != nil {
88 t.Fatal(err)
89 }
90 t.Cleanup(func() { a.ReleaseAttachmentTarget(fresh.Token) })
91 retried, err := a.StageImageForTarget(fresh.Token, "paste", "shot.png", "image/png", "data:image/png;base64,"+desktopTinyPNG)
92 if err != nil || retried.DraftID != draft.DraftID {
93 t.Fatalf("stage receipt did not survive replacement: %+v %v", retried, err)
94 }
95 rebound, err := a.RebindDraftImageForTarget(fresh.Token, draft.DraftID)
96 if err != nil {
97 t.Fatal(err)
98 }
99 if rebound.DraftID == draft.DraftID {
100 t.Fatal("runtime replacement did not renew draft credential")
101 }
102 again, err := a.RebindDraftImageForTarget(fresh.Token, draft.DraftID)
103 if err != nil || again.DraftID != rebound.DraftID {
104 t.Fatalf("rebind retry changed credential: %+v %v", again, err)
105 }
106 if next.RuntimeStatus().Running {
107 t.Fatal("rebind automatically started a turn")
108 }
109 }
110
111 func TestStageImageForTargetIsIdempotentAndRejectsConflicts(t *testing.T) {
112 root := t.TempDir()
113 c := control.New(control.Options{WorkspaceRoot: root, SessionPath: filepath.Join(root, "session.jsonl")})
114 t.Cleanup(c.Close)
115 tab := &WorkspaceTab{ID: "a", WorkspaceRoot: root, Ctrl: c, SessionGeneration: 1}
116 a := &App{tabs: map[string]*WorkspaceTab{"a": tab}, activeTabID: "a"}
117 target, err := a.CaptureAttachmentTarget(ComposerTarget{Kind: "session", TabID: "a"})
118 if err != nil {
119 t.Fatal(err)
120 }
121 t.Cleanup(func() { a.ReleaseAttachmentTarget(target.Token) })
122 first, err := a.StageImageForTarget(target.Token, "same-operation", "shot.png", "image/png", "data:image/png;base64,"+desktopTinyPNG)
123 if err != nil {
124 t.Fatal(err)
125 }
126 retry, err := a.StageImageForTarget(target.Token, "same-operation", "shot.png", "image/png", "data:image/png;base64,"+desktopTinyPNG)
127 if err != nil || retry != first {
128 t.Fatalf("retry = %+v, %v; want %+v", retry, err, first)
129 }
130 if _, err := a.StageImageForTarget(target.Token, "same-operation", "different.png", "image/png", "data:image/png;base64,"+desktopTinyPNG); err == nil || !strings.Contains(err.Error(), "conflicts") {
131 t.Fatalf("different input conflict = %v", err)
132 }
133 if err := a.ReleaseDraftImageForTarget(target.Token, first.DraftID); err != nil {
134 t.Fatal(err)
135 }
136 afterRelease, err := a.StageImageForTarget(target.Token, "same-operation", "shot.png", "image/png", "data:image/png;base64,"+desktopTinyPNG)
137 if err != nil {
138 t.Fatal(err)
139 }
140 if afterRelease.DraftID == first.DraftID {
141 t.Fatal("released credential retained its staging receipt")
142 }
143 }
144
145 func TestStageImageForTargetConcurrentRetrySharesOneOperation(t *testing.T) {
146 root := t.TempDir()
147 c := control.New(control.Options{WorkspaceRoot: root, SessionPath: filepath.Join(root, "session.jsonl")})
148 t.Cleanup(c.Close)
149 tab := &WorkspaceTab{ID: "a", WorkspaceRoot: root, Ctrl: c, SessionGeneration: 1}
150 a := &App{tabs: map[string]*WorkspaceTab{"a": tab}, activeTabID: "a"}
151 target, err := a.CaptureAttachmentTarget(ComposerTarget{Kind: "session", TabID: "a"})
152 if err != nil {
153 t.Fatal(err)
154 }
155 t.Cleanup(func() { a.ReleaseAttachmentTarget(target.Token) })
156
157 ioEntered := make(chan struct{})
158 joinEntered := make(chan struct{})
159 releaseIO := make(chan struct{})
160 var ioOnce, joinOnce sync.Once
161 a.attachmentIOHook = func() {
162 ioOnce.Do(func() { close(ioEntered) })
163 <-releaseIO
164 }
165 a.attachmentJoinHook = func() { joinOnce.Do(func() { close(joinEntered) }) }
166 type result struct {
167 view DraftImageView
168 err error
169 }
170 firstDone := make(chan result, 1)
171 secondDone := make(chan result, 1)
172 go func() {
173 view, err := a.StageImageForTarget(target.Token, "concurrent", "shot.png", "image/png", "data:image/png;base64,"+desktopTinyPNG)
174 firstDone <- result{view: view, err: err}
175 }()
176 select {
177 case <-ioEntered:
178 case <-time.After(5 * time.Second):
179 t.Fatal("first stage did not reach the I/O barrier")
180 }
181 go func() {
182 view, err := a.StageImageForTarget(target.Token, "concurrent", "shot.png", "image/png", "data:image/png;base64,"+desktopTinyPNG)
183 secondDone <- result{view: view, err: err}
184 }()
185 select {
186 case <-joinEntered:
187 case <-time.After(5 * time.Second):
188 t.Fatal("retry did not join the in-flight operation")
189 }
190 close(releaseIO)
191 first, second := <-firstDone, <-secondDone
192 if first.err != nil || second.err != nil || first.view != second.view {
193 t.Fatalf("concurrent results = %+v / %+v", first, second)
194 }
195 }
196
197 func TestStageImageForTargetBoundsOwnerOperations(t *testing.T) {
198 root := t.TempDir()
199 c := control.New(control.Options{WorkspaceRoot: root, SessionPath: filepath.Join(root, "session.jsonl")})
200 t.Cleanup(c.Close)
201 tab := &WorkspaceTab{ID: "a", WorkspaceRoot: root, Ctrl: c, SessionGeneration: 1}
202 a := &App{tabs: map[string]*WorkspaceTab{"a": tab}, activeTabID: "a"}
203 target, err := a.CaptureAttachmentTarget(ComposerTarget{Kind: "session", TabID: "a"})
204 if err != nil {
205 t.Fatal(err)
206 }
207 t.Cleanup(func() { a.ReleaseAttachmentTarget(target.Token) })
208 for i := range 256 {
209 if _, err := a.StageImageForTarget(target.Token, fmt.Sprintf("operation-%d", i), "shot.png", "image/png", "data:image/png;base64,"+desktopTinyPNG); err != nil {
210 t.Fatalf("operation %d: %v", i, err)
211 }
212 }
213 if _, err := a.StageImageForTarget(target.Token, "operation-overflow", "shot.png", "image/png", "data:image/png;base64,"+desktopTinyPNG); err == nil || !strings.Contains(err.Error(), "too many") {
214 t.Fatalf("overflow = %v", err)
215 }
216 }
217
218 func TestAttachmentTargetCancellationIsIndependent(t *testing.T) {
219 c := control.New(control.Options{WorkspaceRoot: t.TempDir()})
220 t.Cleanup(c.Close)
221 a := &App{tabs: map[string]*WorkspaceTab{"a": {ID: "a", WorkspaceRoot: t.TempDir(), Ctrl: c}}, activeTabID: "a"}
222 one, err := a.CaptureAttachmentTarget(ComposerTarget{Kind: "session", TabID: "a"})
223 if err != nil {
224 t.Fatal(err)
225 }
226 two, err := a.CaptureAttachmentTarget(ComposerTarget{Kind: "session", TabID: "a"})
227 if err != nil {
228 t.Fatal(err)
229 }
230 t.Cleanup(func() { a.ReleaseAttachmentTarget(two.Token) })
231 a.ReleaseAttachmentTarget(one.Token)
232 if _, err := a.StageImageForTarget(one.Token, "one", "a.png", "image/png", "data:image/png;base64,"+desktopTinyPNG); err == nil {
233 t.Fatal("canceled target accepted")
234 }
235 if _, err := a.StageImageForTarget(two.Token, "two", "a.png", "image/png", "data:image/png;base64,"+desktopTinyPNG); err != nil {
236 t.Fatal(err)
237 }
238 }
239
239 lines GO