返回 DeepSeek-Reasonix
inbox_target_test.go
根目录 / desktop / inbox_target_test.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "net/http"
7 "net/http/httptest"
8 "path/filepath"
9 "strings"
10 "testing"
11
12 "reasonix/internal/agent"
13 "reasonix/internal/config"
14 "reasonix/internal/control"
15 "reasonix/internal/event"
16 "reasonix/internal/serve"
17 "reasonix/internal/session"
18 "reasonix/internal/tool"
19 )
20
21 func TestCanonicalInboxTargetEnqueuesAndConfirmsReceipt(t *testing.T) {
22 isolateDesktopUserDirs(t)
23 service, err := session.NewService("local", session.NewFilesystemPersistence(t.TempDir()))
24 if err != nil {
25 t.Fatal(err)
26 }
27 executor := agent.New(nil, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard)
28 ctrl := control.New(control.Options{Executor: executor, SessionService: service, Sink: event.Discard})
29 t.Cleanup(func() {
30 ctrl.Close()
31 <-ctrl.Closed()
32 _ = service.CloseAll(context.Background())
33 })
34 if _, err := ctrl.BindFreshSession(t.Context(), "queue-target"); err != nil {
35 t.Fatal(err)
36 }
37 if err := ctrl.SetInboxPaused(true); err != nil {
38 t.Fatal(err)
39 }
40 const path = "session-id:queue-target"
41 a := &App{tabs: map[string]*WorkspaceTab{"tab": {ID: "tab", Ctrl: ctrl, SessionID: "queue-target", SessionGeneration: 1, Ready: true}}}
42 target, err := a.CaptureInboxTarget("tab", path)
43 if err != nil {
44 t.Fatal(err)
45 }
46 receipt, err := a.EnqueueInboxFollowupForTarget(target, "queued input", "queued input", nil, "request")
47 if err != nil || receipt.ItemID == "" {
48 t.Fatalf("canonical enqueue: %+v %v", receipt, err)
49 }
50 queue, err := a.InboxQueueForTarget(target, control.InboxQueueRequest{Kind: "enqueue_steer", TurnID: "finished-turn", Text: "guidance", Display: "guidance", IdempotencyKey: "guidance-request"})
51 if err != nil || queue.Receipt == nil || queue.Receipt.ItemID == "" {
52 t.Fatalf("canonical guidance: %+v %v", queue, err)
53 }
54 confirmed, err := a.LookupInboxFollowupForTarget(target, "request")
55 if err != nil || confirmed.ItemID != receipt.ItemID {
56 t.Fatalf("canonical confirmation: %+v %v", confirmed, err)
57 }
58 a.tabs["tab"].SessionGeneration++
59 if _, err := a.EnqueueInboxFollowupForTarget(target, "stale input", "stale input", nil, "stale"); err == nil {
60 t.Fatal("stale generation accepted a new write")
61 }
62 if confirmed, err := a.LookupInboxFollowupForTarget(target, "request"); err != nil || confirmed.ItemID != receipt.ItemID {
63 t.Fatalf("same-session receipt after rebind: %+v %v", confirmed, err)
64 }
65 }
66
67 func TestRemoteInboxTargetLostReceiptOnlyQueriesOriginalRequest(t *testing.T) {
68 isolateDesktopUserDirs(t)
69 dir := t.TempDir()
70 path := filepath.Join(dir, "session.jsonl")
71 ctrl := control.New(control.Options{SessionDir: dir, SessionPath: path, Sink: event.Discard})
72 defer ctrl.Close()
73 if err := ctrl.SetInboxPaused(true); err != nil {
74 t.Fatal(err)
75 }
76 server := httptest.NewServer(operatorServeHandler(serve.New(ctrl, nil, config.ServeConfig{})))
77 defer server.Close()
78 posts, lookups := 0, 0
79 client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
80 if req.Method == http.MethodPost {
81 posts++
82 resp, err := http.DefaultTransport.RoundTrip(req)
83 if err != nil {
84 return nil, err
85 }
86 resp.Body.Close()
87 return nil, errors.New("accepted POST reply lost")
88 }
89 lookups++
90 if req.URL.Query().Get("session") != path || req.URL.Query().Get("key") != "original-request" {
91 t.Errorf("receipt request changed identity: %s", req.URL.Path)
92 }
93 if lookups == 1 {
94 return nil, errors.New("first receipt response lost")
95 }
96 return http.DefaultTransport.RoundTrip(req)
97 })}
98 a, tab := remoteRuntimeTestApp(client)
99 tab.base, tab.routing.currentPath, tab.session.path = server.URL, path, path
100 target, err := a.CaptureInboxTarget(tab.id, path)
101 if err != nil {
102 t.Fatal(err)
103 }
104 if _, err := a.EnqueueInboxFollowupForTarget(target, "original input", "original input", nil, "original-request"); err == nil {
105 t.Fatal("expected uncertain response")
106 }
107 if len(ctrl.InboxSnapshot().Items) != 1 {
108 t.Fatal("POST was not durably accepted")
109 }
110 for range 3 {
111 receipt, err := a.LookupInboxFollowupForTarget(target, "original-request")
112 if err != nil || receipt.ItemID == "" {
113 t.Fatalf("lookup = %+v, %v", receipt, err)
114 }
115 }
116 if posts != 1 || lookups != 4 || len(ctrl.InboxSnapshot().Items) != 1 {
117 t.Fatalf("lookup replayed POST: posts=%d lookups=%d", posts, lookups)
118 }
119 for _, change := range []string{"selection", "generation", "path", "host"} {
120 t.Run(change, func(t *testing.T) {
121 oldGen, oldSelection, oldPath, oldHost := tab.gen, tab.selectionRevision, tab.routing.currentPath, tab.ref.HostID
122 defer func() {
123 tab.gen, tab.selectionRevision, tab.routing.currentPath, tab.ref.HostID = oldGen, oldSelection, oldPath, oldHost
124 }()
125 switch change {
126 case "selection":
127 tab.selectionRevision++
128 case "generation":
129 tab.gen++
130 case "path":
131 tab.routing.currentPath = "other"
132 case "host":
133 tab.ref.HostID = "different-host"
134 }
135 before := lookups
136 _, lookupErr := a.LookupInboxFollowupForTarget(target, "original-request")
137 if change == "path" || change == "host" {
138 if lookupErr == nil || lookups != before {
139 t.Fatal("lookup accepted different session or host")
140 }
141 } else if lookupErr != nil || lookups != before+1 {
142 t.Fatalf("same-session read did not recover after reconnect: %v", lookupErr)
143 }
144 if _, err := a.EnqueueInboxFollowupForTarget(target, "new", "new", nil, "new"); err == nil || !strings.Contains(err.Error(), "inbox_target_transient") {
145 t.Fatalf("stale target was not explicitly rejected: %v", err)
146 }
147 if posts != 1 {
148 t.Fatal("stale write target crossed network boundary")
149 }
150 })
151 }
152 }
153
154 func TestLocalInboxTargetRejectsReplacementAndPreservesReceipt(t *testing.T) {
155 isolateDesktopUserDirs(t)
156 dir := t.TempDir()
157 path := filepath.Join(dir, "session.jsonl")
158 ctrl := control.New(control.Options{SessionDir: dir, SessionPath: path, Sink: event.Discard})
159 defer ctrl.Close()
160 if err := ctrl.SetInboxPaused(true); err != nil {
161 t.Fatal(err)
162 }
163 a := &App{tabs: map[string]*WorkspaceTab{"tab": {ID: "tab", Ctrl: ctrl, SessionPath: path, SessionGeneration: 1, Ready: true}}}
164 target, err := a.CaptureInboxTarget("tab", path)
165 if err != nil {
166 t.Fatal(err)
167 }
168 receipt, err := a.EnqueueInboxFollowupForTarget(target, "input", "input", nil, "local-request")
169 if err != nil || receipt.ItemID == "" {
170 t.Fatalf("enqueue = %+v, %v", receipt, err)
171 }
172 confirmed, err := a.LookupInboxFollowupForTarget(target, "local-request")
173 if err != nil || confirmed.ItemID != receipt.ItemID {
174 t.Fatalf("lookup = %+v, %v", confirmed, err)
175 }
176 if _, err := a.LookupInboxFollowupForTarget(target, "unknown"); err == nil {
177 t.Fatal("missing receipt reported as confirmed")
178 }
179 a.tabs["tab"].SessionGeneration++
180 if _, err := a.EnqueueInboxFollowupForTarget(target, "replacement", "replacement", nil, "replacement"); err == nil {
181 t.Fatal("replacement accepted old request")
182 }
183 if got, err := a.LookupInboxFollowupForTarget(target, "local-request"); err != nil || got.ItemID != receipt.ItemID {
184 t.Fatalf("same-session read could not rebind: %+v %v", got, err)
185 }
186 if len(ctrl.InboxSnapshot().Items) != 1 {
187 t.Fatal("replacement created another item")
188 }
189 }
190
191 func TestRemoteInboxReceiptRejectsSelectionChangedDuringRead(t *testing.T) {
192 isolateDesktopUserDirs(t)
193 var a *App
194 var tab *remoteTab
195 client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
196 a.remoteTabMu.Lock()
197 tab.selectionRevision++
198 a.remoteTabMu.Unlock()
199 return remoteRuntimeTestResponse(req, 200, `{"itemId":"old-item","position":0,"disposition":"idempotent_hit","paused":false}`), nil
200 })}
201 a, tab = remoteRuntimeTestApp(client)
202 target, err := a.CaptureInboxTarget(tab.id, runtimeRemoteTestPath)
203 if err != nil {
204 t.Fatal(err)
205 }
206 if receipt, err := a.LookupInboxFollowupForTarget(target, "original"); err == nil || receipt.ItemID != "" {
207 t.Fatalf("stale read escaped its selection fence: %+v, %v", receipt, err)
208 }
209 }
210
210 lines GO