detached ff4fca7

fix(client): bind hybrid picker selections to source routes (#236)

* test(client): cover hybrid local route identity

* refactor(client): bind picker handoffs to source routes
commit
ff4fca7fb823863bc2d37e7b187a043bcb72fcfb
author
bnema · · 2026-08-28 18:22 UTC
committer
GitHub · · 2026-08-28 18:22 UTC
parent
76f8a9f
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
2 files · +168 -34
M internal/usecase/client/client.go +51 -34
@@ -203,6 +203,19 @@ type attachRoute struct {
203203 resumeToken uint64
204204 }
205205
206+// attachHandoff binds a picker selection to the route that produced it. The
207+// connection-relative SamePeer hint is consumed before a target crosses this
208+// boundary; the bound source route is the sole transport authority afterward.
209+type attachHandoff struct {
210+ target protocol.AttachTarget
211+ source attachRoute
212+}
213+
214+func bindAttachHandoff(target protocol.AttachTarget, source attachRoute) *attachHandoff {
215+ target.SamePeer = false
216+ return &attachHandoff{target: target, source: source}
217+}
218+
206219 type Runner struct {
207220 dialer ports.ClientDialer
208221 term ports.Terminal
@@ -595,6 +608,14 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
595608 if !result.transportClosed {
596609 _ = connection.Close()
597610 }
611+ if result.target != nil {
612+ result.handoff = bindAttachHandoff(*result.target, attachRoute{
613+ dialer: homeRoute.dialer,
614+ request: cloneAttachRequest(homeRoute.request),
615+ resumeToken: homeRoute.resumeToken,
616+ })
617+ result.target = nil
618+ }
598619 return result
599620 }
600621
@@ -789,8 +810,14 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
789810 return errors.New("vev: unsupported navigation action")
790811 }
791812 }
792- if result.target != nil {
793- if result.target.RemoteTarget != nil || result.target.Endpoint != "" {
813+ if result.target != nil || result.handoff != nil {
814+ target := result.target
815+ source := attachRoute{dialer: dialer, request: attemptRequest, resumeToken: resumeToken}
816+ if result.handoff != nil {
817+ target = &result.handoff.target
818+ source = result.handoff.source
819+ }
820+ if target.RemoteTarget != nil || target.Endpoint != "" {
794821 if homeRoute == nil && attemptRequest.RemoteTarget == nil {
795822 routeRequest := attemptRequest
796823 if result.sessionName != "" {
@@ -807,31 +834,23 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
807834 nextRequest AttachRequest
808835 handoffErr error
809836 )
810- if result.target.Endpoint == "" {
837+ if target.Endpoint == "" {
811838 if r.ledger == nil {
812839 return errors.New("vev: route ledger unavailable")
813840 }
814- baseRequest := attemptRequest
815- nextDialer = dialer
816- if result.homePickerTarget {
817- if homeRoute == nil {
818- return errors.New("vev: home route unavailable for picker target")
819- }
820- nextDialer = homeRoute.dialer
821- baseRequest = homeRoute.request
822- }
823- nextRequest = r.ledger.samePeerHandoff(baseRequest, *result.target)
841+ nextDialer = source.dialer
842+ nextRequest = r.ledger.samePeerHandoff(source.request, *target)
824843 } else {
825844 if r.attachHandoff == nil {
826- return &AttachTargetError{Target: *result.target}
845+ return &AttachTargetError{Target: *target}
827846 }
828- nextDialer, nextRequest, handoffErr = r.attachHandoff(*result.target)
847+ nextDialer, nextRequest, handoffErr = r.attachHandoff(*target)
829848 if handoffErr != nil {
830849 return handoffErr
831850 }
832851 }
833- if result.target.ExactTarget != nil {
834- nextRequest.ExactTarget = result.target.ExactTarget
852+ if target.ExactTarget != nil {
853+ nextRequest.ExactTarget = target.ExactTarget
835854 }
836855 if homeRoute != nil && (nextRequest.RemoteTarget != nil || nextRequest.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned) {
837856 nextRequest.NavigationCapabilities |= protocol.NavigationCapabilityHomePicker
@@ -1091,8 +1110,8 @@ type attachResult struct {
10911110 welcomed bool
10921111 transportClosed bool
10931112 target *protocol.AttachTarget
1113+ handoff *attachHandoff
10941114 action protocol.NavigationAction
1095- homePickerTarget bool
10961115 routeAction *protocol.RouteNavigationAction
10971116 err error
10981117 }
@@ -1472,11 +1491,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
14721491 var samePeerSwitch *samePeerSwitchPending
14731492 var nextSamePeerRequestID uint64
14741493 type parkedRoutePending struct {
1475- requestID uint64
1476- action protocol.ParkedRouteAction
1477- leaseID protocol.ParkedRouteLeaseID
1478- fallbackTarget *protocol.AttachTarget
1479- timer ports.Timer
1494+ requestID uint64
1495+ action protocol.ParkedRouteAction
1496+ leaseID protocol.ParkedRouteLeaseID
1497+ fallback *attachHandoff
1498+ timer ports.Timer
14801499 }
14811500 var parkedPending *parkedRoutePending
14821501 var nextParkedRequestID uint64
@@ -1663,24 +1682,23 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
16631682 return welcomedResult(errLinkOffline), true
16641683 default:
16651684 }
1666- if selection.target != nil && transition != nil {
1685+ if selection.handoff != nil && transition != nil {
16671686 transitionWaitingFull = false
1668- transition.start(*selection.target)
1687+ transition.start(selection.handoff.target)
16691688 }
1670- if selection.target != nil && request.Remote && request.OriginKey != "" && selection.target.Endpoint == request.OriginKey && selection.target.RemoteTarget != nil {
1671- target := *selection.target.RemoteTarget
1689+ if selection.handoff != nil && request.Remote && request.OriginKey != "" && selection.handoff.target.Endpoint == request.OriginKey && selection.handoff.target.RemoteTarget != nil {
1690+ target := *selection.handoff.target.RemoteTarget
16721691 if err := sendParkedRouteSize(); err != nil {
16731692 return welcomedResult(err), true
16741693 }
16751694 if err := sendParkedRouteRequest(protocol.ParkedRouteSwitch, leaseID, &target); err != nil {
16761695 return welcomedResult(fmt.Errorf("vev: switching parked route: %w", err)), true
16771696 }
1678- fallback := *selection.target
1679- parkedPending.fallbackTarget = &fallback
1697+ fallback := *selection.handoff
1698+ parkedPending.fallback = &fallback
16801699 return attachResult{}, false
16811700 }
1682- if selection.target != nil {
1683- selection.homePickerTarget = true
1701+ if selection.handoff != nil {
16841702 return selection, true
16851703 }
16861704 if selection.err != nil {
@@ -1988,12 +2006,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
19882006 return selection
19892007 }
19902008 default:
1991- if pending.fallbackTarget == nil {
2009+ if pending.fallback == nil {
19922010 return welcomedResult(errors.New("vev: parked route switch failed"))
19932011 }
19942012 fallback := welcomedResult(nil)
1995- fallback.target = pending.fallbackTarget
1996- fallback.homePickerTarget = true
2013+ fallback.handoff = pending.fallback
19972014 return fallback
19982015 }
19992016 }
@@ -203,6 +203,19 @@ type attachRoute struct {
203 resumeToken uint64203 resumeToken uint64
204 }204 }
205 205
206+// attachHandoff binds a picker selection to the route that produced it. The
207+// connection-relative SamePeer hint is consumed before a target crosses this
208+// boundary; the bound source route is the sole transport authority afterward.
209+type attachHandoff struct {
210+ target protocol.AttachTarget
211+ source attachRoute
212+}
213+
214+func bindAttachHandoff(target protocol.AttachTarget, source attachRoute) *attachHandoff {
215+ target.SamePeer = false
216+ return &attachHandoff{target: target, source: source}
217+}
218+
206 type Runner struct {219 type Runner struct {
207 dialer ports.ClientDialer220 dialer ports.ClientDialer
208 term ports.Terminal221 term ports.Terminal
@@ -595,6 +608,14 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
595 if !result.transportClosed {608 if !result.transportClosed {
596 _ = connection.Close()609 _ = connection.Close()
597 }610 }
611+ if result.target != nil {
612+ result.handoff = bindAttachHandoff(*result.target, attachRoute{
613+ dialer: homeRoute.dialer,
614+ request: cloneAttachRequest(homeRoute.request),
615+ resumeToken: homeRoute.resumeToken,
616+ })
617+ result.target = nil
618+ }
598 return result619 return result
599 }620 }
600 621
@@ -789,8 +810,14 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
789 return errors.New("vev: unsupported navigation action")810 return errors.New("vev: unsupported navigation action")
790 }811 }
791 }812 }
792- if result.target != nil {813+ if result.target != nil || result.handoff != nil {
793- if result.target.RemoteTarget != nil || result.target.Endpoint != "" {814+ target := result.target
815+ source := attachRoute{dialer: dialer, request: attemptRequest, resumeToken: resumeToken}
816+ if result.handoff != nil {
817+ target = &result.handoff.target
818+ source = result.handoff.source
819+ }
820+ if target.RemoteTarget != nil || target.Endpoint != "" {
794 if homeRoute == nil && attemptRequest.RemoteTarget == nil {821 if homeRoute == nil && attemptRequest.RemoteTarget == nil {
795 routeRequest := attemptRequest822 routeRequest := attemptRequest
796 if result.sessionName != "" {823 if result.sessionName != "" {
@@ -807,31 +834,23 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
807 nextRequest AttachRequest834 nextRequest AttachRequest
808 handoffErr error835 handoffErr error
809 )836 )
810- if result.target.Endpoint == "" {837+ if target.Endpoint == "" {
811 if r.ledger == nil {838 if r.ledger == nil {
812 return errors.New("vev: route ledger unavailable")839 return errors.New("vev: route ledger unavailable")
813 }840 }
814- baseRequest := attemptRequest841+ nextDialer = source.dialer
815- nextDialer = dialer842+ nextRequest = r.ledger.samePeerHandoff(source.request, *target)
816- if result.homePickerTarget {
817- if homeRoute == nil {
818- return errors.New("vev: home route unavailable for picker target")
819- }
820- nextDialer = homeRoute.dialer
821- baseRequest = homeRoute.request
822- }
823- nextRequest = r.ledger.samePeerHandoff(baseRequest, *result.target)
824 } else {843 } else {
825 if r.attachHandoff == nil {844 if r.attachHandoff == nil {
826- return &AttachTargetError{Target: *result.target}845+ return &AttachTargetError{Target: *target}
827 }846 }
828- nextDialer, nextRequest, handoffErr = r.attachHandoff(*result.target)847+ nextDialer, nextRequest, handoffErr = r.attachHandoff(*target)
829 if handoffErr != nil {848 if handoffErr != nil {
830 return handoffErr849 return handoffErr
831 }850 }
832 }851 }
833- if result.target.ExactTarget != nil {852+ if target.ExactTarget != nil {
834- nextRequest.ExactTarget = result.target.ExactTarget853+ nextRequest.ExactTarget = target.ExactTarget
835 }854 }
836 if homeRoute != nil && (nextRequest.RemoteTarget != nil || nextRequest.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned) {855 if homeRoute != nil && (nextRequest.RemoteTarget != nil || nextRequest.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned) {
837 nextRequest.NavigationCapabilities |= protocol.NavigationCapabilityHomePicker856 nextRequest.NavigationCapabilities |= protocol.NavigationCapabilityHomePicker
@@ -1091,8 +1110,8 @@ type attachResult struct {
1091 welcomed bool1110 welcomed bool
1092 transportClosed bool1111 transportClosed bool
1093 target *protocol.AttachTarget1112 target *protocol.AttachTarget
1113+ handoff *attachHandoff
1094 action protocol.NavigationAction1114 action protocol.NavigationAction
1095- homePickerTarget bool
1096 routeAction *protocol.RouteNavigationAction1115 routeAction *protocol.RouteNavigationAction
1097 err error1116 err error
1098 }1117 }
@@ -1472,11 +1491,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1472 var samePeerSwitch *samePeerSwitchPending1491 var samePeerSwitch *samePeerSwitchPending
1473 var nextSamePeerRequestID uint641492 var nextSamePeerRequestID uint64
1474 type parkedRoutePending struct {1493 type parkedRoutePending struct {
1475- requestID uint641494+ requestID uint64
1476- action protocol.ParkedRouteAction1495+ action protocol.ParkedRouteAction
1477- leaseID protocol.ParkedRouteLeaseID1496+ leaseID protocol.ParkedRouteLeaseID
1478- fallbackTarget *protocol.AttachTarget1497+ fallback *attachHandoff
1479- timer ports.Timer1498+ timer ports.Timer
1480 }1499 }
1481 var parkedPending *parkedRoutePending1500 var parkedPending *parkedRoutePending
1482 var nextParkedRequestID uint641501 var nextParkedRequestID uint64
@@ -1663,24 +1682,23 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1663 return welcomedResult(errLinkOffline), true1682 return welcomedResult(errLinkOffline), true
1664 default:1683 default:
1665 }1684 }
1666- if selection.target != nil && transition != nil {1685+ if selection.handoff != nil && transition != nil {
1667 transitionWaitingFull = false1686 transitionWaitingFull = false
1668- transition.start(*selection.target)1687+ transition.start(selection.handoff.target)
1669 }1688 }
1670- if selection.target != nil && request.Remote && request.OriginKey != "" && selection.target.Endpoint == request.OriginKey && selection.target.RemoteTarget != nil {1689+ if selection.handoff != nil && request.Remote && request.OriginKey != "" && selection.handoff.target.Endpoint == request.OriginKey && selection.handoff.target.RemoteTarget != nil {
1671- target := *selection.target.RemoteTarget1690+ target := *selection.handoff.target.RemoteTarget
1672 if err := sendParkedRouteSize(); err != nil {1691 if err := sendParkedRouteSize(); err != nil {
1673 return welcomedResult(err), true1692 return welcomedResult(err), true
1674 }1693 }
1675 if err := sendParkedRouteRequest(protocol.ParkedRouteSwitch, leaseID, &target); err != nil {1694 if err := sendParkedRouteRequest(protocol.ParkedRouteSwitch, leaseID, &target); err != nil {
1676 return welcomedResult(fmt.Errorf("vev: switching parked route: %w", err)), true1695 return welcomedResult(fmt.Errorf("vev: switching parked route: %w", err)), true
1677 }1696 }
1678- fallback := *selection.target1697+ fallback := *selection.handoff
1679- parkedPending.fallbackTarget = &fallback1698+ parkedPending.fallback = &fallback
1680 return attachResult{}, false1699 return attachResult{}, false
1681 }1700 }
1682- if selection.target != nil {1701+ if selection.handoff != nil {
1683- selection.homePickerTarget = true
1684 return selection, true1702 return selection, true
1685 }1703 }
1686 if selection.err != nil {1704 if selection.err != nil {
@@ -1988,12 +2006,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1988 return selection2006 return selection
1989 }2007 }
1990 default:2008 default:
1991- if pending.fallbackTarget == nil {2009+ if pending.fallback == nil {
1992 return welcomedResult(errors.New("vev: parked route switch failed"))2010 return welcomedResult(errors.New("vev: parked route switch failed"))
1993 }2011 }
1994 fallback := welcomedResult(nil)2012 fallback := welcomedResult(nil)
1995- fallback.target = pending.fallbackTarget2013+ fallback.handoff = pending.fallback
1996- fallback.homePickerTarget = true
1997 return fallback2014 return fallback
1998 }2015 }
1999 }2016 }
M internal/usecase/client/client_test.go +117 -0
@@ -782,6 +782,123 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
782782 require.Equal(t, int32(1), remoteDialer.calls.Load(), "same-host picker switch must retain the authenticated remote transport")
783783 }
784784
785+func TestHybridPickerLocalSelectionCommitsLocalRoute(t *testing.T) {
786+ term := newRunTerminal()
787+ defer term.in.unblock()
788+
789+ localHomeLifecycle := domain.SessionLifecycleID{1}
790+ localMiscLifecycle := domain.SessionLifecycleID{2}
791+ remoteSourceLifecycle := domain.SessionLifecycleID{3}
792+ remoteMiscLifecycle := domain.SessionLifecycleID{4}
793+ remoteVev := protocol.ExactSessionTarget{LifecycleID: remoteSourceLifecycle, SessionName: "vev"}
794+ remoteMisc := domain.RemoteSessionTarget{
795+ Endpoint: "remote", DisplayOrigin: "arch", LifecycleID: remoteMiscLifecycle,
796+ SessionName: "misc", LiveTabID: "misc-tab",
797+ }
798+ localInitial := hybridLocalBootstrap(localHomeLifecycle, remoteMisc)
799+ switchSent := make(chan struct{})
800+ prepareSent := make(chan struct{})
801+ resumeOrClosed := make(chan struct{})
802+ remoteClosed := make(chan struct{})
803+ var remoteCloseOnce sync.Once
804+ var resumeOrCloseOnce sync.Once
805+ remote := &recordingTransport{recvs: []recvItem{
806+ {f: hybridWelcomeFrame("misc", remoteMiscLifecycle)},
807+ {f: frameOf(wire.MsgAttachTarget, wire.MarshalAttachTarget(protocol.AttachTarget{
808+ Session: "vev", Intent: protocol.IntentAttach,
809+ ExactTarget: &remoteVev,
810+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, SamePeer: true,
811+ }))},
812+ {f: frameOf(wire.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{
813+ Target: remoteVev,
814+ })), wait: switchSent},
815+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
816+ {f: frameOf(wire.MsgParkedRouteResponse, wire.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})), wait: prepareSent},
817+ {f: frameOf(wire.MsgParkedRouteResponse, wire.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteResumed})), wait: resumeOrClosed},
818+ {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
819+ }, stall: remoteClosed}
820+ remote.onClose = func() {
821+ remoteCloseOnce.Do(func() { close(remoteClosed) })
822+ resumeOrCloseOnce.Do(func() { close(resumeOrClosed) })
823+ }
824+ parkedHandler := hybridParkedRequestHandler(nil, nil, map[protocol.ParkedRouteAction]chan struct{}{
825+ protocol.ParkedRoutePrepare: prepareSent,
826+ })
827+ var switchOnce sync.Once
828+ remote.onSend = func(frame wire.Frame) {
829+ parkedHandler(frame)
830+ if frame.Type == wire.MsgSamePeerSwitchRequest {
831+ switchOnce.Do(func() { close(switchSent) })
832+ }
833+ if frame.Type != wire.MsgParkedRouteRequest {
834+ return
835+ }
836+ request, err := wire.UnmarshalParkedRouteRequest(frame.Payload)
837+ if err == nil && request.Action == protocol.ParkedRouteResume {
838+ resumeOrCloseOnce.Do(func() { close(resumeOrClosed) })
839+ }
840+ }
841+ localTarget := protocol.ExactSessionTarget{LifecycleID: localMiscLifecycle, SessionName: "misc"}
842+ localPickerClosed := make(chan struct{})
843+ var localPickerCloseOnce sync.Once
844+ localPicker := &recordingTransport{recvs: []recvItem{
845+ {f: hybridWelcomeFrame("local", localHomeLifecycle)},
846+ {f: frameOf(wire.MsgAttachTarget, wire.MarshalAttachTarget(protocol.AttachTarget{
847+ Session: "misc", Intent: protocol.IntentAttach, ExactTarget: &localTarget,
848+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, SamePeer: true,
849+ }))},
850+ }, stall: localPickerClosed}
851+ localPicker.onClose = func() { localPickerCloseOnce.Do(func() { close(localPickerClosed) }) }
852+ localMisc := &recordingTransport{recvs: []recvItem{
853+ {f: hybridWelcomeFrame("misc", localMiscLifecycle)},
854+ {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
855+ }}
856+ localDialer := &sequenceDialer{trs: []wire.Transport{localInitial, localPicker, localMisc}}
857+ remoteDialer := &sequenceDialer{trs: []wire.Transport{markedDatagramTransport{Transport: remote}}}
858+ deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{"remote": remoteDialer})
859+
860+ err := runTestClient(t.Context(), deps, client.AttachRequest{
861+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
862+ })
863+ require.NoError(t, err)
864+ require.Equal(t, int32(3), localDialer.calls.Load())
865+ require.Equal(t, int32(1), remoteDialer.calls.Load())
866+ var remoteSwitches []protocol.SamePeerSwitchRequest
867+ for _, sent := range remote.Sends() {
868+ if sent.Type != wire.MsgSamePeerSwitchRequest {
869+ continue
870+ }
871+ switchRequest, err := wire.UnmarshalSamePeerSwitchRequest(sent.Payload)
872+ require.NoError(t, err)
873+ remoteSwitches = append(remoteSwitches, switchRequest)
874+ }
875+ require.Len(t, remoteSwitches, 1)
876+ require.Equal(t, remoteVev, remoteSwitches[0].Target, "the local misc target must never be switched on the remote peer")
877+
878+ var snapshot protocol.RecentRouteSnapshot
879+ for _, sent := range localMisc.Sends() {
880+ if sent.Type != wire.MsgRecentRouteSnapshot {
881+ continue
882+ }
883+ var err error
884+ snapshot, err = wire.UnmarshalRecentRouteSnapshot(sent.Payload)
885+ require.NoError(t, err)
886+ }
887+ require.Equal(t, localTarget, snapshot.ActiveEntry.Target)
888+ require.Equal(t, protocol.RouteKindLocal, snapshot.ActiveEntry.Kind)
889+ require.Empty(t, snapshot.ActiveEntry.HostLabel)
890+ remoteMiscFound := false
891+ for _, entry := range snapshot.Entries {
892+ if entry.Target != (protocol.ExactSessionTarget{LifecycleID: remoteMiscLifecycle, SessionName: "misc"}) {
893+ continue
894+ }
895+ remoteMiscFound = true
896+ require.Equal(t, "arch", entry.HostLabel)
897+ require.Equal(t, protocol.RouteKindRemote, entry.Kind)
898+ }
899+ require.True(t, remoteMiscFound, "the distinct remote misc route must remain qualified in history")
900+}
901+
785902 func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) {
786903 term := newRunTerminal()
787904 defer term.in.unblock()
@@ -782,6 +782,123 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
782 require.Equal(t, int32(1), remoteDialer.calls.Load(), "same-host picker switch must retain the authenticated remote transport")782 require.Equal(t, int32(1), remoteDialer.calls.Load(), "same-host picker switch must retain the authenticated remote transport")
783 }783 }
784 784
785+func TestHybridPickerLocalSelectionCommitsLocalRoute(t *testing.T) {
786+ term := newRunTerminal()
787+ defer term.in.unblock()
788+
789+ localHomeLifecycle := domain.SessionLifecycleID{1}
790+ localMiscLifecycle := domain.SessionLifecycleID{2}
791+ remoteSourceLifecycle := domain.SessionLifecycleID{3}
792+ remoteMiscLifecycle := domain.SessionLifecycleID{4}
793+ remoteVev := protocol.ExactSessionTarget{LifecycleID: remoteSourceLifecycle, SessionName: "vev"}
794+ remoteMisc := domain.RemoteSessionTarget{
795+ Endpoint: "remote", DisplayOrigin: "arch", LifecycleID: remoteMiscLifecycle,
796+ SessionName: "misc", LiveTabID: "misc-tab",
797+ }
798+ localInitial := hybridLocalBootstrap(localHomeLifecycle, remoteMisc)
799+ switchSent := make(chan struct{})
800+ prepareSent := make(chan struct{})
801+ resumeOrClosed := make(chan struct{})
802+ remoteClosed := make(chan struct{})
803+ var remoteCloseOnce sync.Once
804+ var resumeOrCloseOnce sync.Once
805+ remote := &recordingTransport{recvs: []recvItem{
806+ {f: hybridWelcomeFrame("misc", remoteMiscLifecycle)},
807+ {f: frameOf(wire.MsgAttachTarget, wire.MarshalAttachTarget(protocol.AttachTarget{
808+ Session: "vev", Intent: protocol.IntentAttach,
809+ ExactTarget: &remoteVev,
810+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, SamePeer: true,
811+ }))},
812+ {f: frameOf(wire.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{
813+ Target: remoteVev,
814+ })), wait: switchSent},
815+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
816+ {f: frameOf(wire.MsgParkedRouteResponse, wire.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})), wait: prepareSent},
817+ {f: frameOf(wire.MsgParkedRouteResponse, wire.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteResumed})), wait: resumeOrClosed},
818+ {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
819+ }, stall: remoteClosed}
820+ remote.onClose = func() {
821+ remoteCloseOnce.Do(func() { close(remoteClosed) })
822+ resumeOrCloseOnce.Do(func() { close(resumeOrClosed) })
823+ }
824+ parkedHandler := hybridParkedRequestHandler(nil, nil, map[protocol.ParkedRouteAction]chan struct{}{
825+ protocol.ParkedRoutePrepare: prepareSent,
826+ })
827+ var switchOnce sync.Once
828+ remote.onSend = func(frame wire.Frame) {
829+ parkedHandler(frame)
830+ if frame.Type == wire.MsgSamePeerSwitchRequest {
831+ switchOnce.Do(func() { close(switchSent) })
832+ }
833+ if frame.Type != wire.MsgParkedRouteRequest {
834+ return
835+ }
836+ request, err := wire.UnmarshalParkedRouteRequest(frame.Payload)
837+ if err == nil && request.Action == protocol.ParkedRouteResume {
838+ resumeOrCloseOnce.Do(func() { close(resumeOrClosed) })
839+ }
840+ }
841+ localTarget := protocol.ExactSessionTarget{LifecycleID: localMiscLifecycle, SessionName: "misc"}
842+ localPickerClosed := make(chan struct{})
843+ var localPickerCloseOnce sync.Once
844+ localPicker := &recordingTransport{recvs: []recvItem{
845+ {f: hybridWelcomeFrame("local", localHomeLifecycle)},
846+ {f: frameOf(wire.MsgAttachTarget, wire.MarshalAttachTarget(protocol.AttachTarget{
847+ Session: "misc", Intent: protocol.IntentAttach, ExactTarget: &localTarget,
848+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, SamePeer: true,
849+ }))},
850+ }, stall: localPickerClosed}
851+ localPicker.onClose = func() { localPickerCloseOnce.Do(func() { close(localPickerClosed) }) }
852+ localMisc := &recordingTransport{recvs: []recvItem{
853+ {f: hybridWelcomeFrame("misc", localMiscLifecycle)},
854+ {f: frameOf(wire.MsgDetached, wire.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
855+ }}
856+ localDialer := &sequenceDialer{trs: []wire.Transport{localInitial, localPicker, localMisc}}
857+ remoteDialer := &sequenceDialer{trs: []wire.Transport{markedDatagramTransport{Transport: remote}}}
858+ deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{"remote": remoteDialer})
859+
860+ err := runTestClient(t.Context(), deps, client.AttachRequest{
861+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
862+ })
863+ require.NoError(t, err)
864+ require.Equal(t, int32(3), localDialer.calls.Load())
865+ require.Equal(t, int32(1), remoteDialer.calls.Load())
866+ var remoteSwitches []protocol.SamePeerSwitchRequest
867+ for _, sent := range remote.Sends() {
868+ if sent.Type != wire.MsgSamePeerSwitchRequest {
869+ continue
870+ }
871+ switchRequest, err := wire.UnmarshalSamePeerSwitchRequest(sent.Payload)
872+ require.NoError(t, err)
873+ remoteSwitches = append(remoteSwitches, switchRequest)
874+ }
875+ require.Len(t, remoteSwitches, 1)
876+ require.Equal(t, remoteVev, remoteSwitches[0].Target, "the local misc target must never be switched on the remote peer")
877+
878+ var snapshot protocol.RecentRouteSnapshot
879+ for _, sent := range localMisc.Sends() {
880+ if sent.Type != wire.MsgRecentRouteSnapshot {
881+ continue
882+ }
883+ var err error
884+ snapshot, err = wire.UnmarshalRecentRouteSnapshot(sent.Payload)
885+ require.NoError(t, err)
886+ }
887+ require.Equal(t, localTarget, snapshot.ActiveEntry.Target)
888+ require.Equal(t, protocol.RouteKindLocal, snapshot.ActiveEntry.Kind)
889+ require.Empty(t, snapshot.ActiveEntry.HostLabel)
890+ remoteMiscFound := false
891+ for _, entry := range snapshot.Entries {
892+ if entry.Target != (protocol.ExactSessionTarget{LifecycleID: remoteMiscLifecycle, SessionName: "misc"}) {
893+ continue
894+ }
895+ remoteMiscFound = true
896+ require.Equal(t, "arch", entry.HostLabel)
897+ require.Equal(t, protocol.RouteKindRemote, entry.Kind)
898+ }
899+ require.True(t, remoteMiscFound, "the distinct remote misc route must remain qualified in history")
900+}
901+
785 func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) {902 func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) {
786 term := newRunTerminal()903 term := newRunTerminal()
787 defer term.in.unblock()904 defer term.in.unblock()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close