返回 DeepSeek-Reasonix
runtime_http_test.go
根目录 / internal / serve / runtime_http_test.go
1 package serve
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "io"
8 "net/http"
9 "net/http/httptest"
10 "strings"
11 "testing"
12
13 "reasonix/internal/config"
14 "reasonix/internal/control"
15 "reasonix/internal/jobs"
16 "reasonix/internal/session"
17 )
18
19 type rejectingGoalAPI struct {
20 control.SessionAPI
21 err error
22 }
23
24 func (a *rejectingGoalAPI) SetGoalDurable(string) error { return a.err }
25
26 func postRuntimeJSON(t *testing.T, url, body string) *http.Response {
27 t.Helper()
28 req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(body))
29 if err != nil {
30 t.Fatal(err)
31 }
32 req.Header.Set("Content-Type", "application/json")
33 resp, err := http.DefaultClient.Do(req)
34 if err != nil {
35 t.Fatal(err)
36 }
37 return resp
38 }
39
40 func TestGoalPauseAndResumeRoutes(t *testing.T) {
41 bc := NewBroadcaster()
42 ctrl := control.New(control.Options{Sink: bc})
43 ctrl.SetGoal("ship the remote surface")
44 srv := httptest.NewServer(New(ctrl, bc, config.ServeConfig{}).Handler())
45 defer srv.Close()
46
47 resp := postRuntimeJSON(t, srv.URL+"/goal/pause", `{}`)
48 resp.Body.Close()
49 if resp.StatusCode != http.StatusNoContent || ctrl.GoalStatus() != control.GoalStatusBlocked {
50 t.Fatalf("pause status/goal = %d/%q", resp.StatusCode, ctrl.GoalStatus())
51 }
52 resp = postRuntimeJSON(t, srv.URL+"/goal/resume", `{}`)
53 resp.Body.Close()
54 if resp.StatusCode != http.StatusNoContent || ctrl.GoalStatus() != control.GoalStatusRunning {
55 t.Fatalf("resume status/goal = %d/%q", resp.StatusCode, ctrl.GoalStatus())
56 }
57 }
58
59 func TestGoalRouteReportsPersistenceFailureBeforeChangingPlanMode(t *testing.T) {
60 bc := NewBroadcaster()
61 base := control.New(control.Options{Sink: bc})
62 base.SetPlanMode(true)
63 api := &rejectingGoalAPI{SessionAPI: base, err: errors.New("disk full")}
64 srv := httptest.NewServer(New(api, bc, config.ServeConfig{}).Handler())
65 defer srv.Close()
66 defer base.Close()
67
68 resp := postRuntimeJSON(t, srv.URL+"/goal", `{"goal":"ship it"}`)
69 defer resp.Body.Close()
70 if resp.StatusCode != http.StatusServiceUnavailable {
71 t.Fatalf("goal status = %d, want %d", resp.StatusCode, http.StatusServiceUnavailable)
72 }
73 if !base.PlanMode() || base.Goal() != "" {
74 t.Fatalf("failed goal mutation changed runtime: plan=%v goal=%q", base.PlanMode(), base.Goal())
75 }
76 }
77
78 func TestGoalEditRoutePreservesGoalIdentity(t *testing.T) {
79 bc := NewBroadcaster()
80 service, err := session.NewService("serve", session.NewFilesystemPersistence(t.TempDir()))
81 if err != nil {
82 t.Fatal(err)
83 }
84 t.Cleanup(func() {
85 if err := service.Shutdown(context.Background()); err != nil {
86 t.Errorf("shutdown session service: %v", err)
87 }
88 })
89 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "goal-edit-route"})
90 if err != nil {
91 t.Fatal(err)
92 }
93 ctrl := control.New(control.Options{Sink: bc, SessionService: service, SessionRuntime: runtime, ExclusiveSession: true})
94 defer ctrl.ReleaseResources()
95 if err := ctrl.SetGoalDurable("original"); err != nil {
96 t.Fatal(err)
97 }
98 before, err := ctrl.GetGoal(t.Context())
99 if err != nil || before == nil {
100 t.Fatalf("goal before edit = %+v, %v", before, err)
101 }
102 srv := httptest.NewServer(New(ctrl, bc, config.ServeConfig{}).Handler())
103 defer srv.Close()
104
105 resp := postRuntimeJSON(t, srv.URL+"/goal/edit", `{"objective":"revised","maxGoalRounds":12}`)
106 defer resp.Body.Close()
107 if resp.StatusCode != http.StatusNoContent {
108 t.Fatalf("edit status = %d", resp.StatusCode)
109 }
110 after, _ := ctrl.GetGoal(t.Context())
111 if after == nil || after.ID != before.ID || after.Revision != before.Revision+1 || after.Objective != "revised" || after.MaxGoalRounds == nil || *after.MaxGoalRounds != 12 {
112 t.Fatalf("goal after edit = %+v, before = %+v", after, before)
113 }
114 }
115
116 func TestQualityFloorRouteAcceptsLegacyValueWithoutChangingStatus(t *testing.T) {
117 bc := NewBroadcaster()
118 ctrl := control.New(control.Options{Sink: bc})
119 srv := httptest.NewServer(New(ctrl, bc, config.ServeConfig{}).Handler())
120 defer srv.Close()
121
122 resp := postRuntimeJSON(t, srv.URL+"/quality-floor", `{"floor":"delivery"}`)
123 resp.Body.Close()
124 if resp.StatusCode != http.StatusNoContent || ctrl.QualityFloor() != control.QualityFloorStandard {
125 t.Fatalf("quality floor status/value = %d/%q", resp.StatusCode, ctrl.QualityFloor())
126 }
127 status, err := http.Get(srv.URL + "/status")
128 if err != nil {
129 t.Fatal(err)
130 }
131 defer status.Body.Close()
132 var payload struct {
133 QualityFloor string `json:"qualityFloor"`
134 }
135 if err := json.NewDecoder(status.Body).Decode(&payload); err != nil {
136 t.Fatal(err)
137 }
138 if payload.QualityFloor != control.QualityFloorStandard {
139 t.Fatalf("status qualityFloor = %q", payload.QualityFloor)
140 }
141
142 bad := postRuntimeJSON(t, srv.URL+"/quality-floor", `{"floor":"turbo"}`)
143 bad.Body.Close()
144 if bad.StatusCode != http.StatusBadRequest {
145 t.Fatalf("invalid quality floor status = %d", bad.StatusCode)
146 }
147 }
148
149 func TestJobsCancelRouteCancelsOwnedJobs(t *testing.T) {
150 bc := NewBroadcaster()
151 manager := jobs.NewManager(bc)
152 ctrl := control.New(control.Options{Sink: bc, Jobs: manager})
153 defer ctrl.Close()
154 job := manager.Start("task", "verify", func(ctx context.Context, _ io.Writer) (string, error) {
155 <-ctx.Done()
156 return "", ctx.Err()
157 })
158 srv := httptest.NewServer(New(ctrl, bc, config.ServeConfig{}).Handler())
159 defer srv.Close()
160
161 resp := postRuntimeJSON(t, srv.URL+"/jobs/cancel", `{"ids":["`+job.ID+`","`+job.ID+`","missing"]}`)
162 defer resp.Body.Close()
163 if resp.StatusCode != http.StatusOK {
164 t.Fatalf("cancel status = %d", resp.StatusCode)
165 }
166 var result struct {
167 Cancelled []string `json:"cancelled"`
168 NotRunning []string `json:"notRunning"`
169 }
170 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
171 t.Fatal(err)
172 }
173 if len(result.Cancelled) != 1 || result.Cancelled[0] != job.ID {
174 t.Fatalf("cancelled = %v", result.Cancelled)
175 }
176 if len(result.NotRunning) != 1 || result.NotRunning[0] != "missing" {
177 t.Fatalf("not running = %v", result.NotRunning)
178 }
179 }
180
180 lines GO