Test that a stale tasks fetch result is discarded
Exercises the generation/left-planning guard in startTasksFetchLocked via the fakeProvider gate: a fetch in flight when its generation is superseded must not clobber the current tasks state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -843,3 +843,78 @@ func TestTasksViewAbsentOutsidePlanning(t *testing.T) {
|
|||||||
t.Fatalf("no tasks view while Locked: %+v", c.State().Tasks)
|
t.Fatalf("no tasks view while Locked: %+v", c.State().Tasks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestStaleTasksFetchDiscardedAfterLeavingPlanning exercises the
|
||||||
|
// "left planning" arm of the discard guard in startTasksFetchLocked.
|
||||||
|
//
|
||||||
|
// Sequence:
|
||||||
|
// 1. Enter planning with a gated provider (fetch in flight, blocked).
|
||||||
|
// 2. StartManualCommitment moves Planning -> Active, leaving planning.
|
||||||
|
// 3. End the session to return to Locked.
|
||||||
|
// 4. Install a second (ungated) provider and re-enter planning.
|
||||||
|
// 5. Wait for the fresh fetch to reach "ready" with the new provider's list.
|
||||||
|
// 6. Release the gate — the stale goroutine from step 1 now runs and must be
|
||||||
|
// discarded (runtimeState != Planning at commit time).
|
||||||
|
// 7. Assert the stale result did not clobber the fresh tasks list.
|
||||||
|
func TestStaleTasksFetchDiscardedAfterLeavingPlanning(t *testing.T) {
|
||||||
|
c, _ := newTestController(t)
|
||||||
|
|
||||||
|
// Step 1: gated provider whose result ("STALE") must never surface.
|
||||||
|
staleGate := make(chan struct{})
|
||||||
|
stale := &fakeProvider{
|
||||||
|
list: []tasks.Task{{ID: "s1", Title: "STALE"}},
|
||||||
|
gate: staleGate,
|
||||||
|
}
|
||||||
|
c.SetTasks(stale)
|
||||||
|
if err := c.EnterPlanning(); err != nil {
|
||||||
|
t.Fatalf("enter planning: %v", err)
|
||||||
|
}
|
||||||
|
// Wait until the goroutine has started and is pending (blocked on gate).
|
||||||
|
waitTasksStatus(t, c, "pending")
|
||||||
|
|
||||||
|
// Step 2: leave planning via a commitment — the stale fetch is still blocked.
|
||||||
|
if err := c.StartManualCommitment("task", "done", 25*time.Minute, nil); err != nil {
|
||||||
|
t.Fatalf("start commitment: %v", err)
|
||||||
|
}
|
||||||
|
if c.State().RuntimeState != domain.RuntimeActive {
|
||||||
|
t.Fatalf("expected Active after StartManualCommitment")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: end the session to return to Locked.
|
||||||
|
if err := c.Complete(); err != nil {
|
||||||
|
t.Fatalf("complete: %v", err)
|
||||||
|
}
|
||||||
|
if err := c.End(); err != nil {
|
||||||
|
t.Fatalf("end: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: fresh provider whose result ("FRESH") is the expected state.
|
||||||
|
fresh := &fakeProvider{list: []tasks.Task{{ID: "f1", Title: "FRESH"}}}
|
||||||
|
c.SetTasks(fresh)
|
||||||
|
if err := c.EnterPlanning(); err != nil {
|
||||||
|
t.Fatalf("second enter planning: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: wait for the fresh fetch to complete and be visible.
|
||||||
|
st := waitTasksStatus(t, c, "ready")
|
||||||
|
if len(st.Tasks.Tasks) != 1 || st.Tasks.Tasks[0].Title != "FRESH" {
|
||||||
|
t.Fatalf("expected FRESH tasks after second planning entry, got %+v", st.Tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: release the stale goroutine — it must detect runtimeState != Planning
|
||||||
|
// (we are now in Planning again, but tasksGen has moved on) and discard.
|
||||||
|
close(staleGate)
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
// Step 7: the fresh list must still be intact; STALE must not have landed.
|
||||||
|
st = c.State()
|
||||||
|
if st.Tasks == nil {
|
||||||
|
t.Fatalf("tasks view should still be present in planning")
|
||||||
|
}
|
||||||
|
if st.Tasks.Status != "ready" {
|
||||||
|
t.Fatalf("tasks status should still be ready, got %q", st.Tasks.Status)
|
||||||
|
}
|
||||||
|
if len(st.Tasks.Tasks) != 1 || st.Tasks.Tasks[0].Title != "FRESH" {
|
||||||
|
t.Fatalf("stale fetch result clobbered the fresh tasks: %+v", st.Tasks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user