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