From 88fdb8f578064d508c42e82400f59409864459c9 Mon Sep 17 00:00:00 2001 From: nicodes Date: Thu, 27 Aug 2026 16:13:49 -0600 Subject: [PATCH] Remove exited terminal restart action --- internal/system/system.go | 18 ------ internal/system/terminal_http_test.go | 80 --------------------------- internal/system/tui.go | 18 ++---- internal/system/tui_test.go | 40 +++++--------- 4 files changed, 17 insertions(+), 139 deletions(-) diff --git a/internal/system/system.go b/internal/system/system.go index d711d6e..1adc4a7 100644 --- a/internal/system/system.go +++ b/internal/system/system.go @@ -971,24 +971,6 @@ func (d *system) createTerminalSession(ctx context.Context, projectID string) (r return relay.TerminalSessionInfo{}, fmt.Errorf("could not allocate a unique terminal session id") } -func (d *system) restartTerminalSession(ctx context.Context, recordID, projectID, sessionID string, previousGeneration int) (relay.TerminalSessionInfo, error) { - if err := d.terminalLifecycleReady(); err != nil { - return relay.TerminalSessionInfo{}, err - } - data, err := d.relayDo(ctx, http.MethodPost, "/system/terminal-sessions/"+url.PathEscape(recordID)+"/restart", map[string]int{"generation": previousGeneration}) - if err != nil { - return relay.TerminalSessionInfo{}, err - } - var info relay.TerminalSessionInfo - if err := json.Unmarshal(data, &info); err != nil { - return relay.TerminalSessionInfo{}, err - } - if info.ID == "" || info.ID != recordID || info.ProjectID != projectID || info.SessionID != sessionID || info.State != relay.TerminalStateRunning || info.Generation <= previousGeneration { - return relay.TerminalSessionInfo{}, fmt.Errorf("restart returned invalid terminal session") - } - return info, nil -} - func (d *system) deleteTerminalSession(ctx context.Context, recordID string, generation int) error { if _, err := d.relayDo(ctx, http.MethodDelete, "/system/terminal-sessions/"+url.PathEscape(recordID), map[string]int{"generation": generation}); err != nil { return err diff --git a/internal/system/terminal_http_test.go b/internal/system/terminal_http_test.go index 3abb4fe..c534ad6 100644 --- a/internal/system/terminal_http_test.go +++ b/internal/system/terminal_http_test.go @@ -121,12 +121,6 @@ func TestTerminalMutationGenerationWireShape(t *testing.T) { return nil, err } } - if req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/restart") { - if len(body) != 1 || body["generation"] != float64(3) { - t.Fatalf("restart body=%#v", body) - } - return testHTTPResponse(http.StatusOK, `{"id":"record","project_id":"project","session_id":"tab","state":"running","generation":4}`), nil - } if req.Method == http.MethodDelete && req.URL.Path == "/system/terminal-sessions/record" { if len(body) != 1 || body["generation"] != float64(4) { t.Fatalf("delete body=%#v", body) @@ -139,9 +133,6 @@ func TestTerminalMutationGenerationWireShape(t *testing.T) { return testHTTPResponse(http.StatusNotFound, ""), nil })} d := &system{cfg: systemConfig{RelayURL: "ws://relay.test"}, resetDone: true, terminals: make(map[string]*terminalSession)} - if _, err := d.restartTerminalSession(context.Background(), "record", "project", "tab", 3); err != nil { - t.Fatal(err) - } if err := d.deleteTerminalSession(context.Background(), "record", 4); err != nil { t.Fatal(err) } @@ -214,77 +205,6 @@ func TestTerminalSessionCreateRetriesOnlyExplicitConflict(t *testing.T) { } } -func TestRestartTerminalSessionRequiresFlatRunningGeneration(t *testing.T) { - oldClient := httpClient - t.Cleanup(func() { httpClient = oldClient }) - for name, body := range map[string]string{ - "wrapper": `{"session":{"id":"record","project_id":"project","session_id":"tab","state":"running","generation":2}}`, - "missing id": `{"project_id":"project","session_id":"tab","state":"running","generation":2}`, - "missing session": `{"id":"record","project_id":"project","state":"running","generation":2}`, - "not running": `{"id":"record","project_id":"project","session_id":"tab","state":"closing","generation":2}`, - "bad generation": `{"id":"record","project_id":"project","session_id":"tab","state":"running","generation":0}`, - "wrong record": `{"id":"other","project_id":"project","session_id":"tab","state":"running","generation":2}`, - "wrong project": `{"id":"record","project_id":"other","session_id":"tab","state":"running","generation":2}`, - "wrong session": `{"id":"record","project_id":"project","session_id":"other","state":"running","generation":2}`, - "same generation": `{"id":"record","project_id":"project","session_id":"tab","state":"running","generation":1}`, - } { - t.Run(name, func(t *testing.T) { - httpClient = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { - return testHTTPResponse(http.StatusOK, body), nil - })} - d := &system{cfg: systemConfig{RelayURL: "ws://relay.test"}, resetDone: true} - if _, err := d.restartTerminalSession(context.Background(), "record", "project", "tab", 1); err == nil { - t.Fatal("malformed restart response was accepted") - } - }) - } -} - -func TestRestartTerminalSessionRequiresCompletedReset(t *testing.T) { - for name, state := range map[string]struct { - done bool - err error - }{ - "reset pending": {done: false}, - "reset failed": {err: errors.New("reset unavailable")}, - } { - t.Run(name, func(t *testing.T) { - calls := 0 - oldClient := httpClient - t.Cleanup(func() { httpClient = oldClient }) - httpClient = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { - calls++ - return testHTTPResponse(http.StatusOK, `{"id":"record","project_id":"project","session_id":"tab","state":"running","generation":2}`), nil - })} - d := &system{cfg: systemConfig{RelayURL: "ws://relay.test"}, resetDone: state.done, resetErr: state.err} - if _, err := d.restartTerminalSession(context.Background(), "record", "project", "tab", 1); err == nil { - t.Fatal("restart bypassed terminal lifecycle reset") - } - if calls != 0 { - t.Fatalf("restart issued %d POSTs before reset completion", calls) - } - }) - } - - calls := 0 - oldClient := httpClient - t.Cleanup(func() { httpClient = oldClient }) - httpClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { - calls++ - if req.Method != http.MethodPost || req.URL.Path != "/system/terminal-sessions/record/restart" { - t.Fatalf("unexpected restart request: %s %s", req.Method, req.URL.Path) - } - return testHTTPResponse(http.StatusOK, `{"id":"record","project_id":"project","session_id":"tab","state":"running","generation":2}`), nil - })} - d := &system{cfg: systemConfig{RelayURL: "ws://relay.test"}, resetDone: true} - if _, err := d.restartTerminalSession(context.Background(), "record", "project", "tab", 1); err != nil { - t.Fatalf("completed reset blocked restart: %v", err) - } - if calls != 1 { - t.Fatalf("completed reset issued %d POSTs, want one", calls) - } -} - func TestCreateTerminalSessionRequiresRequestedIdentity(t *testing.T) { oldClient, oldRandom := httpClient, terminalSessionRandom t.Cleanup(func() { httpClient, terminalSessionRandom = oldClient, oldRandom }) diff --git a/internal/system/tui.go b/internal/system/tui.go index 0777ccf..27c6bfb 100644 --- a/internal/system/tui.go +++ b/internal/system/tui.go @@ -109,7 +109,6 @@ type terminalsMsg struct { type terminalCreatedMsg struct { projectRoot string info relay.TerminalSessionInfo - restarted bool err error } @@ -399,11 +398,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.terminalsCmd() } m.err = "" - if msg.restarted { - m.notice = "terminal restarted" - } else { - m.notice = "terminal created" - } + m.notice = "terminal created" m, attach := m.startTerminal(msg.projectRoot, msg.info, msg.info.SessionID) return m, tea.Batch( tea.DisableMouse, @@ -551,13 +546,8 @@ func (m model) updateNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) { p := m.projects[r.projectIdx] tab := m.terminals[r.terminalIdx] if tab.info.State == relay.TerminalStateExited { - recordID := tab.info.ID - return m, func() tea.Msg { - ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) - defer cancel() - info, err := m.d.restartTerminalSession(ctx, recordID, p.ID, tab.info.SessionID, tab.info.Generation) - return terminalCreatedMsg{projectRoot: p.RootDir, info: info, restarted: true, err: err} - } + m.notice = "terminal exited; removal is automatic" + return m, nil } if tab.info.State != "" && tab.info.State != relay.TerminalStateRunning { m.err = "terminal unavailable" @@ -944,7 +934,7 @@ func (m model) View() string { tab := m.terminals[r.terminalIdx] switch tab.info.State { case relay.TerminalStateExited: - hints += " · enter restart" + hints += " · exited; removing automatically" case relay.TerminalStateClosing: hints += " · d delete" case "", relay.TerminalStateRunning: diff --git a/internal/system/tui_test.go b/internal/system/tui_test.go index a85f963..7745796 100644 --- a/internal/system/tui_test.go +++ b/internal/system/tui_test.go @@ -5,6 +5,7 @@ package system import ( "context" "errors" + "fmt" "net/http" "strconv" "strings" @@ -205,34 +206,24 @@ func TestTerminalCreationPersistsBeforeAttach(t *testing.T) { } } -func TestExitedTerminalEnterRestartsBeforeAttach(t *testing.T) { +func TestExitedTerminalEnterHasNoRestartAction(t *testing.T) { m := terminalDashboard(t) next, _ := m.Update(terminalsMsg{terminals: []relay.TerminalSessionInfo{{ID: "record", ProjectID: "project-a", SessionID: "a-tab", State: relay.TerminalStateExited, Generation: 3}}}) m = next.(model) m.cursor = rowIndex(m.rows, rowTerminal, "project-a", "a-tab") - oldClient, oldAttach := httpClient, attachTerminalScreen - t.Cleanup(func() { httpClient, attachTerminalScreen = oldClient, oldAttach }) + oldClient := httpClient + t.Cleanup(func() { httpClient = oldClient }) + requests := 0 httpClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { - if req.Method != http.MethodPost || req.URL.Path != "/system/terminal-sessions/record/restart" { - t.Fatalf("restart request=%s %s", req.Method, req.URL.Path) - } - return testHTTPResponse(http.StatusOK, `{"id":"record","project_id":"project-a","session_id":"a-tab","state":"running","generation":4}`), nil + requests++ + return nil, fmt.Errorf("unexpected request: %s %s", req.Method, req.URL.Path) })} - _, restartCmd := m.Update(tea.KeyMsg{Type: tea.KeyEnter}) - created, ok := restartCmd().(terminalCreatedMsg) - if !ok || created.info.ID != "record" || created.info.Generation != 4 { - t.Fatalf("restart result=%#v", created) - } - client := newFakeTerminalAttachment() - attachTerminalScreen = func(_ *system, _ string, info relay.TerminalSessionInfo, _, _ int) (terminalAttachment, error) { - if info.ID != "record" || info.Generation != 4 { - t.Fatalf("attach info=%+v", info) - } - return client, nil + updated, cmd := m.Update(tea.KeyMsg{Type: tea.KeyEnter}) + if cmd != nil || requests != 0 || updated.(model).mode == modeTerminal { + t.Fatalf("exited enter command=%v requests=%d mode=%v", cmd, requests, updated.(model).mode) } - updated, _ := m.Update(created) - if updated.(model).mode != modeTerminal { - t.Fatal("restarted terminal did not enter terminal mode") + if got := updated.(model).notice; got != "terminal exited; removal is automatic" { + t.Fatalf("exited notice=%q", got) } } @@ -243,7 +234,7 @@ func TestTerminalStateLabelsAndNoticesMatchActions(t *testing.T) { want string avoid string }{ - {relay.TerminalStateExited, "enter restart", "open in TUI"}, + {relay.TerminalStateExited, "exited; removing automatically", "restart"}, {relay.TerminalStateClosing, "d delete", "enter open in TUI"}, {"future-state", "terminal unavailable", "enter open in TUI"}, } { @@ -257,11 +248,6 @@ func TestTerminalStateLabelsAndNoticesMatchActions(t *testing.T) { t.Fatalf("state=%s view lacks truthful footer: %q", tc.state, view) } } - m.notice = "" - next, _ := m.Update(terminalCreatedMsg{info: relay.TerminalSessionInfo{ID: "record", SessionID: "a-tab", State: relay.TerminalStateRunning, Generation: 4}, restarted: true, err: nil}) - if got := next.(model).notice; got != "terminal restarted" { - t.Fatalf("restart notice=%q", got) - } } func TestTerminalDeleteNoticeReportsCompletion(t *testing.T) {