Reflect on entering Review and ground the next coach
The controller fetches a reflection asynchronously on enterReview (generation-guarded, non-blocking, graceful) and caches a one-line recap plus a latest-wins carry-forward. The recap projects onto Review, the carry-forward onto the next Planning, and both ride the snapshot. RequestCoach composes the carry-forward into the coach's existing free-form grounding string, so ai.Coach is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@@ -1029,3 +1030,150 @@ func TestCoachReceivesCachedGrounding(t *testing.T) {
|
||||
t.Fatalf("coach grounding = %q, want the cached profile", fc.grounding())
|
||||
}
|
||||
}
|
||||
|
||||
type fakeReviewer struct {
|
||||
refl ai.Reflection
|
||||
err error
|
||||
gate chan struct{} // if non-nil, Review blocks until it receives
|
||||
calls int32
|
||||
}
|
||||
|
||||
func (f *fakeReviewer) Review(ctx context.Context, finished, history string) (ai.Reflection, error) {
|
||||
atomic.AddInt32(&f.calls, 1)
|
||||
if f.gate != nil {
|
||||
<-f.gate
|
||||
}
|
||||
return f.refl, f.err
|
||||
}
|
||||
|
||||
// waitReflectionStatus polls until the reflection view reaches want, or fails after 2s.
|
||||
func waitReflectionStatus(t *testing.T, c *Controller, want string) State {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
st := c.State()
|
||||
if st.Reflection != nil && st.Reflection.Status == want {
|
||||
return st
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("reflection status never reached %q (last: %+v)", want, c.State().Reflection)
|
||||
return State{}
|
||||
}
|
||||
|
||||
func driveToReview(t *testing.T, c *Controller) {
|
||||
t.Helper()
|
||||
if err := c.EnterPlanning(); err != nil {
|
||||
t.Fatalf("planning: %v", err)
|
||||
}
|
||||
if err := c.StartManualCommitment("write plan", "plan done", 25*time.Minute, nil); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
if err := c.Complete(); err != nil {
|
||||
t.Fatalf("complete: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReflectionFetchedOnReview(t *testing.T) {
|
||||
c, _ := newTestController(t)
|
||||
c.SetReviewer(&fakeReviewer{refl: ai.Reflection{Recap: "held focus", CarryForward: "start in the editor"}})
|
||||
driveToReview(t, c)
|
||||
st := waitReflectionStatus(t, c, "ready")
|
||||
if st.Reflection.Recap != "held focus" {
|
||||
t.Fatalf("recap not projected on Review: %+v", st.Reflection)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoReviewerYieldsIdleReflection(t *testing.T) {
|
||||
c, _ := newTestController(t)
|
||||
driveToReview(t, c)
|
||||
st := c.State()
|
||||
if st.Reflection == nil || st.Reflection.Status != "idle" {
|
||||
t.Fatalf("nil reviewer should yield idle reflection, got %+v", st.Reflection)
|
||||
}
|
||||
if err := c.End(); err != nil {
|
||||
t.Fatalf("End must still work with no reviewer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCarryForwardGroundsNextCoach(t *testing.T) {
|
||||
c, _ := newTestController(t)
|
||||
c.SetReviewer(&fakeReviewer{refl: ai.Reflection{Recap: "ok", CarryForward: "begin with the hardest test"}})
|
||||
fc := &fakeCoach{prop: ai.Proposal{NextAction: "a", SuccessCondition: "b", TimeboxSecs: 1200}}
|
||||
c.SetCoach(fc)
|
||||
driveToReview(t, c)
|
||||
waitReflectionStatus(t, c, "ready")
|
||||
if err := c.End(); err != nil {
|
||||
t.Fatalf("end: %v", err)
|
||||
}
|
||||
if err := c.EnterPlanning(); err != nil {
|
||||
t.Fatalf("planning: %v", err)
|
||||
}
|
||||
if err := c.RequestCoach("ship it"); err != nil {
|
||||
t.Fatalf("request coach: %v", err)
|
||||
}
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) && fc.grounding() == "" {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
if g := fc.grounding(); !strings.Contains(g, "begin with the hardest test") {
|
||||
t.Fatalf("carry-forward not threaded into coach grounding: %q", g)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReflectionStaleResultDiscarded(t *testing.T) {
|
||||
c, _ := newTestController(t)
|
||||
slow := &fakeReviewer{refl: ai.Reflection{Recap: "OLD", CarryForward: "old"}, gate: make(chan struct{})}
|
||||
c.SetReviewer(slow)
|
||||
driveToReview(t, c) // gen1 pending, goroutine blocks on gate
|
||||
waitReflectionStatus(t, c, "pending")
|
||||
|
||||
_ = c.End() // Review -> Locked; gen1 still blocked
|
||||
fast := &fakeReviewer{refl: ai.Reflection{Recap: "NEW", CarryForward: "new"}}
|
||||
c.SetReviewer(fast)
|
||||
driveToReview(t, c) // gen2 -> ready NEW
|
||||
st := waitReflectionStatus(t, c, "ready")
|
||||
if st.Reflection.Recap != "NEW" {
|
||||
t.Fatalf("expected NEW, got %+v", st.Reflection)
|
||||
}
|
||||
|
||||
close(slow.gate) // release gen1; it must be discarded
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if got := c.State().Reflection.Recap; got != "NEW" {
|
||||
t.Fatalf("stale gen1 overwrote reflection: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCarryForwardSurvivesRestart(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "state.json")
|
||||
first, err := New(path)
|
||||
if err != nil {
|
||||
t.Fatalf("new: %v", err)
|
||||
}
|
||||
first.SetReviewer(&fakeReviewer{refl: ai.Reflection{Recap: "ok", CarryForward: "lead with tests"}})
|
||||
if err := first.EnterPlanning(); err != nil {
|
||||
t.Fatalf("planning: %v", err)
|
||||
}
|
||||
if err := first.StartManualCommitment("a", "b", 25*time.Minute, nil); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
if err := first.Complete(); err != nil {
|
||||
t.Fatalf("complete: %v", err)
|
||||
}
|
||||
waitReflectionStatus(t, first, "ready")
|
||||
if err := first.End(); err != nil {
|
||||
t.Fatalf("end: %v", err)
|
||||
}
|
||||
|
||||
second, err := New(path)
|
||||
if err != nil {
|
||||
t.Fatalf("reopen: %v", err)
|
||||
}
|
||||
if err := second.EnterPlanning(); err != nil {
|
||||
t.Fatalf("planning: %v", err)
|
||||
}
|
||||
st := second.State()
|
||||
if st.Reflection == nil || st.Reflection.CarryForward != "lead with tests" {
|
||||
t.Fatalf("carry-forward not restored onto planning: %+v", st.Reflection)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user