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 Unverified
B5690EEEBB952194The key is unknown
2 files · +168 -34
M internal/usecase/client/client.go +51 -34
| @@ -203,6 +203,19 @@ type attachRoute struct { | |||
| 203 | 203 | 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 | 219 | type Runner struct { | |
| 207 | 220 | dialer ports.ClientDialer | |
| 208 | 221 | term ports.Terminal | |
| @@ -595,6 +608,14 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||
| 595 | 608 | if !result.transportClosed { | |
| 596 | 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 | 619 | return result | |
| 599 | 620 | } | |
| 600 | 621 | ||
| @@ -789,8 +810,14 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||
| 789 | 810 | return errors.New("vev: unsupported navigation action") | |
| 790 | 811 | } | |
| 791 | 812 | } | |
| 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 != "" { | |
| 794 | 821 | if homeRoute == nil && attemptRequest.RemoteTarget == nil { | |
| 795 | 822 | routeRequest := attemptRequest | |
| 796 | 823 | if result.sessionName != "" { | |
| @@ -807,31 +834,23 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||
| 807 | 834 | nextRequest AttachRequest | |
| 808 | 835 | handoffErr error | |
| 809 | 836 | ) | |
| 810 | - | if result.target.Endpoint == "" { | |
| 837 | + | if target.Endpoint == "" { | |
| 811 | 838 | if r.ledger == nil { | |
| 812 | 839 | return errors.New("vev: route ledger unavailable") | |
| 813 | 840 | } | |
| 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) | |
| 824 | 843 | } else { | |
| 825 | 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 | 848 | if handoffErr != nil { | |
| 830 | 849 | return handoffErr | |
| 831 | 850 | } | |
| 832 | 851 | } | |
| 833 | - | if result.target.ExactTarget != nil { | |
| 834 | - | nextRequest.ExactTarget = result.target.ExactTarget | |
| 852 | + | if target.ExactTarget != nil { | |
| 853 | + | nextRequest.ExactTarget = target.ExactTarget | |
| 835 | 854 | } | |
| 836 | 855 | if homeRoute != nil && (nextRequest.RemoteTarget != nil || nextRequest.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned) { | |
| 837 | 856 | nextRequest.NavigationCapabilities |= protocol.NavigationCapabilityHomePicker | |
| @@ -1091,8 +1110,8 @@ type attachResult struct { | |||
| 1091 | 1110 | welcomed bool | |
| 1092 | 1111 | transportClosed bool | |
| 1093 | 1112 | target *protocol.AttachTarget | |
| 1113 | + | handoff *attachHandoff | |
| 1094 | 1114 | action protocol.NavigationAction | |
| 1095 | - | homePickerTarget bool | |
| 1096 | 1115 | routeAction *protocol.RouteNavigationAction | |
| 1097 | 1116 | err error | |
| 1098 | 1117 | } | |
| @@ -1472,11 +1491,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||
| 1472 | 1491 | var samePeerSwitch *samePeerSwitchPending | |
| 1473 | 1492 | var nextSamePeerRequestID uint64 | |
| 1474 | 1493 | 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 | |
| 1480 | 1499 | } | |
| 1481 | 1500 | var parkedPending *parkedRoutePending | |
| 1482 | 1501 | var nextParkedRequestID uint64 | |
| @@ -1663,24 +1682,23 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||
| 1663 | 1682 | return welcomedResult(errLinkOffline), true | |
| 1664 | 1683 | default: | |
| 1665 | 1684 | } | |
| 1666 | - | if selection.target != nil && transition != nil { | |
| 1685 | + | if selection.handoff != nil && transition != nil { | |
| 1667 | 1686 | 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 { | |
| 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 | |
| 1672 | 1691 | if err := sendParkedRouteSize(); err != nil { | |
| 1673 | 1692 | return welcomedResult(err), true | |
| 1674 | 1693 | } | |
| 1675 | 1694 | if err := sendParkedRouteRequest(protocol.ParkedRouteSwitch, leaseID, &target); err != nil { | |
| 1676 | 1695 | return welcomedResult(fmt.Errorf("vev: switching parked route: %w", err)), true | |
| 1677 | 1696 | } | |
| 1678 | - | fallback := *selection.target | |
| 1679 | - | parkedPending.fallbackTarget = &fallback | |
| 1697 | + | fallback := *selection.handoff | |
| 1698 | + | parkedPending.fallback = &fallback | |
| 1680 | 1699 | return attachResult{}, false | |
| 1681 | 1700 | } | |
| 1682 | - | if selection.target != nil { | |
| 1683 | - | selection.homePickerTarget = true | |
| 1701 | + | if selection.handoff != nil { | |
| 1684 | 1702 | return selection, true | |
| 1685 | 1703 | } | |
| 1686 | 1704 | if selection.err != nil { | |
| @@ -1988,12 +2006,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||
| 1988 | 2006 | return selection | |
| 1989 | 2007 | } | |
| 1990 | 2008 | default: | |
| 1991 | - | if pending.fallbackTarget == nil { | |
| 2009 | + | if pending.fallback == nil { | |
| 1992 | 2010 | return welcomedResult(errors.New("vev: parked route switch failed")) | |
| 1993 | 2011 | } | |
| 1994 | 2012 | fallback := welcomedResult(nil) | |
| 1995 | - | fallback.target = pending.fallbackTarget | |
| 1996 | - | fallback.homePickerTarget = true | |
| 2013 | + | fallback.handoff = pending.fallback | |
| 1997 | 2014 | return fallback | |
| 1998 | 2015 | } | |
| 1999 | 2016 | } | |
| @@ -203,6 +203,19 @@ type attachRoute struct { | |||||
| 203 | resumeToken uint64 | 203 | 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.ClientDialer | 220 | dialer ports.ClientDialer | ||
| 208 | term ports.Terminal | 221 | 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 result | 619 | 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 := attemptRequest | 822 | 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 AttachRequest | 834 | nextRequest AttachRequest | ||
| 808 | handoffErr error | 835 | 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 := attemptRequest | 841 | + | nextDialer = source.dialer |
| 815 | - | nextDialer = dialer | 842 | + | 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 handoffErr | 849 | return handoffErr | ||
| 831 | } | 850 | } | ||
| 832 | } | 851 | } | ||
| 833 | - | if result.target.ExactTarget != nil { | 852 | + | if target.ExactTarget != nil { |
| 834 | - | nextRequest.ExactTarget = result.target.ExactTarget | 853 | + | 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.NavigationCapabilityHomePicker | 856 | nextRequest.NavigationCapabilities |= protocol.NavigationCapabilityHomePicker | ||
| @@ -1091,8 +1110,8 @@ type attachResult struct { | |||||
| 1091 | welcomed bool | 1110 | welcomed bool | ||
| 1092 | transportClosed bool | 1111 | transportClosed bool | ||
| 1093 | target *protocol.AttachTarget | 1112 | target *protocol.AttachTarget | ||
| 1113 | + | handoff *attachHandoff | |||
| 1094 | action protocol.NavigationAction | 1114 | action protocol.NavigationAction | ||
| 1095 | - | homePickerTarget bool | |||
| 1096 | routeAction *protocol.RouteNavigationAction | 1115 | routeAction *protocol.RouteNavigationAction | ||
| 1097 | err error | 1116 | err error | ||
| 1098 | } | 1117 | } | ||
| @@ -1472,11 +1491,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||||
| 1472 | var samePeerSwitch *samePeerSwitchPending | 1491 | var samePeerSwitch *samePeerSwitchPending | ||
| 1473 | var nextSamePeerRequestID uint64 | 1492 | var nextSamePeerRequestID uint64 | ||
| 1474 | type parkedRoutePending struct { | 1493 | type parkedRoutePending struct { | ||
| 1475 | - | requestID uint64 | 1494 | + | requestID uint64 |
| 1476 | - | action protocol.ParkedRouteAction | 1495 | + | action protocol.ParkedRouteAction |
| 1477 | - | leaseID protocol.ParkedRouteLeaseID | 1496 | + | leaseID protocol.ParkedRouteLeaseID |
| 1478 | - | fallbackTarget *protocol.AttachTarget | 1497 | + | fallback *attachHandoff |
| 1479 | - | timer ports.Timer | 1498 | + | timer ports.Timer |
| 1480 | } | 1499 | } | ||
| 1481 | var parkedPending *parkedRoutePending | 1500 | var parkedPending *parkedRoutePending | ||
| 1482 | var nextParkedRequestID uint64 | 1501 | var nextParkedRequestID uint64 | ||
| @@ -1663,24 +1682,23 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||||
| 1663 | return welcomedResult(errLinkOffline), true | 1682 | 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 = false | 1686 | 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.RemoteTarget | 1690 | + | target := *selection.handoff.target.RemoteTarget |
| 1672 | if err := sendParkedRouteSize(); err != nil { | 1691 | if err := sendParkedRouteSize(); err != nil { | ||
| 1673 | return welcomedResult(err), true | 1692 | 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)), true | 1695 | return welcomedResult(fmt.Errorf("vev: switching parked route: %w", err)), true | ||
| 1677 | } | 1696 | } | ||
| 1678 | - | fallback := *selection.target | 1697 | + | fallback := *selection.handoff |
| 1679 | - | parkedPending.fallbackTarget = &fallback | 1698 | + | parkedPending.fallback = &fallback |
| 1680 | return attachResult{}, false | 1699 | return attachResult{}, false | ||
| 1681 | } | 1700 | } | ||
| 1682 | - | if selection.target != nil { | 1701 | + | if selection.handoff != nil { |
| 1683 | - | selection.homePickerTarget = true | |||
| 1684 | return selection, true | 1702 | 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 selection | 2006 | 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.fallbackTarget | 2013 | + | fallback.handoff = pending.fallback |
| 1996 | - | fallback.homePickerTarget = true | |||
| 1997 | return fallback | 2014 | return fallback | ||
| 1998 | } | 2015 | } | ||
| 1999 | } | 2016 | } | ||
M internal/usecase/client/client_test.go +117 -0
| @@ -782,6 +782,123 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) { | |||
| 782 | 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 | 902 | func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) { | |
| 786 | 903 | term := newRunTerminal() | |
| 787 | 904 | 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() | ||