M2: POST /coach route wiring the planning coach
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,7 @@ func (s *Server) Router() *gin.Engine {
|
|||||||
})
|
})
|
||||||
r.GET("/events", s.handleEvents)
|
r.GET("/events", s.handleEvents)
|
||||||
r.POST("/planning", s.handlePlanning)
|
r.POST("/planning", s.handlePlanning)
|
||||||
|
r.POST("/coach", s.handleCoach)
|
||||||
r.POST("/commitment", s.handleCommitment)
|
r.POST("/commitment", s.handleCommitment)
|
||||||
r.POST("/complete", s.handleComplete)
|
r.POST("/complete", s.handleComplete)
|
||||||
r.POST("/end", s.handleEnd)
|
r.POST("/end", s.handleEnd)
|
||||||
@@ -82,6 +83,19 @@ func (s *Server) handlePlanning(c *gin.Context) {
|
|||||||
s.respond(c, s.ctrl.EnterPlanning())
|
s.respond(c, s.ctrl.EnterPlanning())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type coachRequest struct {
|
||||||
|
Intent string `json:"intent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleCoach(c *gin.Context) {
|
||||||
|
var req coachRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid json"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.respond(c, s.ctrl.RequestCoach(req.Intent))
|
||||||
|
}
|
||||||
|
|
||||||
type commitmentRequest struct {
|
type commitmentRequest struct {
|
||||||
NextAction string `json:"next_action"`
|
NextAction string `json:"next_action"`
|
||||||
SuccessCondition string `json:"success_condition"`
|
SuccessCondition string `json:"success_condition"`
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"antidrift/internal/ai"
|
||||||
"antidrift/internal/domain"
|
"antidrift/internal/domain"
|
||||||
"antidrift/internal/evidence"
|
"antidrift/internal/evidence"
|
||||||
"antidrift/internal/session"
|
"antidrift/internal/session"
|
||||||
@@ -98,3 +101,60 @@ func TestLockedStateHasNullEvidence(t *testing.T) {
|
|||||||
t.Fatalf("locked payload should have null evidence: %s", js)
|
t.Fatalf("locked payload should have null evidence: %s", js)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type stubCoach struct {
|
||||||
|
prop ai.Proposal
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubCoach) Coach(ctx context.Context, intent string) (ai.Proposal, error) {
|
||||||
|
return s.prop, s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoachRouteHappyPath(t *testing.T) {
|
||||||
|
s := newTestServer(t)
|
||||||
|
s.ctrl.SetCoach(stubCoach{prop: ai.Proposal{NextAction: "Do X", SuccessCondition: "done", TimeboxSecs: 1500}})
|
||||||
|
r := s.Router()
|
||||||
|
_ = post(t, r, "/planning", "")
|
||||||
|
if w := post(t, r, "/coach", `{"intent":"do something"}`); w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("/coach code %d body %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if cv := s.ctrl.State().Coach; cv != nil && cv.Status == "ready" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
t.Fatalf("coach never reached ready: %+v", s.ctrl.State().Coach)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoachRouteOutsidePlanning(t *testing.T) {
|
||||||
|
s := newTestServer(t)
|
||||||
|
s.ctrl.SetCoach(stubCoach{})
|
||||||
|
r := s.Router()
|
||||||
|
if w := post(t, r, "/coach", `{"intent":"x"}`); w.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected 400, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoachRouteInvalidJSON(t *testing.T) {
|
||||||
|
s := newTestServer(t)
|
||||||
|
r := s.Router()
|
||||||
|
_ = post(t, r, "/planning", "")
|
||||||
|
if w := post(t, r, "/coach", `not json`); w.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected 400, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoachRouteUnavailableDegrades(t *testing.T) {
|
||||||
|
s := newTestServer(t) // no SetCoach
|
||||||
|
r := s.Router()
|
||||||
|
_ = post(t, r, "/planning", "")
|
||||||
|
if w := post(t, r, "/coach", `{"intent":"x"}`); w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("unavailable coach should still 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
if cv := s.ctrl.State().Coach; cv == nil || cv.Status != "error" {
|
||||||
|
t.Fatalf("want error status, got %+v", cv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user