test(tabs): cover display and persistence lifecycle
- commit
- fb3dc1334f77ceb536583db868631c929d998305
- author
- brice · · 2026-07-09 18:18 UTC
- parent
- c8d167e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +67 -16
M internal/usecase/daemon/palette.go +1 -6
| @@ -1,8 +1,6 @@ | |||
| 1 | 1 | package daemon | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "strconv" | |
| 5 | - | ||
| 6 | 4 | "github.com/bnema/vev/internal/domain" | |
| 7 | 5 | "github.com/bnema/vev/internal/usecase/command" | |
| 8 | 6 | "github.com/bnema/vev/internal/usecase/palette" | |
| @@ -155,10 +153,7 @@ func (e paletteExec) RenameTab() error { | |||
| 155 | 153 | return nil | |
| 156 | 154 | } | |
| 157 | 155 | e.sess.mu.Lock() | |
| 158 | - | currentName := tb.name | |
| 159 | - | if currentName == "" { | |
| 160 | - | currentName = strconv.Itoa(e.sess.active + 1) | |
| 161 | - | } | |
| 156 | + | currentName := tabDisplayName(tb, e.sess.active) | |
| 162 | 157 | e.sess.mu.Unlock() | |
| 163 | 158 | e.d.enterPrompt(e.sess, e.ac, " Rename tab ", currentName, func(name string) error { | |
| 164 | 159 | return e.d.renameTab(e.sess, tb, name) | |
| @@ -1,8 +1,6 @@ | |||||
| 1 | package daemon | 1 | package daemon | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "strconv" | |||
| 5 | - | ||||
| 6 | "github.com/bnema/vev/internal/domain" | 4 | "github.com/bnema/vev/internal/domain" | ||
| 7 | "github.com/bnema/vev/internal/usecase/command" | 5 | "github.com/bnema/vev/internal/usecase/command" | ||
| 8 | "github.com/bnema/vev/internal/usecase/palette" | 6 | "github.com/bnema/vev/internal/usecase/palette" | ||
| @@ -155,10 +153,7 @@ func (e paletteExec) RenameTab() error { | |||||
| 155 | return nil | 153 | return nil | ||
| 156 | } | 154 | } | ||
| 157 | e.sess.mu.Lock() | 155 | e.sess.mu.Lock() | ||
| 158 | - | currentName := tb.name | 156 | + | currentName := tabDisplayName(tb, e.sess.active) |
| 159 | - | if currentName == "" { | |||
| 160 | - | currentName = strconv.Itoa(e.sess.active + 1) | |||
| 161 | - | } | |||
| 162 | e.sess.mu.Unlock() | 157 | e.sess.mu.Unlock() | ||
| 163 | e.d.enterPrompt(e.sess, e.ac, " Rename tab ", currentName, func(name string) error { | 158 | e.d.enterPrompt(e.sess, e.ac, " Rename tab ", currentName, func(name string) error { | ||
| 164 | return e.d.renameTab(e.sess, tb, name) | 159 | return e.d.renameTab(e.sess, tb, name) | ||
M internal/usecase/daemon/picker.go +1 -5
| @@ -27,7 +27,6 @@ package daemon | |||
| 27 | 27 | ||
| 28 | 28 | import ( | |
| 29 | 29 | "sort" | |
| 30 | - | "strconv" | |
| 31 | 30 | ||
| 32 | 31 | "github.com/bnema/vev/internal/domain" | |
| 33 | 32 | "github.com/bnema/vev/internal/platform" | |
| @@ -71,10 +70,7 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, int) { | |||
| 71 | 70 | view := picker.SessionView{ID: s.id, Name: s.name, Active: s.active, Tabs: make([]string, len(s.tabs))} | |
| 72 | 71 | sessionAttention := false | |
| 73 | 72 | for i, tb := range s.tabs { | |
| 74 | - | label := tb.name | |
| 75 | - | if label == "" { | |
| 76 | - | label = strconv.Itoa(i + 1) | |
| 77 | - | } | |
| 73 | + | label := tabDisplayName(tb, i) | |
| 78 | 74 | if tb.attention { | |
| 79 | 75 | label = attentionSuffix(label) | |
| 80 | 76 | sessionAttention = true | |
| @@ -27,7 +27,6 @@ package daemon | |||||
| 27 | 27 | ||||
| 28 | import ( | 28 | import ( | ||
| 29 | "sort" | 29 | "sort" | ||
| 30 | - | "strconv" | |||
| 31 | 30 | ||||
| 32 | "github.com/bnema/vev/internal/domain" | 31 | "github.com/bnema/vev/internal/domain" | ||
| 33 | "github.com/bnema/vev/internal/platform" | 32 | "github.com/bnema/vev/internal/platform" | ||
| @@ -71,10 +70,7 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, int) { | |||||
| 71 | view := picker.SessionView{ID: s.id, Name: s.name, Active: s.active, Tabs: make([]string, len(s.tabs))} | 70 | view := picker.SessionView{ID: s.id, Name: s.name, Active: s.active, Tabs: make([]string, len(s.tabs))} | ||
| 72 | sessionAttention := false | 71 | sessionAttention := false | ||
| 73 | for i, tb := range s.tabs { | 72 | for i, tb := range s.tabs { | ||
| 74 | - | label := tb.name | 73 | + | label := tabDisplayName(tb, i) |
| 75 | - | if label == "" { | |||
| 76 | - | label = strconv.Itoa(i + 1) | |||
| 77 | - | } | |||
| 78 | if tb.attention { | 74 | if tb.attention { | ||
| 79 | label = attentionSuffix(label) | 75 | label = attentionSuffix(label) | ||
| 80 | sessionAttention = true | 76 | sessionAttention = true | ||
M internal/usecase/daemon/session_test.go +40 -0
| @@ -701,6 +701,46 @@ func TestRenameTabPersistsForNamedSession(t *testing.T) { | |||
| 701 | 701 | require.Equal(t, []string{"shell"}, records[0].TabNames) | |
| 702 | 702 | } | |
| 703 | 703 | ||
| 704 | + | func TestTabNamePersistenceTracksTabIndexShifts(t *testing.T) { | |
| 705 | + | tests := []struct { | |
| 706 | + | name string | |
| 707 | + | tabNames []string | |
| 708 | + | closeIndex int | |
| 709 | + | want []string | |
| 710 | + | }{ | |
| 711 | + | {name: "close before named tab", tabNames: []string{"shell", "", "logs"}, closeIndex: 0, want: []string{"", "logs"}}, | |
| 712 | + | {name: "close after named tab", tabNames: []string{"shell", "logs", ""}, closeIndex: 2, want: []string{"shell", "logs"}}, | |
| 713 | + | } | |
| 714 | + | for _, tt := range tests { | |
| 715 | + | t.Run(tt.name, func(t *testing.T) { | |
| 716 | + | ptys := make([]ports.PTY, 3) | |
| 717 | + | for i := range ptys { | |
| 718 | + | p, release := newBlockingPTY(t) | |
| 719 | + | ptys[i] = p | |
| 720 | + | t.Cleanup(release) | |
| 721 | + | } | |
| 722 | + | store, _ := newMockStore(t) | |
| 723 | + | d := newTestDaemon(t, newFactorySeq(t, ptys...), stubClock{}) | |
| 724 | + | WithStore(store)(d) | |
| 725 | + | sz := domain.Size{Cols: 80, Rows: 24} | |
| 726 | + | ||
| 727 | + | sess, err := d.createSessionLocked("work", false, "/tmp/work", sz, nil) | |
| 728 | + | require.NoError(t, err) | |
| 729 | + | require.NoError(t, d.createTab(sess, sz)) | |
| 730 | + | require.NoError(t, d.createTab(sess, sz)) | |
| 731 | + | for i, name := range tt.tabNames { | |
| 732 | + | require.NoError(t, d.renameTab(sess, sess.tabs[i], name)) | |
| 733 | + | } | |
| 734 | + | ||
| 735 | + | d.closeTab(sess, sess.tabs[tt.closeIndex], false) | |
| 736 | + | records, err := d.persist.LoadAll() | |
| 737 | + | require.NoError(t, err) | |
| 738 | + | require.Len(t, records, 1) | |
| 739 | + | require.Equal(t, tt.want, records[0].TabNames) | |
| 740 | + | }) | |
| 741 | + | } | |
| 742 | + | } | |
| 743 | + | ||
| 704 | 744 | func TestRenameTabDoesNotPersistForEphemeralSession(t *testing.T) { | |
| 705 | 745 | sz := domain.Size{Cols: 80, Rows: 24} | |
| 706 | 746 | p, release := newBlockingPTY(t) | |
| @@ -701,6 +701,46 @@ func TestRenameTabPersistsForNamedSession(t *testing.T) { | |||||
| 701 | require.Equal(t, []string{"shell"}, records[0].TabNames) | 701 | require.Equal(t, []string{"shell"}, records[0].TabNames) | ||
| 702 | } | 702 | } | ||
| 703 | 703 | ||||
| 704 | + | func TestTabNamePersistenceTracksTabIndexShifts(t *testing.T) { | |||
| 705 | + | tests := []struct { | |||
| 706 | + | name string | |||
| 707 | + | tabNames []string | |||
| 708 | + | closeIndex int | |||
| 709 | + | want []string | |||
| 710 | + | }{ | |||
| 711 | + | {name: "close before named tab", tabNames: []string{"shell", "", "logs"}, closeIndex: 0, want: []string{"", "logs"}}, | |||
| 712 | + | {name: "close after named tab", tabNames: []string{"shell", "logs", ""}, closeIndex: 2, want: []string{"shell", "logs"}}, | |||
| 713 | + | } | |||
| 714 | + | for _, tt := range tests { | |||
| 715 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 716 | + | ptys := make([]ports.PTY, 3) | |||
| 717 | + | for i := range ptys { | |||
| 718 | + | p, release := newBlockingPTY(t) | |||
| 719 | + | ptys[i] = p | |||
| 720 | + | t.Cleanup(release) | |||
| 721 | + | } | |||
| 722 | + | store, _ := newMockStore(t) | |||
| 723 | + | d := newTestDaemon(t, newFactorySeq(t, ptys...), stubClock{}) | |||
| 724 | + | WithStore(store)(d) | |||
| 725 | + | sz := domain.Size{Cols: 80, Rows: 24} | |||
| 726 | + | ||||
| 727 | + | sess, err := d.createSessionLocked("work", false, "/tmp/work", sz, nil) | |||
| 728 | + | require.NoError(t, err) | |||
| 729 | + | require.NoError(t, d.createTab(sess, sz)) | |||
| 730 | + | require.NoError(t, d.createTab(sess, sz)) | |||
| 731 | + | for i, name := range tt.tabNames { | |||
| 732 | + | require.NoError(t, d.renameTab(sess, sess.tabs[i], name)) | |||
| 733 | + | } | |||
| 734 | + | ||||
| 735 | + | d.closeTab(sess, sess.tabs[tt.closeIndex], false) | |||
| 736 | + | records, err := d.persist.LoadAll() | |||
| 737 | + | require.NoError(t, err) | |||
| 738 | + | require.Len(t, records, 1) | |||
| 739 | + | require.Equal(t, tt.want, records[0].TabNames) | |||
| 740 | + | }) | |||
| 741 | + | } | |||
| 742 | + | } | |||
| 743 | + | ||||
| 704 | func TestRenameTabDoesNotPersistForEphemeralSession(t *testing.T) { | 744 | func TestRenameTabDoesNotPersistForEphemeralSession(t *testing.T) { | ||
| 705 | sz := domain.Size{Cols: 80, Rows: 24} | 745 | sz := domain.Size{Cols: 80, Rows: 24} | ||
| 706 | p, release := newBlockingPTY(t) | 746 | p, release := newBlockingPTY(t) | ||
M internal/usecase/daemon/status.go +8 -5
| @@ -141,15 +141,18 @@ func (s *session) statusSegments() statusSnapshot { | |||
| 141 | 141 | } | |
| 142 | 142 | snap := statusSnapshot{session: name, tabs: make([]statusTab, len(s.tabs))} | |
| 143 | 143 | for i, tb := range s.tabs { | |
| 144 | - | name := tb.name | |
| 145 | - | if name == "" { | |
| 146 | - | name = strconv.Itoa(i + 1) | |
| 147 | - | } | |
| 148 | - | snap.tabs[i] = statusTab{name: name, active: i == s.active, attention: tb.attention} | |
| 144 | + | snap.tabs[i] = statusTab{name: tabDisplayName(tb, i), active: i == s.active, attention: tb.attention} | |
| 149 | 145 | } | |
| 150 | 146 | return snap | |
| 151 | 147 | } | |
| 152 | 148 | ||
| 149 | + | func tabDisplayName(tb *tab, index int) string { | |
| 150 | + | if tb.name != "" { | |
| 151 | + | return tb.name | |
| 152 | + | } | |
| 153 | + | return strconv.Itoa(index + 1) | |
| 154 | + | } | |
| 155 | + | ||
| 153 | 156 | func (d *Daemon) barStateFor(cur *session, copyFeedback string) barState { | |
| 154 | 157 | state := barState{copyFeedback: copyFeedback} | |
| 155 | 158 | if d != nil { | |
| @@ -141,15 +141,18 @@ func (s *session) statusSegments() statusSnapshot { | |||||
| 141 | } | 141 | } | ||
| 142 | snap := statusSnapshot{session: name, tabs: make([]statusTab, len(s.tabs))} | 142 | snap := statusSnapshot{session: name, tabs: make([]statusTab, len(s.tabs))} | ||
| 143 | for i, tb := range s.tabs { | 143 | for i, tb := range s.tabs { | ||
| 144 | - | name := tb.name | 144 | + | snap.tabs[i] = statusTab{name: tabDisplayName(tb, i), active: i == s.active, attention: tb.attention} |
| 145 | - | if name == "" { | |||
| 146 | - | name = strconv.Itoa(i + 1) | |||
| 147 | - | } | |||
| 148 | - | snap.tabs[i] = statusTab{name: name, active: i == s.active, attention: tb.attention} | |||
| 149 | } | 145 | } | ||
| 150 | return snap | 146 | return snap | ||
| 151 | } | 147 | } | ||
| 152 | 148 | ||||
| 149 | + | func tabDisplayName(tb *tab, index int) string { | |||
| 150 | + | if tb.name != "" { | |||
| 151 | + | return tb.name | |||
| 152 | + | } | |||
| 153 | + | return strconv.Itoa(index + 1) | |||
| 154 | + | } | |||
| 155 | + | ||||
| 153 | func (d *Daemon) barStateFor(cur *session, copyFeedback string) barState { | 156 | func (d *Daemon) barStateFor(cur *session, copyFeedback string) barState { | ||
| 154 | state := barState{copyFeedback: copyFeedback} | 157 | state := barState{copyFeedback: copyFeedback} | ||
| 155 | if d != nil { | 158 | if d != nil { | ||
M internal/usecase/daemon/status_test.go +17 -0
| @@ -49,6 +49,23 @@ func TestStatusCompositionGolden(t *testing.T) { | |||
| 49 | 49 | } | |
| 50 | 50 | } | |
| 51 | 51 | ||
| 52 | + | func TestTabDisplayName(t *testing.T) { | |
| 53 | + | tests := []struct { | |
| 54 | + | name string | |
| 55 | + | tab *tab | |
| 56 | + | index int | |
| 57 | + | want string | |
| 58 | + | }{ | |
| 59 | + | {name: "custom name", tab: &tab{name: "logs"}, index: 3, want: "logs"}, | |
| 60 | + | {name: "numeric fallback", tab: &tab{}, index: 3, want: "4"}, | |
| 61 | + | } | |
| 62 | + | for _, tt := range tests { | |
| 63 | + | t.Run(tt.name, func(t *testing.T) { | |
| 64 | + | require.Equal(t, tt.want, tabDisplayName(tt.tab, tt.index)) | |
| 65 | + | }) | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | ||
| 52 | 69 | func TestStatusCompositionUsesTruecolorTheme(t *testing.T) { | |
| 53 | 70 | p, release := newBlockingPTY(t) | |
| 54 | 71 | _, sess, ac, _ := newManualSessionWithPTYs(t, p) | |
| @@ -49,6 +49,23 @@ func TestStatusCompositionGolden(t *testing.T) { | |||||
| 49 | } | 49 | } | ||
| 50 | } | 50 | } | ||
| 51 | 51 | ||||
| 52 | + | func TestTabDisplayName(t *testing.T) { | |||
| 53 | + | tests := []struct { | |||
| 54 | + | name string | |||
| 55 | + | tab *tab | |||
| 56 | + | index int | |||
| 57 | + | want string | |||
| 58 | + | }{ | |||
| 59 | + | {name: "custom name", tab: &tab{name: "logs"}, index: 3, want: "logs"}, | |||
| 60 | + | {name: "numeric fallback", tab: &tab{}, index: 3, want: "4"}, | |||
| 61 | + | } | |||
| 62 | + | for _, tt := range tests { | |||
| 63 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 64 | + | require.Equal(t, tt.want, tabDisplayName(tt.tab, tt.index)) | |||
| 65 | + | }) | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | ||||
| 52 | func TestStatusCompositionUsesTruecolorTheme(t *testing.T) { | 69 | func TestStatusCompositionUsesTruecolorTheme(t *testing.T) { | ||
| 53 | p, release := newBlockingPTY(t) | 70 | p, release := newBlockingPTY(t) | ||
| 54 | _, sess, ac, _ := newManualSessionWithPTYs(t, p) | 71 | _, sess, ac, _ := newManualSessionWithPTYs(t, p) | ||