fix(picker): bound stopped tab selectors
- commit
- 0c2bd9538de5829586b365332b186841b9c00c00
- author
- brice · · 2026-08-11 18:23 UTC
- parent
- 0380a23
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +20 -3
M internal/usecase/daemon/remote_picker.go +3 -1
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "encoding/base64" | |
| 6 | 6 | "errors" | |
| 7 | 7 | "fmt" | |
| 8 | + | "math" | |
| 8 | 9 | "sort" | |
| 9 | 10 | "time" | |
| 10 | 11 | ||
| @@ -166,7 +167,8 @@ func remotePickerView(key domain.RemoteSessionKey, session ports.RemoteCatalogSe | |||
| 166 | 167 | if first.ID != "" { | |
| 167 | 168 | target.StoppedTab = domain.NewStableTabSelector(domain.TabStableID(first.ID)) | |
| 168 | 169 | } else { | |
| 169 | - | target.StoppedTab = domain.NewOrdinalTabSelector(0, first.Name, uint16(len(tabs))) | |
| 170 | + | tabCount := min(len(tabs), math.MaxUint16) | |
| 171 | + | target.StoppedTab = domain.NewOrdinalTabSelector(0, first.Name, uint16(tabCount)) | |
| 170 | 172 | } | |
| 171 | 173 | } else { | |
| 172 | 174 | target.LiveTabID = domain.TabStableID(tabs[active].ID) | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "encoding/base64" | 5 | "encoding/base64" | ||
| 6 | "errors" | 6 | "errors" | ||
| 7 | "fmt" | 7 | "fmt" | ||
| 8 | + | "math" | |||
| 8 | "sort" | 9 | "sort" | ||
| 9 | "time" | 10 | "time" | ||
| 10 | 11 | ||||
| @@ -166,7 +167,8 @@ func remotePickerView(key domain.RemoteSessionKey, session ports.RemoteCatalogSe | |||||
| 166 | if first.ID != "" { | 167 | if first.ID != "" { | ||
| 167 | target.StoppedTab = domain.NewStableTabSelector(domain.TabStableID(first.ID)) | 168 | target.StoppedTab = domain.NewStableTabSelector(domain.TabStableID(first.ID)) | ||
| 168 | } else { | 169 | } else { | ||
| 169 | - | target.StoppedTab = domain.NewOrdinalTabSelector(0, first.Name, uint16(len(tabs))) | 170 | + | tabCount := min(len(tabs), math.MaxUint16) |
| 171 | + | target.StoppedTab = domain.NewOrdinalTabSelector(0, first.Name, uint16(tabCount)) | |||
| 170 | } | 172 | } | ||
| 171 | } else { | 173 | } else { | ||
| 172 | target.LiveTabID = domain.TabStableID(tabs[active].ID) | 174 | target.LiveTabID = domain.TabStableID(tabs[active].ID) | ||
M internal/usecase/daemon/remote_picker_test.go +13 -1
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | "io" | |
| 8 | 8 | "log/slog" | |
| 9 | + | "math" | |
| 9 | 10 | "sync" | |
| 10 | 11 | "testing" | |
| 11 | 12 | "time" | |
| @@ -84,6 +85,17 @@ func TestRemotePickerStoppedRowsUseCanonicalStateAndSafeSelection(t *testing.T) | |||
| 84 | 85 | } | |
| 85 | 86 | } | |
| 86 | 87 | ||
| 88 | + | func TestRemotePickerClampsStoppedOrdinalTabCount(t *testing.T) { | |
| 89 | + | tabs := make([]ports.RemoteCatalogTab, math.MaxUint16+1) | |
| 90 | + | tabs[0] = ports.RemoteCatalogTab{Name: "main"} | |
| 91 | + | view := remotePickerView(domain.RemoteSessionKey{Host: "arch", Name: "work"}, ports.RemoteCatalogSession{ | |
| 92 | + | LifecycleID: remoteLifecycleForTest(), Name: "work", State: "down", Tabs: tabs, | |
| 93 | + | }, remoteHostFresh, time.Unix(100, 0)) | |
| 94 | + | ||
| 95 | + | require.NotNil(t, view.RemoteTarget) | |
| 96 | + | require.Equal(t, uint16(math.MaxUint16), view.RemoteTarget.StoppedTab.ExpectedCount) | |
| 97 | + | } | |
| 98 | + | ||
| 87 | 99 | func TestRemotePickerUsesCompactUpDetailRegardlessOfAttachment(t *testing.T) { | |
| 88 | 100 | for _, attached := range []bool{false, true} { | |
| 89 | 101 | view := remotePickerView(domain.RemoteSessionKey{Host: "remote", Name: "work"}, ports.RemoteCatalogSession{ | |
| @@ -750,9 +762,9 @@ func TestRemotePickerSelectsStoppedRemoteTabAndRestoresIt(t *testing.T) { | |||
| 750 | 762 | EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, | |
| 751 | 763 | }, transport) | |
| 752 | 764 | require.NoError(t, err) | |
| 765 | + | t.Cleanup(func() { remote.clientGone(restored, attachment, transport, false) }) | |
| 753 | 766 | require.Equal(t, lifecycle, restored.incarnation) | |
| 754 | 767 | require.Equal(t, domain.TabStableID("tab-b"), attachment.viewSnapshot().tabID) | |
| 755 | - | remote.clientGone(restored, attachment, transport, false) | |
| 756 | 768 | } | |
| 757 | 769 | ||
| 758 | 770 | func TestNavigationActionHandoffSendsBoundedAction(t *testing.T) { | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "io" | 7 | "io" | ||
| 8 | "log/slog" | 8 | "log/slog" | ||
| 9 | + | "math" | |||
| 9 | "sync" | 10 | "sync" | ||
| 10 | "testing" | 11 | "testing" | ||
| 11 | "time" | 12 | "time" | ||
| @@ -84,6 +85,17 @@ func TestRemotePickerStoppedRowsUseCanonicalStateAndSafeSelection(t *testing.T) | |||||
| 84 | } | 85 | } | ||
| 85 | } | 86 | } | ||
| 86 | 87 | ||||
| 88 | + | func TestRemotePickerClampsStoppedOrdinalTabCount(t *testing.T) { | |||
| 89 | + | tabs := make([]ports.RemoteCatalogTab, math.MaxUint16+1) | |||
| 90 | + | tabs[0] = ports.RemoteCatalogTab{Name: "main"} | |||
| 91 | + | view := remotePickerView(domain.RemoteSessionKey{Host: "arch", Name: "work"}, ports.RemoteCatalogSession{ | |||
| 92 | + | LifecycleID: remoteLifecycleForTest(), Name: "work", State: "down", Tabs: tabs, | |||
| 93 | + | }, remoteHostFresh, time.Unix(100, 0)) | |||
| 94 | + | ||||
| 95 | + | require.NotNil(t, view.RemoteTarget) | |||
| 96 | + | require.Equal(t, uint16(math.MaxUint16), view.RemoteTarget.StoppedTab.ExpectedCount) | |||
| 97 | + | } | |||
| 98 | + | ||||
| 87 | func TestRemotePickerUsesCompactUpDetailRegardlessOfAttachment(t *testing.T) { | 99 | func TestRemotePickerUsesCompactUpDetailRegardlessOfAttachment(t *testing.T) { | ||
| 88 | for _, attached := range []bool{false, true} { | 100 | for _, attached := range []bool{false, true} { | ||
| 89 | view := remotePickerView(domain.RemoteSessionKey{Host: "remote", Name: "work"}, ports.RemoteCatalogSession{ | 101 | view := remotePickerView(domain.RemoteSessionKey{Host: "remote", Name: "work"}, ports.RemoteCatalogSession{ | ||
| @@ -750,9 +762,9 @@ func TestRemotePickerSelectsStoppedRemoteTabAndRestoresIt(t *testing.T) { | |||||
| 750 | EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, | 762 | EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, | ||
| 751 | }, transport) | 763 | }, transport) | ||
| 752 | require.NoError(t, err) | 764 | require.NoError(t, err) | ||
| 765 | + | t.Cleanup(func() { remote.clientGone(restored, attachment, transport, false) }) | |||
| 753 | require.Equal(t, lifecycle, restored.incarnation) | 766 | require.Equal(t, lifecycle, restored.incarnation) | ||
| 754 | require.Equal(t, domain.TabStableID("tab-b"), attachment.viewSnapshot().tabID) | 767 | require.Equal(t, domain.TabStableID("tab-b"), attachment.viewSnapshot().tabID) | ||
| 755 | - | remote.clientGone(restored, attachment, transport, false) | |||
| 756 | } | 768 | } | ||
| 757 | 769 | ||||
| 758 | func TestNavigationActionHandoffSendsBoundedAction(t *testing.T) { | 770 | func TestNavigationActionHandoffSendsBoundedAction(t *testing.T) { | ||
M internal/usecase/picker/model.go +4 -1
| @@ -1,6 +1,8 @@ | |||
| 1 | 1 | package picker | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "math" | |
| 5 | + | ||
| 4 | 6 | renderer "github.com/bnema/vev-vt" | |
| 5 | 7 | "github.com/bnema/vev/internal/domain" | |
| 6 | 8 | "github.com/bnema/vev/internal/usecase/ui" | |
| @@ -329,7 +331,8 @@ func rowsForSession(session SessionView, config SelectionConfig) []row { | |||
| 329 | 331 | if tab.TabID != "" { | |
| 330 | 332 | remoteTarget.StoppedTab = domain.NewStableTabSelector(tab.TabID) | |
| 331 | 333 | } else { | |
| 332 | - | remoteTarget.StoppedTab = domain.NewOrdinalTabSelector(uint16(i), tab.RawName, uint16(len(session.Tabs))) | |
| 334 | + | tabCount := min(len(session.Tabs), math.MaxUint16) | |
| 335 | + | remoteTarget.StoppedTab = domain.NewOrdinalTabSelector(uint16(i), tab.RawName, uint16(tabCount)) | |
| 333 | 336 | } | |
| 334 | 337 | } else { | |
| 335 | 338 | remoteTarget.StoppedTab = domain.TabSelector{} | |
| @@ -1,6 +1,8 @@ | |||||
| 1 | package picker | 1 | package picker | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "math" | |||
| 5 | + | ||||
| 4 | renderer "github.com/bnema/vev-vt" | 6 | renderer "github.com/bnema/vev-vt" | ||
| 5 | "github.com/bnema/vev/internal/domain" | 7 | "github.com/bnema/vev/internal/domain" | ||
| 6 | "github.com/bnema/vev/internal/usecase/ui" | 8 | "github.com/bnema/vev/internal/usecase/ui" | ||
| @@ -329,7 +331,8 @@ func rowsForSession(session SessionView, config SelectionConfig) []row { | |||||
| 329 | if tab.TabID != "" { | 331 | if tab.TabID != "" { | ||
| 330 | remoteTarget.StoppedTab = domain.NewStableTabSelector(tab.TabID) | 332 | remoteTarget.StoppedTab = domain.NewStableTabSelector(tab.TabID) | ||
| 331 | } else { | 333 | } else { | ||
| 332 | - | remoteTarget.StoppedTab = domain.NewOrdinalTabSelector(uint16(i), tab.RawName, uint16(len(session.Tabs))) | 334 | + | tabCount := min(len(session.Tabs), math.MaxUint16) |
| 335 | + | remoteTarget.StoppedTab = domain.NewOrdinalTabSelector(uint16(i), tab.RawName, uint16(tabCount)) | |||
| 333 | } | 336 | } | ||
| 334 | } else { | 337 | } else { | ||
| 335 | remoteTarget.StoppedTab = domain.TabSelector{} | 338 | remoteTarget.StoppedTab = domain.TabSelector{} | ||