fix: deduplicate remote sessions in quick switch (#215)
* Deduplicate remote palette sessions * Refactor remote palette matching tests
- commit
- b6054449f1449787b7ec9253b4a9aa07f3ede5d1
- author
- bnema · · 2026-08-26 05:07 UTC
- committer
- GitHub · · 2026-08-26 05:07 UTC
- parent
- a20dc2b
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
2 files · +84 -67
M internal/usecase/daemon/palette.go +22 -1
| @@ -66,6 +66,12 @@ type paletteSessionIdentity struct { | |||
| 66 | 66 | lifecycle domain.SessionLifecycleID | |
| 67 | 67 | } | |
| 68 | 68 | ||
| 69 | + | type remotePalettePresentationIdentity struct { | |
| 70 | + | origin string | |
| 71 | + | name string | |
| 72 | + | lifecycle domain.SessionLifecycleID | |
| 73 | + | } | |
| 74 | + | ||
| 69 | 75 | func localPaletteSessionIdentity(lifecycle domain.SessionLifecycleID) paletteSessionIdentity { | |
| 70 | 76 | return paletteSessionIdentity{kind: ports.RouteKindLocal, endpoint: "local", lifecycle: lifecycle} | |
| 71 | 77 | } | |
| @@ -137,6 +143,7 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command, ro | |||
| 137 | 143 | result palette.Result | |
| 138 | 144 | } | |
| 139 | 145 | discovered := make([]discoveredRemote, 0, remoteSessionCount) | |
| 146 | + | discoveredByPresentation := make(map[remotePalettePresentationIdentity][]paletteSessionIdentity, remoteSessionCount) | |
| 140 | 147 | for _, host := range remoteCatalog { | |
| 141 | 148 | for _, session := range host.entry.Sessions { | |
| 142 | 149 | if session.Ephemeral { | |
| @@ -148,10 +155,15 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command, ro | |||
| 148 | 155 | if key.Validate() != nil || target.Validate() != nil { | |
| 149 | 156 | continue | |
| 150 | 157 | } | |
| 158 | + | identity := remotePaletteSessionIdentity(key.Host, session.LifecycleID) | |
| 159 | + | presentation := remotePalettePresentationIdentity{ | |
| 160 | + | origin: key.DisplayOrigin, name: session.Name, lifecycle: session.LifecycleID, | |
| 161 | + | } | |
| 151 | 162 | discovered = append(discovered, discoveredRemote{ | |
| 152 | - | identity: remotePaletteSessionIdentity(key.Host, session.LifecycleID), | |
| 163 | + | identity: identity, | |
| 153 | 164 | result: palette.NewRemoteSessionResult(key, target, remotePaletteUnavailableReason(host.status, session)), | |
| 154 | 165 | }) | |
| 166 | + | discoveredByPresentation[presentation] = append(discoveredByPresentation[presentation], identity) | |
| 155 | 167 | } | |
| 156 | 168 | } | |
| 157 | 169 | formattedRoutes := formatRecentRouteSnapshot(routeSnapshot) | |
| @@ -174,6 +186,15 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command, ro | |||
| 174 | 186 | })) | |
| 175 | 187 | if entry.Kind == ports.RouteKindLocal { | |
| 176 | 188 | represented[identity] = struct{}{} | |
| 189 | + | continue | |
| 190 | + | } | |
| 191 | + | presentation := remotePalettePresentationIdentity{ | |
| 192 | + | origin: domain.RemoteDisplayOrigin(entry.HostLabel), | |
| 193 | + | name: entry.Target.SessionName, | |
| 194 | + | lifecycle: entry.Target.LifecycleID, | |
| 195 | + | } | |
| 196 | + | if matches := discoveredByPresentation[presentation]; len(matches) == 1 { | |
| 197 | + | represented[matches[0]] = struct{}{} | |
| 177 | 198 | } | |
| 178 | 199 | } | |
| 179 | 200 | for _, remote := range discovered { | |
| @@ -66,6 +66,12 @@ type paletteSessionIdentity struct { | |||||
| 66 | lifecycle domain.SessionLifecycleID | 66 | lifecycle domain.SessionLifecycleID | ||
| 67 | } | 67 | } | ||
| 68 | 68 | ||||
| 69 | + | type remotePalettePresentationIdentity struct { | |||
| 70 | + | origin string | |||
| 71 | + | name string | |||
| 72 | + | lifecycle domain.SessionLifecycleID | |||
| 73 | + | } | |||
| 74 | + | ||||
| 69 | func localPaletteSessionIdentity(lifecycle domain.SessionLifecycleID) paletteSessionIdentity { | 75 | func localPaletteSessionIdentity(lifecycle domain.SessionLifecycleID) paletteSessionIdentity { | ||
| 70 | return paletteSessionIdentity{kind: ports.RouteKindLocal, endpoint: "local", lifecycle: lifecycle} | 76 | return paletteSessionIdentity{kind: ports.RouteKindLocal, endpoint: "local", lifecycle: lifecycle} | ||
| 71 | } | 77 | } | ||
| @@ -137,6 +143,7 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command, ro | |||||
| 137 | result palette.Result | 143 | result palette.Result | ||
| 138 | } | 144 | } | ||
| 139 | discovered := make([]discoveredRemote, 0, remoteSessionCount) | 145 | discovered := make([]discoveredRemote, 0, remoteSessionCount) | ||
| 146 | + | discoveredByPresentation := make(map[remotePalettePresentationIdentity][]paletteSessionIdentity, remoteSessionCount) | |||
| 140 | for _, host := range remoteCatalog { | 147 | for _, host := range remoteCatalog { | ||
| 141 | for _, session := range host.entry.Sessions { | 148 | for _, session := range host.entry.Sessions { | ||
| 142 | if session.Ephemeral { | 149 | if session.Ephemeral { | ||
| @@ -148,10 +155,15 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command, ro | |||||
| 148 | if key.Validate() != nil || target.Validate() != nil { | 155 | if key.Validate() != nil || target.Validate() != nil { | ||
| 149 | continue | 156 | continue | ||
| 150 | } | 157 | } | ||
| 158 | + | identity := remotePaletteSessionIdentity(key.Host, session.LifecycleID) | |||
| 159 | + | presentation := remotePalettePresentationIdentity{ | |||
| 160 | + | origin: key.DisplayOrigin, name: session.Name, lifecycle: session.LifecycleID, | |||
| 161 | + | } | |||
| 151 | discovered = append(discovered, discoveredRemote{ | 162 | discovered = append(discovered, discoveredRemote{ | ||
| 152 | - | identity: remotePaletteSessionIdentity(key.Host, session.LifecycleID), | 163 | + | identity: identity, |
| 153 | result: palette.NewRemoteSessionResult(key, target, remotePaletteUnavailableReason(host.status, session)), | 164 | result: palette.NewRemoteSessionResult(key, target, remotePaletteUnavailableReason(host.status, session)), | ||
| 154 | }) | 165 | }) | ||
| 166 | + | discoveredByPresentation[presentation] = append(discoveredByPresentation[presentation], identity) | |||
| 155 | } | 167 | } | ||
| 156 | } | 168 | } | ||
| 157 | formattedRoutes := formatRecentRouteSnapshot(routeSnapshot) | 169 | formattedRoutes := formatRecentRouteSnapshot(routeSnapshot) | ||
| @@ -174,6 +186,15 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command, ro | |||||
| 174 | })) | 186 | })) | ||
| 175 | if entry.Kind == ports.RouteKindLocal { | 187 | if entry.Kind == ports.RouteKindLocal { | ||
| 176 | represented[identity] = struct{}{} | 188 | represented[identity] = struct{}{} | ||
| 189 | + | continue | |||
| 190 | + | } | |||
| 191 | + | presentation := remotePalettePresentationIdentity{ | |||
| 192 | + | origin: domain.RemoteDisplayOrigin(entry.HostLabel), | |||
| 193 | + | name: entry.Target.SessionName, | |||
| 194 | + | lifecycle: entry.Target.LifecycleID, | |||
| 195 | + | } | |||
| 196 | + | if matches := discoveredByPresentation[presentation]; len(matches) == 1 { | |||
| 197 | + | represented[matches[0]] = struct{}{} | |||
| 177 | } | 198 | } | ||
| 178 | } | 199 | } | ||
| 179 | for _, remote := range discovered { | 200 | for _, remote := range discovered { | ||
M internal/usecase/daemon/palette_test.go +62 -66
| @@ -464,36 +464,71 @@ func TestPaletteIncludesExactRemoteCatalogTargetBesideSameNameLocalSession(t *te | |||
| 464 | 464 | }, remoteTarget) | |
| 465 | 465 | } | |
| 466 | 466 | ||
| 467 | - | func TestPaletteKeepsExactCatalogEndpointsWithEqualDisplayAndLifecycle(t *testing.T) { | |
| 468 | - | now := time.Unix(1_000, 0) | |
| 469 | - | d := newTestDaemon(t, nil, fixedRemoteRefreshClock{now: now}) | |
| 470 | - | current := addControlSession(d, "current", "tab-1", "pane-1") | |
| 471 | - | current.ephemeral = false | |
| 472 | - | lifecycle := domain.SessionLifecycleID{22} | |
| 473 | - | d.remoteCatalog.mu.Lock() | |
| 474 | - | for _, endpoint := range []string{"user@arch", "admin@arch"} { | |
| 475 | - | d.remoteCatalog.cache[endpoint] = ports.RemoteCatalogCacheEntry{ | |
| 476 | - | Host: endpoint, FetchedAt: now, | |
| 477 | - | Sessions: []ports.RemoteCatalogSession{{ | |
| 478 | - | LifecycleID: lifecycle, Name: "vev", State: ports.RemoteCatalogSessionUp, | |
| 479 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-vev", Index: 0}}, ActiveTabID: "tab-vev", | |
| 480 | - | }}, | |
| 481 | - | } | |
| 482 | - | d.remoteCatalog.status[endpoint] = remoteHostFresh | |
| 467 | + | func TestPaletteMatchesRecentRemoteRouteToCatalog(t *testing.T) { | |
| 468 | + | tests := []struct { | |
| 469 | + | name string | |
| 470 | + | catalogEndpoints []string | |
| 471 | + | wantCatalogEndpoints []string | |
| 472 | + | }{ | |
| 473 | + | { | |
| 474 | + | name: "unique catalog match is deduplicated", | |
| 475 | + | catalogEndpoints: []string{"arch"}, | |
| 476 | + | }, | |
| 477 | + | { | |
| 478 | + | name: "ambiguous catalog matches remain selectable", | |
| 479 | + | catalogEndpoints: []string{"user@arch", "admin@arch"}, | |
| 480 | + | wantCatalogEndpoints: []string{"user@arch", "admin@arch"}, | |
| 481 | + | }, | |
| 483 | 482 | } | |
| 484 | - | d.remoteCatalog.mu.Unlock() | |
| 485 | 483 | ||
| 486 | - | results := d.paletteResults(current, nil, ports.RecentRouteSnapshot{}) | |
| 487 | - | var endpoints []string | |
| 488 | - | for _, result := range results { | |
| 489 | - | if result.DisplayText() != "Switch to session vev@arch" { | |
| 490 | - | continue | |
| 491 | - | } | |
| 492 | - | target, ok := result.RemoteSessionTarget() | |
| 493 | - | require.True(t, ok) | |
| 494 | - | endpoints = append(endpoints, target.Endpoint) | |
| 484 | + | for _, test := range tests { | |
| 485 | + | t.Run(test.name, func(t *testing.T) { | |
| 486 | + | now := time.Unix(1_000, 0) | |
| 487 | + | d := newTestDaemon(t, nil, fixedRemoteRefreshClock{now: now}) | |
| 488 | + | current := addControlSession(d, "current", "tab-1", "pane-1") | |
| 489 | + | current.ephemeral = false | |
| 490 | + | lifecycle := domain.SessionLifecycleID{22} | |
| 491 | + | d.remoteCatalog.mu.Lock() | |
| 492 | + | for _, endpoint := range test.catalogEndpoints { | |
| 493 | + | d.remoteCatalog.cache[endpoint] = ports.RemoteCatalogCacheEntry{ | |
| 494 | + | Host: endpoint, FetchedAt: now, | |
| 495 | + | Sessions: []ports.RemoteCatalogSession{{ | |
| 496 | + | LifecycleID: lifecycle, Name: "vev", State: ports.RemoteCatalogSessionUp, | |
| 497 | + | Tabs: []ports.RemoteCatalogTab{{ID: "tab-vev", Index: 0}}, ActiveTabID: "tab-vev", | |
| 498 | + | }}, | |
| 499 | + | } | |
| 500 | + | d.remoteCatalog.status[endpoint] = remoteHostFresh | |
| 501 | + | } | |
| 502 | + | d.remoteCatalog.mu.Unlock() | |
| 503 | + | ||
| 504 | + | results := d.paletteResults(current, nil, ports.RecentRouteSnapshot{ | |
| 505 | + | Generation: 2, | |
| 506 | + | Entries: []ports.RecentRouteEntry{{ | |
| 507 | + | Key: 3, Generation: 1, | |
| 508 | + | Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "vev"}, | |
| 509 | + | Name: "vev", HostLabel: "arch", Kind: ports.RouteKindRemote, | |
| 510 | + | }}, | |
| 511 | + | }) | |
| 512 | + | var actions []ports.RouteNavigationAction | |
| 513 | + | var catalogEndpoints []string | |
| 514 | + | for _, result := range results { | |
| 515 | + | if result.DisplayText() != "Switch to session vev@arch" { | |
| 516 | + | continue | |
| 517 | + | } | |
| 518 | + | if action, ok := result.RouteNavigationAction(); ok { | |
| 519 | + | actions = append(actions, action) | |
| 520 | + | continue | |
| 521 | + | } | |
| 522 | + | target, ok := result.RemoteSessionTarget() | |
| 523 | + | require.True(t, ok) | |
| 524 | + | catalogEndpoints = append(catalogEndpoints, target.Endpoint) | |
| 525 | + | } | |
| 526 | + | require.Equal(t, []ports.RouteNavigationAction{{ | |
| 527 | + | SnapshotGeneration: 2, Key: 3, Generation: 1, | |
| 528 | + | }}, actions) | |
| 529 | + | require.ElementsMatch(t, test.wantCatalogEndpoints, catalogEndpoints) | |
| 530 | + | }) | |
| 495 | 531 | } | |
| 496 | - | require.ElementsMatch(t, []string{"user@arch", "admin@arch"}, endpoints) | |
| 497 | 532 | } | |
| 498 | 533 | ||
| 499 | 534 | func TestPaletteRemoteCatalogSelectionSendsExactAttachTarget(t *testing.T) { | |
| @@ -642,45 +677,6 @@ func TestRemoteRefreshTracksPickerAndPaletteOnSameAttachment(t *testing.T) { | |||
| 642 | 677 | d.remoteCatalog.mu.Unlock() | |
| 643 | 678 | } | |
| 644 | 679 | ||
| 645 | - | func TestPaletteKeepsCatalogTargetWhenRecentRouteHasOnlyDisplayOrigin(t *testing.T) { | |
| 646 | - | now := time.Unix(1_200, 0) | |
| 647 | - | d := newTestDaemon(t, nil, fixedRemoteRefreshClock{now: now}) | |
| 648 | - | current := addControlSession(d, "current", "tab-1", "pane-1") | |
| 649 | - | current.ephemeral = false | |
| 650 | - | lifecycle := domain.SessionLifecycleID{51} | |
| 651 | - | d.remoteCatalog.mu.Lock() | |
| 652 | - | d.remoteCatalog.cache["arch"] = ports.RemoteCatalogCacheEntry{ | |
| 653 | - | Host: "arch", FetchedAt: now, | |
| 654 | - | Sessions: []ports.RemoteCatalogSession{{ | |
| 655 | - | LifecycleID: lifecycle, Name: "vev", State: ports.RemoteCatalogSessionUp, | |
| 656 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-vev", Index: 0}}, ActiveTabID: "tab-vev", | |
| 657 | - | }}, | |
| 658 | - | } | |
| 659 | - | d.remoteCatalog.status["arch"] = remoteHostFresh | |
| 660 | - | d.remoteCatalog.mu.Unlock() | |
| 661 | - | ||
| 662 | - | results := d.paletteResults(current, nil, ports.RecentRouteSnapshot{ | |
| 663 | - | Generation: 2, | |
| 664 | - | Entries: []ports.RecentRouteEntry{{ | |
| 665 | - | Key: 3, Generation: 1, | |
| 666 | - | Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "vev"}, | |
| 667 | - | Name: "vev", HostLabel: "arch", Kind: ports.RouteKindRemote, | |
| 668 | - | }}, | |
| 669 | - | }) | |
| 670 | - | var matching []palette.Result | |
| 671 | - | for _, result := range results { | |
| 672 | - | if result.DisplayText() == "Switch to session vev@arch" { | |
| 673 | - | matching = append(matching, result) | |
| 674 | - | } | |
| 675 | - | } | |
| 676 | - | require.Len(t, matching, 2, "presentation-only route origin cannot deduplicate an exact catalog endpoint") | |
| 677 | - | action, routeOK := matching[0].RouteNavigationAction() | |
| 678 | - | _, catalogOK := matching[1].RemoteSessionTarget() | |
| 679 | - | require.True(t, routeOK) | |
| 680 | - | require.True(t, catalogOK) | |
| 681 | - | require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 2, Key: 3, Generation: 1}, action) | |
| 682 | - | } | |
| 683 | - | ||
| 684 | 680 | func TestPaletteResultsDeduplicateByLifecycleAndKeepEqualLabels(t *testing.T) { | |
| 685 | 681 | d := newTestDaemon(t, nil, stubClock{}) | |
| 686 | 682 | current := addControlSession(d, "hi", "tab-1", "pane-1") | |
| @@ -464,36 +464,71 @@ func TestPaletteIncludesExactRemoteCatalogTargetBesideSameNameLocalSession(t *te | |||||
| 464 | }, remoteTarget) | 464 | }, remoteTarget) | ||
| 465 | } | 465 | } | ||
| 466 | 466 | ||||
| 467 | - | func TestPaletteKeepsExactCatalogEndpointsWithEqualDisplayAndLifecycle(t *testing.T) { | 467 | + | func TestPaletteMatchesRecentRemoteRouteToCatalog(t *testing.T) { |
| 468 | - | now := time.Unix(1_000, 0) | 468 | + | tests := []struct { |
| 469 | - | d := newTestDaemon(t, nil, fixedRemoteRefreshClock{now: now}) | 469 | + | name string |
| 470 | - | current := addControlSession(d, "current", "tab-1", "pane-1") | 470 | + | catalogEndpoints []string |
| 471 | - | current.ephemeral = false | 471 | + | wantCatalogEndpoints []string |
| 472 | - | lifecycle := domain.SessionLifecycleID{22} | 472 | + | }{ |
| 473 | - | d.remoteCatalog.mu.Lock() | 473 | + | { |
| 474 | - | for _, endpoint := range []string{"user@arch", "admin@arch"} { | 474 | + | name: "unique catalog match is deduplicated", |
| 475 | - | d.remoteCatalog.cache[endpoint] = ports.RemoteCatalogCacheEntry{ | 475 | + | catalogEndpoints: []string{"arch"}, |
| 476 | - | Host: endpoint, FetchedAt: now, | 476 | + | }, |
| 477 | - | Sessions: []ports.RemoteCatalogSession{{ | 477 | + | { |
| 478 | - | LifecycleID: lifecycle, Name: "vev", State: ports.RemoteCatalogSessionUp, | 478 | + | name: "ambiguous catalog matches remain selectable", |
| 479 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-vev", Index: 0}}, ActiveTabID: "tab-vev", | 479 | + | catalogEndpoints: []string{"user@arch", "admin@arch"}, |
| 480 | - | }}, | 480 | + | wantCatalogEndpoints: []string{"user@arch", "admin@arch"}, |
| 481 | - | } | 481 | + | }, |
| 482 | - | d.remoteCatalog.status[endpoint] = remoteHostFresh | |||
| 483 | } | 482 | } | ||
| 484 | - | d.remoteCatalog.mu.Unlock() | |||
| 485 | 483 | ||||
| 486 | - | results := d.paletteResults(current, nil, ports.RecentRouteSnapshot{}) | 484 | + | for _, test := range tests { |
| 487 | - | var endpoints []string | 485 | + | t.Run(test.name, func(t *testing.T) { |
| 488 | - | for _, result := range results { | 486 | + | now := time.Unix(1_000, 0) |
| 489 | - | if result.DisplayText() != "Switch to session vev@arch" { | 487 | + | d := newTestDaemon(t, nil, fixedRemoteRefreshClock{now: now}) |
| 490 | - | continue | 488 | + | current := addControlSession(d, "current", "tab-1", "pane-1") |
| 491 | - | } | 489 | + | current.ephemeral = false |
| 492 | - | target, ok := result.RemoteSessionTarget() | 490 | + | lifecycle := domain.SessionLifecycleID{22} |
| 493 | - | require.True(t, ok) | 491 | + | d.remoteCatalog.mu.Lock() |
| 494 | - | endpoints = append(endpoints, target.Endpoint) | 492 | + | for _, endpoint := range test.catalogEndpoints { |
| 493 | + | d.remoteCatalog.cache[endpoint] = ports.RemoteCatalogCacheEntry{ | |||
| 494 | + | Host: endpoint, FetchedAt: now, | |||
| 495 | + | Sessions: []ports.RemoteCatalogSession{{ | |||
| 496 | + | LifecycleID: lifecycle, Name: "vev", State: ports.RemoteCatalogSessionUp, | |||
| 497 | + | Tabs: []ports.RemoteCatalogTab{{ID: "tab-vev", Index: 0}}, ActiveTabID: "tab-vev", | |||
| 498 | + | }}, | |||
| 499 | + | } | |||
| 500 | + | d.remoteCatalog.status[endpoint] = remoteHostFresh | |||
| 501 | + | } | |||
| 502 | + | d.remoteCatalog.mu.Unlock() | |||
| 503 | + | ||||
| 504 | + | results := d.paletteResults(current, nil, ports.RecentRouteSnapshot{ | |||
| 505 | + | Generation: 2, | |||
| 506 | + | Entries: []ports.RecentRouteEntry{{ | |||
| 507 | + | Key: 3, Generation: 1, | |||
| 508 | + | Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "vev"}, | |||
| 509 | + | Name: "vev", HostLabel: "arch", Kind: ports.RouteKindRemote, | |||
| 510 | + | }}, | |||
| 511 | + | }) | |||
| 512 | + | var actions []ports.RouteNavigationAction | |||
| 513 | + | var catalogEndpoints []string | |||
| 514 | + | for _, result := range results { | |||
| 515 | + | if result.DisplayText() != "Switch to session vev@arch" { | |||
| 516 | + | continue | |||
| 517 | + | } | |||
| 518 | + | if action, ok := result.RouteNavigationAction(); ok { | |||
| 519 | + | actions = append(actions, action) | |||
| 520 | + | continue | |||
| 521 | + | } | |||
| 522 | + | target, ok := result.RemoteSessionTarget() | |||
| 523 | + | require.True(t, ok) | |||
| 524 | + | catalogEndpoints = append(catalogEndpoints, target.Endpoint) | |||
| 525 | + | } | |||
| 526 | + | require.Equal(t, []ports.RouteNavigationAction{{ | |||
| 527 | + | SnapshotGeneration: 2, Key: 3, Generation: 1, | |||
| 528 | + | }}, actions) | |||
| 529 | + | require.ElementsMatch(t, test.wantCatalogEndpoints, catalogEndpoints) | |||
| 530 | + | }) | |||
| 495 | } | 531 | } | ||
| 496 | - | require.ElementsMatch(t, []string{"user@arch", "admin@arch"}, endpoints) | |||
| 497 | } | 532 | } | ||
| 498 | 533 | ||||
| 499 | func TestPaletteRemoteCatalogSelectionSendsExactAttachTarget(t *testing.T) { | 534 | func TestPaletteRemoteCatalogSelectionSendsExactAttachTarget(t *testing.T) { | ||
| @@ -642,45 +677,6 @@ func TestRemoteRefreshTracksPickerAndPaletteOnSameAttachment(t *testing.T) { | |||||
| 642 | d.remoteCatalog.mu.Unlock() | 677 | d.remoteCatalog.mu.Unlock() | ||
| 643 | } | 678 | } | ||
| 644 | 679 | ||||
| 645 | - | func TestPaletteKeepsCatalogTargetWhenRecentRouteHasOnlyDisplayOrigin(t *testing.T) { | |||
| 646 | - | now := time.Unix(1_200, 0) | |||
| 647 | - | d := newTestDaemon(t, nil, fixedRemoteRefreshClock{now: now}) | |||
| 648 | - | current := addControlSession(d, "current", "tab-1", "pane-1") | |||
| 649 | - | current.ephemeral = false | |||
| 650 | - | lifecycle := domain.SessionLifecycleID{51} | |||
| 651 | - | d.remoteCatalog.mu.Lock() | |||
| 652 | - | d.remoteCatalog.cache["arch"] = ports.RemoteCatalogCacheEntry{ | |||
| 653 | - | Host: "arch", FetchedAt: now, | |||
| 654 | - | Sessions: []ports.RemoteCatalogSession{{ | |||
| 655 | - | LifecycleID: lifecycle, Name: "vev", State: ports.RemoteCatalogSessionUp, | |||
| 656 | - | Tabs: []ports.RemoteCatalogTab{{ID: "tab-vev", Index: 0}}, ActiveTabID: "tab-vev", | |||
| 657 | - | }}, | |||
| 658 | - | } | |||
| 659 | - | d.remoteCatalog.status["arch"] = remoteHostFresh | |||
| 660 | - | d.remoteCatalog.mu.Unlock() | |||
| 661 | - | ||||
| 662 | - | results := d.paletteResults(current, nil, ports.RecentRouteSnapshot{ | |||
| 663 | - | Generation: 2, | |||
| 664 | - | Entries: []ports.RecentRouteEntry{{ | |||
| 665 | - | Key: 3, Generation: 1, | |||
| 666 | - | Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "vev"}, | |||
| 667 | - | Name: "vev", HostLabel: "arch", Kind: ports.RouteKindRemote, | |||
| 668 | - | }}, | |||
| 669 | - | }) | |||
| 670 | - | var matching []palette.Result | |||
| 671 | - | for _, result := range results { | |||
| 672 | - | if result.DisplayText() == "Switch to session vev@arch" { | |||
| 673 | - | matching = append(matching, result) | |||
| 674 | - | } | |||
| 675 | - | } | |||
| 676 | - | require.Len(t, matching, 2, "presentation-only route origin cannot deduplicate an exact catalog endpoint") | |||
| 677 | - | action, routeOK := matching[0].RouteNavigationAction() | |||
| 678 | - | _, catalogOK := matching[1].RemoteSessionTarget() | |||
| 679 | - | require.True(t, routeOK) | |||
| 680 | - | require.True(t, catalogOK) | |||
| 681 | - | require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 2, Key: 3, Generation: 1}, action) | |||
| 682 | - | } | |||
| 683 | - | ||||
| 684 | func TestPaletteResultsDeduplicateByLifecycleAndKeepEqualLabels(t *testing.T) { | 680 | func TestPaletteResultsDeduplicateByLifecycleAndKeepEqualLabels(t *testing.T) { | ||
| 685 | d := newTestDaemon(t, nil, stubClock{}) | 681 | d := newTestDaemon(t, nil, stubClock{}) | ||
| 686 | current := addControlSession(d, "hi", "tab-1", "pane-1") | 682 | current := addControlSession(d, "hi", "tab-1", "pane-1") | ||