返回 DeepSeek-Reasonix
session_rotation_http_test.go
根目录 / internal / serve / session_rotation_http_test.go
1 package serve
2
3 import (
4 "net/http"
5 "net/http/httptest"
6 "path/filepath"
7 "strconv"
8 "strings"
9 "testing"
10
11 "reasonix/internal/agent"
12 "reasonix/internal/config"
13 "reasonix/internal/control"
14 "reasonix/internal/event"
15 "reasonix/internal/eventwire"
16 )
17
18 func TestServePlanDecisionValidatesRequest(t *testing.T) {
19 bc := NewBroadcaster()
20 ctrl := control.New(control.Options{Sink: bc})
21 srv := httptest.NewServer(New(ctrl, bc, config.ServeConfig{}).Handler())
22 defer srv.Close()
23
24 resp, err := http.Post(srv.URL+"/plan-decision", "application/json", strings.NewReader(`{"action":"revise_plan"}`))
25 if err != nil {
26 t.Fatal(err)
27 }
28 resp.Body.Close()
29 if resp.StatusCode != http.StatusBadRequest {
30 t.Errorf("plan decision missing id = %d, want 400", resp.StatusCode)
31 }
32 }
33
34 func TestServeSilentRotationsPublishSessionChanged(t *testing.T) {
35 for _, endpoint := range []string{"/clear", "/new"} {
36 t.Run(endpoint, func(t *testing.T) {
37 bc := NewBroadcaster()
38 exec := agent.New(nil, nil, agent.NewSession("system"), agent.Options{}, bc)
39 ctrl := control.New(control.Options{Executor: exec, Sink: bc, SessionDir: t.TempDir()})
40 ctrl.EnsureSessionPath()
41 oldPath := ctrl.SessionPath()
42 server := newLifecycleTestServer(t, ctrl, bc, config.ServeConfig{})
43 all, stop := bc.SubscribeAll()
44 defer stop()
45 httpServer := httptest.NewServer(server.Handler())
46 defer httpServer.Close()
47
48 resp, err := http.Post(httpServer.URL+endpoint, "application/json", nil)
49 if err != nil {
50 t.Fatal(err)
51 }
52 resp.Body.Close()
53 if resp.StatusCode != http.StatusNoContent {
54 t.Fatalf("%s status = %d, want 204", endpoint, resp.StatusCode)
55 }
56 frame := nextServeProtocolFrame(t, all, func(state eventwire.Event) {
57 if state.SessionPath != agent.CanonicalSessionPath(oldPath) {
58 t.Fatalf("new runtime state preceded session_changed routing barrier: %+v", state)
59 }
60 })
61 if frame.Kind != "session_changed" || !frame.SessionCurrent || !frame.SessionReset || frame.SessionPath == "" || frame.SessionPath == oldPath {
62 t.Fatalf("%s routing frame = %+v, old path %q", endpoint, frame, oldPath)
63 }
64 })
65 }
66 }
67
68 func TestServeResumeBuffersSynchronousEventsUntilRoutePublication(t *testing.T) {
69 dir := t.TempDir()
70 active := filepath.Join(dir, "active.jsonl")
71 target := filepath.Join(dir, "target.jsonl")
72 saveServeTestSession(t, active)
73 saveServeTestSession(t, target)
74
75 bc := NewBroadcaster()
76 tag := NewSessionTagSink(bc)
77 tag.SetPath(active)
78 ctrl := control.New(control.Options{Sink: tag, SessionDir: dir, SessionPath: active})
79 defer ctrl.Close()
80 server := New(ctrl, bc, config.ServeConfig{})
81 server.RegisterSessionTag(ctrl, tag)
82 all, stop := bc.SubscribeAll()
83 defer stop()
84 resumeBindHookForTest = func() {
85 tag.Emit(event.Event{Kind: event.Notice, Text: "synchronous resume warning"})
86 }
87 defer func() { resumeBindHookForTest = nil }()
88
89 httpServer := httptest.NewServer(server.Handler())
90 defer httpServer.Close()
91 payload := `{"path":` + strconv.Quote(target) + `}`
92 resp, err := http.Post(httpServer.URL+"/resume", "application/json", strings.NewReader(payload))
93 if err != nil {
94 t.Fatal(err)
95 }
96 resp.Body.Close()
97 if resp.StatusCode != http.StatusNoContent {
98 t.Fatalf("resume status = %d, want 204", resp.StatusCode)
99 }
100
101 canonicalTarget := agent.CanonicalSessionPath(target)
102 for _, wantKind := range []string{"notice", "session_changed"} {
103 frame := nextServeProtocolFrame(t, all, func(state eventwire.Event) {
104 if state.SessionPath != agent.CanonicalSessionPath(active) {
105 t.Fatalf("resumed runtime state preceded session_changed routing barrier: %+v", state)
106 }
107 })
108 if frame.Kind != wantKind || frame.SessionPath != canonicalTarget || !frame.SessionCurrent {
109 t.Fatalf("resumed %s frame = %+v, want target-tagged foreground frame", wantKind, frame)
110 }
111 }
112 }
113
114 func TestServeClearSessionEndpoint(t *testing.T) {
115 bc := NewBroadcaster()
116 ctrl := control.New(control.Options{Sink: bc, SessionDir: t.TempDir()})
117 srv := httptest.NewServer(newLifecycleTestServer(t, ctrl, bc, config.ServeConfig{}).Handler())
118 defer srv.Close()
119
120 resp, err := http.Post(srv.URL+"/clear", "application/json", nil)
121 if err != nil {
122 t.Fatal(err)
123 }
124 resp.Body.Close()
125 if resp.StatusCode != http.StatusNoContent {
126 t.Errorf("clear session = %d, want 204", resp.StatusCode)
127 }
128 if got := resp.Header.Get(sessionPathHeader); got == "" || got != ctrl.SessionPath() {
129 t.Errorf("clear session path header = %q, controller path %q", got, ctrl.SessionPath())
130 }
131 }
132
133 func TestServeSubmitClearCompletesRotationBeforeReturning(t *testing.T) {
134 bc := NewBroadcaster()
135 ctrl := control.New(control.Options{Sink: bc, SessionDir: t.TempDir()})
136 ctrl.EnsureSessionPath()
137 srv := httptest.NewServer(newLifecycleTestServer(t, ctrl, bc, config.ServeConfig{}).Handler())
138 defer srv.Close()
139
140 resp, err := http.Post(srv.URL+"/submit", "application/json", strings.NewReader(`{"input":"/clear"}`))
141 if err != nil {
142 t.Fatal(err)
143 }
144 resp.Body.Close()
145 if resp.StatusCode != http.StatusNoContent {
146 t.Fatalf("submit clear status = %d, want 204", resp.StatusCode)
147 }
148 if got := resp.Header.Get(sessionPathHeader); got == "" || got != ctrl.SessionPath() {
149 t.Fatalf("submit clear returned path %q, controller path %q", got, ctrl.SessionPath())
150 }
151 }
152
153 func TestServeNewSessionEndpoint(t *testing.T) {
154 bc := NewBroadcaster()
155 ctrl := control.New(control.Options{Sink: bc, SessionDir: t.TempDir()})
156 srv := httptest.NewServer(newLifecycleTestServer(t, ctrl, bc, config.ServeConfig{}).Handler())
157 defer srv.Close()
158 resp, err := http.Post(srv.URL+"/new", "application/json", nil)
159 if err != nil {
160 t.Fatal(err)
161 }
162 resp.Body.Close()
163 if resp.StatusCode != http.StatusNoContent {
164 t.Errorf("new session = %d, want 204", resp.StatusCode)
165 }
166 if got := resp.Header.Get(sessionPathHeader); got == "" || got != ctrl.SessionPath() {
167 t.Errorf("new session path header = %q, controller path %q", got, ctrl.SessionPath())
168 }
169 }
170
171 func TestServeManagementSubmitReturnsNoContent(t *testing.T) {
172 bc := NewBroadcaster()
173 ctrl := control.New(control.Options{Sink: bc})
174 srv := httptest.NewServer(New(ctrl, bc, config.ServeConfig{}).Handler())
175 defer srv.Close()
176
177 resp, err := http.Post(srv.URL+"/submit", "application/json", strings.NewReader(`{"input":"/context"}`))
178 if err != nil {
179 t.Fatal(err)
180 }
181 resp.Body.Close()
182 if resp.StatusCode != http.StatusNoContent {
183 t.Errorf("management submit = %d, want 204", resp.StatusCode)
184 }
185 }
186
186 lines GO