summaryrefslogtreecommitdiff
path: root/internal/scheduler/story_orchestrator_test.go
diff options
context:
space:
mode:
authorAgent <agent@claudomator>2026-07-09 19:45:21 +0000
committerAgent <agent@claudomator>2026-07-09 19:45:21 +0000
commita90ac76f3ccf0bf1259b7ef45e5f331d2b4ec26f (patch)
tree03b5895d5edc5d0e45721ae3257c877652cc44cb /internal/scheduler/story_orchestrator_test.go
parent7b51e781f4b5ded41e8d3ac2b566abf2c1b00cdc (diff)
refactor(scheduler): migrate story fix loop off RootTaskID re-pointing onto CurrentAttempt resolution
Diffstat (limited to 'internal/scheduler/story_orchestrator_test.go')
-rw-r--r--internal/scheduler/story_orchestrator_test.go33
1 files changed, 19 insertions, 14 deletions
diff --git a/internal/scheduler/story_orchestrator_test.go b/internal/scheduler/story_orchestrator_test.go
index 0ace7e7..200e38e 100644
--- a/internal/scheduler/story_orchestrator_test.go
+++ b/internal/scheduler/story_orchestrator_test.go
@@ -660,7 +660,15 @@ func seedNeedsFixStory(t *testing.T) (*fakeStoryStore, *story.Story, *task.Task,
// at it (RootTaskID updated, Status reset to IN_PROGRESS) so the very next
// tick re-enters the normal Builder->Evaluators->Arbitration flow against
// the new attempt.
-func TestStoryOrchestrator_NeedsFix_SpawnsFixAttemptAndRepointsRoot(t *testing.T) {
+// TestStoryOrchestrator_NeedsFix_SpawnsFixAttempt proves the core mechanism:
+// a story at NEEDS_FIX gets a new builder-role fix-attempt task spawned
+// (depending on the rejected root), and the story's Status resets to
+// IN_PROGRESS so the very next tick re-enters the normal
+// Builder->Evaluators->Arbitration flow against the new attempt --
+// discovered via task.CurrentAttempt resolving forward from st.RootTaskID,
+// which is never mutated (it's an immutable anchor set once at story
+// creation).
+func TestStoryOrchestrator_NeedsFix_SpawnsFixAttempt(t *testing.T) {
store, st, oldRoot, _, _ := seedNeedsFixStory(t)
pool := &fakePool{}
@@ -689,8 +697,8 @@ func TestStoryOrchestrator_NeedsFix_SpawnsFixAttemptAndRepointsRoot(t *testing.T
if got == nil {
t.Fatal("story not found")
}
- if got.RootTaskID != fix.ID {
- t.Errorf("story RootTaskID = %q, want the new fix-attempt task %q", got.RootTaskID, fix.ID)
+ if got.RootTaskID != oldRoot.ID {
+ t.Errorf("story RootTaskID = %q, want unchanged (still %q -- it's an immutable anchor now, resolved via task.CurrentAttempt)", got.RootTaskID, oldRoot.ID)
}
if got.Status != "IN_PROGRESS" {
t.Errorf("story Status = %q, want IN_PROGRESS", got.Status)
@@ -744,10 +752,9 @@ func TestStoryOrchestrator_NeedsFix_IdempotentAcrossTicks(t *testing.T) {
// TestStoryOrchestrator_EnsureFixAttempt_FindsExistingSpawnedAttempt proves
// ensureFixAttempt's own structural idempotency: simulating a restart
-// between "fix task spawned" and "story RootTaskID/Status updated to point
-// at it" -- calling ensureFixAttempt again must find the already-spawned
-// builder-role dependent rather than spawning a second one, and still
-// complete the re-pointing.
+// between "fix task spawned" and "story Status reset" -- calling
+// ensureFixAttempt again must find the already-spawned builder-role
+// dependent rather than spawning a second one, and still reset Status.
func TestStoryOrchestrator_EnsureFixAttempt_FindsExistingSpawnedAttempt(t *testing.T) {
store, st, oldRoot, _, _ := seedNeedsFixStory(t)
@@ -768,8 +775,8 @@ func TestStoryOrchestrator_EnsureFixAttempt_FindsExistingSpawnedAttempt(t *testi
if fixAttempts[0].ID != preexisting.ID {
t.Errorf("expected ensureFixAttempt to reuse the pre-existing task %s, got a different one %s", preexisting.ID, fixAttempts[0].ID)
}
- if st.RootTaskID != preexisting.ID {
- t.Errorf("story RootTaskID = %q, want the pre-existing fix attempt %q", st.RootTaskID, preexisting.ID)
+ if st.RootTaskID != oldRoot.ID {
+ t.Errorf("story RootTaskID = %q, want unchanged (still %q -- it's an immutable anchor now)", st.RootTaskID, oldRoot.ID)
}
if st.Status != "IN_PROGRESS" {
t.Errorf("story Status = %q, want IN_PROGRESS", st.Status)
@@ -779,7 +786,9 @@ func TestStoryOrchestrator_EnsureFixAttempt_FindsExistingSpawnedAttempt(t *testi
// TestStoryOrchestrator_NeedsFix_CapsAtMaxFixAttempts proves the safety net:
// once a chain of maxFixAttempts consecutive fix attempts already exists,
// ensureFixAttempt stops spawning new ones and leaves the story at
-// NEEDS_FIX, exactly like today's fully-manual behavior.
+// NEEDS_FIX, exactly like today's fully-manual behavior. st.RootTaskID stays
+// at oldRoot.ID throughout (it's an immutable anchor); ensureFixAttempt
+// resolves the chain's tip itself via task.CurrentAttempt.
func TestStoryOrchestrator_NeedsFix_CapsAtMaxFixAttempts(t *testing.T) {
store, st, oldRoot, _, _ := seedNeedsFixStory(t)
@@ -797,10 +806,6 @@ func TestStoryOrchestrator_NeedsFix_CapsAtMaxFixAttempts(t *testing.T) {
}
prev = nt
}
- st.RootTaskID = prev.ID
- if err := store.UpdateStory(st); err != nil {
- t.Fatalf("persist chained root: %v", err)
- }
orch.Tick(context.Background())