| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "reasonix/internal/event" |
| 6 | "reasonix/internal/plancontract" |
| 7 | "reasonix/internal/provider" |
| 8 | "reasonix/internal/tool" |
| 9 | "reflect" |
| 10 | "testing" |
| 11 | ) |
| 12 | |
| 13 | func contractPlan() plancontract.Plan { |
| 14 | return plancontract.Plan{ |
| 15 | Objective: "make the cache key model-aware", |
| 16 | Steps: []plancontract.Step{ |
| 17 | { |
| 18 | ID: "p1", Title: "thread the model ref through", |
| 19 | VerifiedFiles: []string{"internal/provider/cache.go"}, |
| 20 | CandidateFiles: []string{"internal/boot/boot.go"}, |
| 21 | Risks: []string{"warm caches invalidate once"}, |
| 22 | Acceptance: []plancontract.Criterion{ |
| 23 | {Text: "two model refs never share an entry"}, |
| 24 | {Text: "existing hits keep hitting", Regression: true}, |
| 25 | {Text: "the hit rate is logged", Optional: true}, |
| 26 | }, |
| 27 | Verification: []plancontract.Verification{{Command: "go test ./internal/provider/"}}, |
| 28 | }, |
| 29 | }, |
| 30 | }.Normalize() |
| 31 | } |
| 32 | |
| 33 | func TestApprovedPlanRemainsInstructionData(t *testing.T) { |
| 34 | plan := contractPlan() |
| 35 | a := New(nil, tool.NewRegistry(), NewSession(""), Options{}, event.Discard) |
| 36 | a.SetPlanContract(&plan) |
| 37 | if !reflect.DeepEqual(a.planContractSnapshot(), &plan) { |
| 38 | t.Fatal("plan content changed") |
| 39 | } |
| 40 | if !a.ReadinessResult().Ready { |
| 41 | t.Fatal("plan content created a quality gate") |
| 42 | } |
| 43 | } |
| 44 | func TestCoordinatorClearsThePlanContractEachTurn(t *testing.T) { |
| 45 | planner := &mockProvider{name: "planner", streams: submitPlanCall(e2ePlanArgs)} |
| 46 | exec := &mockProvider{name: "executor", chunks: []provider.Chunk{ |
| 47 | {Type: provider.ChunkText, Text: "Done."}, |
| 48 | {Type: provider.ChunkDone}, |
| 49 | }} |
| 50 | coord, executor := submitPlanCoordinator(t, planner, exec, event.Discard) |
| 51 | |
| 52 | if err := coord.Run(withNoClosedLoop(context.Background()), "fix the cache key"); err != nil { |
| 53 | t.Fatalf("Run: %v", err) |
| 54 | } |
| 55 | if executor.planContractSnapshot() == nil { |
| 56 | t.Fatal("the approved plan never reached the executor's contract") |
| 57 | } |
| 58 | |
| 59 | coord.plannerPolicy = func(context.Context, string) PlannerDecision { |
| 60 | return PlannerDecision{Route: PlannerRouteExecutorOnly, Reason: "test"} |
| 61 | } |
| 62 | if err := coord.Run(withNoClosedLoop(context.Background()), "just answer me"); err != nil { |
| 63 | t.Fatalf("Run: %v", err) |
| 64 | } |
| 65 | if executor.planContractSnapshot() != nil { |
| 66 | t.Fatal("an executor-only turn inherited the previous turn's plan") |
| 67 | } |
| 68 | } |
| 69 |