summaryrefslogtreecommitdiff
path: root/internal/scheduler/story_orchestrator_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/scheduler/story_orchestrator_test.go')
-rw-r--r--internal/scheduler/story_orchestrator_test.go100
1 files changed, 100 insertions, 0 deletions
diff --git a/internal/scheduler/story_orchestrator_test.go b/internal/scheduler/story_orchestrator_test.go
index b811206..0d96840 100644
--- a/internal/scheduler/story_orchestrator_test.go
+++ b/internal/scheduler/story_orchestrator_test.go
@@ -499,6 +499,106 @@ func TestStoryOrchestrator_ArbitrationCompletes_EmitsDecisionAndReviewReady(t *t
}
}
+// TestStoryOrchestrator_ArbitrationRejects_SetsStoryNeedsFix proves that
+// when the arbitration task reports a structured approved=false verdict
+// (via a KindVerdictReported event, the same one AgentChannel.ReportVerdict
+// records), finalizeArbitration routes the story to NEEDS_FIX instead of
+// unconditionally REVIEW_READY.
+func TestStoryOrchestrator_ArbitrationRejects_SetsStoryNeedsFix(t *testing.T) {
+ store, st, evaluators := seedStoryWithEvaluators(t, task.StateCompleted)
+
+ pool := &fakePool{}
+ orch := &StoryOrchestrator{Store: store, Pool: pool}
+ orch.Tick(context.Background()) // spawns arbitration
+
+ arbitrations := store.dependentsWithRole(evaluators[0].ID, "planner")
+ if len(arbitrations) != 1 {
+ t.Fatalf("expected 1 arbitration task, got %d", len(arbitrations))
+ }
+ arb := arbitrations[0]
+
+ // Simulate the arbitration agent calling report_verdict with a rejection
+ // before finishing, the same event storeChannel.ReportVerdict records.
+ payload, _ := json.Marshal(struct {
+ Approved bool `json:"approved"`
+ Reasoning string `json:"reasoning"`
+ }{Approved: false, Reasoning: "breaks the running-log panel styling"})
+ if err := store.CreateEvent(&event.Event{
+ TaskID: arb.ID,
+ Kind: event.KindVerdictReported,
+ Actor: event.ActorAgent,
+ Payload: payload,
+ }); err != nil {
+ t.Fatalf("seed verdict event: %v", err)
+ }
+
+ store.setTaskState(arb.ID, task.StateCompleted)
+ store.setTaskSummary(arb.ID, "found a real problem")
+
+ orch.Tick(context.Background()) // finalizeArbitration runs
+
+ stories, _ := store.ListStories(storage.StoryFilter{})
+ var got *story.Story
+ for _, s := range stories {
+ if s.ID == st.ID {
+ got = s
+ }
+ }
+ if got == nil {
+ t.Fatal("story not found")
+ }
+ if got.Status != "NEEDS_FIX" {
+ t.Errorf("story status: want NEEDS_FIX, got %q", got.Status)
+ }
+}
+
+// TestStoryOrchestrator_ArbitrationApproves_SetsReviewReady proves the
+// mirror case: an explicit approved=true verdict still routes to
+// REVIEW_READY, same as today's unconditional behavior -- this isn't just
+// "absence of a verdict defaults to proceed", a real approval also proceeds.
+func TestStoryOrchestrator_ArbitrationApproves_SetsReviewReady(t *testing.T) {
+ store, st, evaluators := seedStoryWithEvaluators(t, task.StateCompleted)
+
+ pool := &fakePool{}
+ orch := &StoryOrchestrator{Store: store, Pool: pool}
+ orch.Tick(context.Background())
+
+ arbitrations := store.dependentsWithRole(evaluators[0].ID, "planner")
+ arb := arbitrations[0]
+
+ payload, _ := json.Marshal(struct {
+ Approved bool `json:"approved"`
+ Reasoning string `json:"reasoning"`
+ }{Approved: true, Reasoning: "meets all acceptance criteria"})
+ if err := store.CreateEvent(&event.Event{
+ TaskID: arb.ID,
+ Kind: event.KindVerdictReported,
+ Actor: event.ActorAgent,
+ Payload: payload,
+ }); err != nil {
+ t.Fatalf("seed verdict event: %v", err)
+ }
+
+ store.setTaskState(arb.ID, task.StateCompleted)
+ store.setTaskSummary(arb.ID, "ship it")
+
+ orch.Tick(context.Background())
+
+ stories, _ := store.ListStories(storage.StoryFilter{})
+ var got *story.Story
+ for _, s := range stories {
+ if s.ID == st.ID {
+ got = s
+ }
+ }
+ if got == nil {
+ t.Fatal("story not found")
+ }
+ if got.Status != "REVIEW_READY" {
+ t.Errorf("story status: want REVIEW_READY, got %q", got.Status)
+ }
+}
+
// TestStoryOrchestrator_DoesNothing_WhenBuilderNotComplete proves the
// orchestrator is inert for a story whose builder task hasn't reached
// COMPLETED yet and isn't auto-acceptable either (still RUNNING — not