From c82def744f23d09562f0bd980ee8333eeb1b90e4 Mon Sep 17 00:00:00 2001 From: Claudomator Agent Date: Wed, 8 Jul 2026 09:43:25 +0000 Subject: feat(scheduler): finalizeArbitration consults a structured verdict instead of always routing to REVIEW_READY --- internal/scheduler/story_orchestrator_test.go | 100 ++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) (limited to 'internal/scheduler/story_orchestrator_test.go') 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 -- cgit v1.2.3