diff --git a/internal/tui/panels.go b/internal/tui/panels.go index d26a412..672175c 100644 --- a/internal/tui/panels.go +++ b/internal/tui/panels.go @@ -989,10 +989,15 @@ func (m *Model) handleSessionDetail(msg sessionDetailMsg) tea.Cmd { m.lastLatency = 0 m.msgs = m.msgs[:0] m.convCount = -1 // transcript swapped for the resumed one — drop the cache + // The plan surface is session-scoped knowledge: drop it and refetch for + // the resumed session. Neither live path covers a resume — the replayed + // transcript bypasses the WS trigger, and the adopt's session event + // carries the already-swapped id (no switch delta → no reset). + m.resetPlanState() m.replayTranscript(msg.sess.Messages) note := m.transientNoteCmd("resumed session " + shortID(msg.sess.ID)) m.closePanel() - return tea.Batch(note, m.adoptSession()) + return tea.Batch(note, m.adoptSession(), m.fetchPlan()) } // handleSessionUpdated applies a pin/unpin outcome to the list in place. diff --git a/internal/tui/plan_test.go b/internal/tui/plan_test.go index 266e084..7abbd48 100644 --- a/internal/tui/plan_test.go +++ b/internal/tui/plan_test.go @@ -2,6 +2,7 @@ package tui import ( "errors" + "strings" "testing" "github.com/BackendStack21/bodek/internal/client" @@ -20,6 +21,46 @@ func planCallEvent(name string) client.Event { return client.Event{Type: "tool_call", Name: name, Data: "{}"} } +// TestPlanSurface_SessionResumeResetsAndRefetches pins the resume path: +// handleSessionDetail drops the previous session's plan knowledge and seeds +// a fresh fetch, so the strip reflects the resumed session instead of +// leaking the old one (and never showing anything until a live plan call). +func TestPlanSurface_SessionResumeResetsAndRefetches(t *testing.T) { + m := newTestModel() + m.cl = &client.Client{} // fetch closures are built but never executed + m.sessionID = "s1" + acceptPlan(m, planFixture()) + + // Resume an interrupted session: transcript swap + adopt. + cmd := m.handleSessionDetail(sessionDetailMsg{sess: client.Session{ID: "s2"}, token: "a2"}) + if cmd == nil { + t.Fatal("resume must return the adopt/fetch command batch") + } + if m.planInit || m.planVer != 0 { + t.Fatalf("resume must drop stale plan knowledge: init=%v ver=%d", m.planInit, m.planVer) + } + if m.planReqSeq == 0 { + t.Fatal("resume must seed a plan fetch for the resumed session") + } + + // Until the resumed session's snapshot arrives, the old plan must not + // leak into the strip even while busy. + m.busy = true + if got := m.planStripLabel(); got != "" { + t.Fatalf("stale pre-resume plan leaked into the strip: %q", got) + } + + snap := planFixture() + snap.SessionID = "s2" + acceptPlan(m, snap) + got := m.planStripLabel() + for _, want := range []string{"plan 1/4", "wire flag parsing"} { + if !strings.Contains(got, want) { + t.Errorf("resumed-session strip %q missing %q", got, want) + } + } +} + func TestPlanWSTrigger_DebouncedRefresh(t *testing.T) { m := &Model{} before := m.planDebSeq