|
| 1 | +package tui |
| 2 | + |
| 3 | +import ( |
| 4 | + "path/filepath" |
| 5 | + "strings" |
| 6 | + "testing" |
| 7 | + |
| 8 | + "github.com/BackendStack21/bodek/internal/client" |
| 9 | + "github.com/BackendStack21/bodek/internal/workspace" |
| 10 | +) |
| 11 | + |
| 12 | +// Regression tests for the connect-time placeholder session id: between the |
| 13 | +// first prompt and the session frame that confirms the minted id, m.sessionID |
| 14 | +// may still hold the placeholder the connect-time session event stamped — |
| 15 | +// /copy-session-id must never hand it to the clipboard, and /new must not let |
| 16 | +// the fresh connection's first session frame re-persist it. |
| 17 | + |
| 18 | +// BUG 1: a prompt arms sessionLive while the id is still the connect-time |
| 19 | +// placeholder; /copy-session-id must wait for the post-prompt session frame. |
| 20 | +func TestCopySessionIDPlaceholderNotCopiedBetweenPromptAndConfirm(t *testing.T) { |
| 21 | + m := newTestModel() |
| 22 | + m.handleEvent(client.Event{Type: "session", SessionID: "sess-placeholder"}) |
| 23 | + _ = m.sendPrompt("hi") // arms sessionLive; the confirm frame has not arrived |
| 24 | + |
| 25 | + runCopySessionID(m) |
| 26 | + if m.copyFlashing() { |
| 27 | + t.Error("placeholder id copied before the post-prompt session frame confirmed it") |
| 28 | + } |
| 29 | + if n := len(m.notices); n == 0 || !strings.Contains(m.notices[n-1], "no session") { |
| 30 | + t.Errorf("placeholder window must show the no-session note, got %v", m.notices) |
| 31 | + } |
| 32 | + |
| 33 | + m.handleEvent(client.Event{Type: "session", SessionID: "sess-real"}) |
| 34 | + runCopySessionID(m) |
| 35 | + if !m.copyFlashing() { |
| 36 | + t.Error("confirmed session id must copy after the post-prompt session frame") |
| 37 | + } |
| 38 | +} |
| 39 | + |
| 40 | +// BUG 3: the old wall-clock gate compared sessionIDAt against |
| 41 | +// lastPromptStart, which sendPrompt re-stamps on every dispatch. odek does |
| 42 | +// not re-emit a session frame per turn, so after the SECOND prompt the |
| 43 | +// confirmed stamp was older than the prompt stamp and /copy-session-id |
| 44 | +// permanently reported "no session yet". The ordering-based gate |
| 45 | +// (awaitSessionConfirm, armed per prompt, cleared by any session frame) |
| 46 | +// must keep the confirmed id copyable across later prompts. |
| 47 | +func TestCopySessionIDStillCopiesAfterSecondPrompt(t *testing.T) { |
| 48 | + m := newTestModel() |
| 49 | + m.handleEvent(client.Event{Type: "session", SessionID: "sess-placeholder"}) |
| 50 | + _ = m.sendPrompt("first") |
| 51 | + m.handleEvent(client.Event{Type: "session", SessionID: "sess-real"}) // confirms the minted id |
| 52 | + _ = m.sendPrompt("second") // no further session frame arrives |
| 53 | + |
| 54 | + runCopySessionID(m) |
| 55 | + if !m.copyFlashing() { |
| 56 | + t.Error("the confirmed session id must still copy after a second prompt") |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +// Resume path is unaffected: an adopted session is live and copyable without |
| 61 | +// any prompt. |
| 62 | +func TestCopySessionIDResumedSessionCopiesWithoutPrompt(t *testing.T) { |
| 63 | + m := wired(t) |
| 64 | + m.handleSessionDetail(sessionDetailMsg{sess: client.Session{ID: "sess-resumed"}}) |
| 65 | + runCopySessionID(m) |
| 66 | + if !m.copyFlashing() { |
| 67 | + t.Error("an adopted (resumed) session id must copy without a prompt") |
| 68 | + } |
| 69 | +} |
| 70 | + |
| 71 | +// BUG 2: the reconnect consumes freshStart before the fresh connection's |
| 72 | +// session frame is ingested, so rememberSession re-persists the placeholder. |
| 73 | +func TestNewReconnectDoesNotRepersistPlaceholder(t *testing.T) { |
| 74 | + t.Setenv("BODEK_WORKSPACE", filepath.Join(t.TempDir(), "workspaces.json")) |
| 75 | + m := wired(t) |
| 76 | + m.ws = workspace.Open() |
| 77 | + m.opts.CWD = "/tmp/bodek-test-fresh-remember" |
| 78 | + _ = m.ws.Save(m.opts.CWD, workspace.State{SessionID: "s1"}) |
| 79 | + seedSessionState(m) |
| 80 | + |
| 81 | + runNew(m) |
| 82 | + if got := m.ws.Load(m.opts.CWD).SessionID; got != "" { |
| 83 | + t.Fatalf("/new must clear the mapping, got %q", got) |
| 84 | + } |
| 85 | + |
| 86 | + m.disconn = true |
| 87 | + m.handleReconnect(reconnectMsg{cl: m.cl}) |
| 88 | + m.handleEvent(client.Event{Type: "session", SessionID: "sess-placeholder"}) |
| 89 | + |
| 90 | + if got := m.ws.Load(m.opts.CWD).SessionID; got != "" { |
| 91 | + t.Errorf("fresh connection's placeholder frame re-persisted session %q", got) |
| 92 | + } |
| 93 | + |
| 94 | + // The suppression is one-shot: the real id from a later frame persists. |
| 95 | + m.handleEvent(client.Event{Type: "session", SessionID: "sess-real"}) |
| 96 | + if got := m.ws.Load(m.opts.CWD).SessionID; got != "sess-real" { |
| 97 | + t.Errorf("real session id must persist after the fresh session starts, got %q", got) |
| 98 | + } |
| 99 | +} |
0 commit comments