fix(remote): unify target validation errors
- commit
- 610e63d1fe5f0dbb25e24120a037d661c1470d90
- author
- brice · · 2026-08-08 18:10 UTC
- parent
- 417c304
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +14 -20
M internal/ports/wire.go +1 -1
| @@ -91,7 +91,7 @@ func ValidateNavigation(capabilities NavigationCapabilities, overlay StartupOver | |||
| 91 | 91 | } | |
| 92 | 92 | home := capabilities&NavigationCapabilityHomePicker != 0 | |
| 93 | 93 | back := capabilities&NavigationCapabilityBack != 0 | |
| 94 | - | if home && !remoteTarget || back != (overlay == StartupOverlaySessionPicker) { | |
| 94 | + | if (home && !remoteTarget) || back != (overlay == StartupOverlaySessionPicker) { | |
| 95 | 95 | return ErrInvalidNavigation | |
| 96 | 96 | } | |
| 97 | 97 | if remoteTarget && back { | |
| @@ -91,7 +91,7 @@ func ValidateNavigation(capabilities NavigationCapabilities, overlay StartupOver | |||||
| 91 | } | 91 | } | ||
| 92 | home := capabilities&NavigationCapabilityHomePicker != 0 | 92 | home := capabilities&NavigationCapabilityHomePicker != 0 | ||
| 93 | back := capabilities&NavigationCapabilityBack != 0 | 93 | back := capabilities&NavigationCapabilityBack != 0 | ||
| 94 | - | if home && !remoteTarget || back != (overlay == StartupOverlaySessionPicker) { | 94 | + | if (home && !remoteTarget) || back != (overlay == StartupOverlaySessionPicker) { |
| 95 | return ErrInvalidNavigation | 95 | return ErrInvalidNavigation | ||
| 96 | } | 96 | } | ||
| 97 | if remoteTarget && back { | 97 | if remoteTarget && back { | ||
M internal/usecase/client/client.go +2 -16
| @@ -236,22 +236,8 @@ func (r *Runner) terminalInput() *terminalInputPump { | |||
| 236 | 236 | } | |
| 237 | 237 | ||
| 238 | 238 | func validateAttachRequest(request AttachRequest) error { | |
| 239 | - | switch request.Intent { | |
| 240 | - | case ports.IntentEphemeral: | |
| 241 | - | if request.SessionName != "" { | |
| 242 | - | return errors.New("vev: ephemeral attach cannot name a session") | |
| 243 | - | } | |
| 244 | - | case ports.IntentNew, ports.IntentAttach, ports.IntentResume: | |
| 245 | - | if request.SessionName == "" { | |
| 246 | - | return errors.New("vev: session name is required") | |
| 247 | - | } | |
| 248 | - | default: | |
| 249 | - | return fmt.Errorf("vev: invalid session intent %d", request.Intent) | |
| 250 | - | } | |
| 251 | - | if request.SessionName != "" { | |
| 252 | - | if err := domain.ValidateSessionName(request.SessionName); err != nil { | |
| 253 | - | return fmt.Errorf("vev: invalid session name: %w", err) | |
| 254 | - | } | |
| 239 | + | if err := (SessionTarget{Intent: request.Intent, SessionName: request.SessionName}).validate(); err != nil { | |
| 240 | + | return fmt.Errorf("vev: invalid session target: %w", err) | |
| 255 | 241 | } | |
| 256 | 242 | if err := ports.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.RemoteTarget != nil || request.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned); err != nil { | |
| 257 | 243 | return fmt.Errorf("vev: invalid navigation route: %w", err) | |
| @@ -236,22 +236,8 @@ func (r *Runner) terminalInput() *terminalInputPump { | |||||
| 236 | } | 236 | } | ||
| 237 | 237 | ||||
| 238 | func validateAttachRequest(request AttachRequest) error { | 238 | func validateAttachRequest(request AttachRequest) error { | ||
| 239 | - | switch request.Intent { | 239 | + | if err := (SessionTarget{Intent: request.Intent, SessionName: request.SessionName}).validate(); err != nil { |
| 240 | - | case ports.IntentEphemeral: | 240 | + | return fmt.Errorf("vev: invalid session target: %w", err) |
| 241 | - | if request.SessionName != "" { | |||
| 242 | - | return errors.New("vev: ephemeral attach cannot name a session") | |||
| 243 | - | } | |||
| 244 | - | case ports.IntentNew, ports.IntentAttach, ports.IntentResume: | |||
| 245 | - | if request.SessionName == "" { | |||
| 246 | - | return errors.New("vev: session name is required") | |||
| 247 | - | } | |||
| 248 | - | default: | |||
| 249 | - | return fmt.Errorf("vev: invalid session intent %d", request.Intent) | |||
| 250 | - | } | |||
| 251 | - | if request.SessionName != "" { | |||
| 252 | - | if err := domain.ValidateSessionName(request.SessionName); err != nil { | |||
| 253 | - | return fmt.Errorf("vev: invalid session name: %w", err) | |||
| 254 | - | } | |||
| 255 | } | 241 | } | ||
| 256 | if err := ports.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.RemoteTarget != nil || request.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned); err != nil { | 242 | if err := ports.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.RemoteTarget != nil || request.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned); err != nil { | ||
| 257 | return fmt.Errorf("vev: invalid navigation route: %w", err) | 243 | return fmt.Errorf("vev: invalid navigation route: %w", err) | ||
M internal/usecase/client/session_connection.go +5 -2
| @@ -8,7 +8,10 @@ import ( | |||
| 8 | 8 | "github.com/bnema/vev/internal/ports" | |
| 9 | 9 | ) | |
| 10 | 10 | ||
| 11 | - | var errNilSessionTransport = errors.New("client: nil session transport") | |
| 11 | + | var ( | |
| 12 | + | errNilSessionTransport = errors.New("client: nil session transport") | |
| 13 | + | ErrEphemeralSessionName = errors.New("client: ephemeral sessions cannot have a name") | |
| 14 | + | ) | |
| 12 | 15 | ||
| 13 | 16 | // SessionTarget is the validated daemon-facing session selection. Endpoint | |
| 14 | 17 | // details are resolved before a transport is opened and never enter this type. | |
| @@ -21,7 +24,7 @@ func (t SessionTarget) validate() error { | |||
| 21 | 24 | switch t.Intent { | |
| 22 | 25 | case ports.IntentEphemeral: | |
| 23 | 26 | if t.SessionName != "" { | |
| 24 | - | return domain.ErrInvalidSessionName | |
| 27 | + | return ErrEphemeralSessionName | |
| 25 | 28 | } | |
| 26 | 29 | return nil | |
| 27 | 30 | case ports.IntentNew, ports.IntentAttach, ports.IntentResume: | |
| @@ -8,7 +8,10 @@ import ( | |||||
| 8 | "github.com/bnema/vev/internal/ports" | 8 | "github.com/bnema/vev/internal/ports" | ||
| 9 | ) | 9 | ) | ||
| 10 | 10 | ||||
| 11 | - | var errNilSessionTransport = errors.New("client: nil session transport") | 11 | + | var ( |
| 12 | + | errNilSessionTransport = errors.New("client: nil session transport") | |||
| 13 | + | ErrEphemeralSessionName = errors.New("client: ephemeral sessions cannot have a name") | |||
| 14 | + | ) | |||
| 12 | 15 | ||||
| 13 | // SessionTarget is the validated daemon-facing session selection. Endpoint | 16 | // SessionTarget is the validated daemon-facing session selection. Endpoint | ||
| 14 | // details are resolved before a transport is opened and never enter this type. | 17 | // details are resolved before a transport is opened and never enter this type. | ||
| @@ -21,7 +24,7 @@ func (t SessionTarget) validate() error { | |||||
| 21 | switch t.Intent { | 24 | switch t.Intent { | ||
| 22 | case ports.IntentEphemeral: | 25 | case ports.IntentEphemeral: | ||
| 23 | if t.SessionName != "" { | 26 | if t.SessionName != "" { | ||
| 24 | - | return domain.ErrInvalidSessionName | 27 | + | return ErrEphemeralSessionName |
| 25 | } | 28 | } | ||
| 26 | return nil | 29 | return nil | ||
| 27 | case ports.IntentNew, ports.IntentAttach, ports.IntentResume: | 30 | case ports.IntentNew, ports.IntentAttach, ports.IntentResume: | ||
M internal/usecase/daemon/remote_attach_test.go +6 -1
| @@ -59,7 +59,7 @@ func TestRemotePickerRichHandoffRejectsReplacedLifecycle(t *testing.T) { | |||
| 59 | 59 | d.remoteCatalog.mu.Lock() | |
| 60 | 60 | d.remoteCatalog.status["arch"] = remoteHostFresh | |
| 61 | 61 | d.remoteCatalog.mu.Unlock() | |
| 62 | - | sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "local") | |
| 62 | + | sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local") | |
| 63 | 63 | token := sess.attachmentToken(ac, ac.transport()) | |
| 64 | 64 | effect, admitted := ac.beginAttachmentEffect(token) | |
| 65 | 65 | require.True(t, admitted) | |
| @@ -69,4 +69,9 @@ func TestRemotePickerRichHandoffRejectsReplacedLifecycle(t *testing.T) { | |||
| 69 | 69 | remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"} | |
| 70 | 70 | target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"} | |
| 71 | 71 | require.ErrorIs(t, d.sendRemoteAttachTargetForAttachment(token, target, key, sessionHandoffGuard{}, "picker-select"), errAttachmentTransition) | |
| 72 | + | select { | |
| 73 | + | case frame := <-sends: | |
| 74 | + | t.Fatalf("rejected handoff sent frame type %d", frame.Type) | |
| 75 | + | default: | |
| 76 | + | } | |
| 72 | 77 | } | |
| @@ -59,7 +59,7 @@ func TestRemotePickerRichHandoffRejectsReplacedLifecycle(t *testing.T) { | |||||
| 59 | d.remoteCatalog.mu.Lock() | 59 | d.remoteCatalog.mu.Lock() | ||
| 60 | d.remoteCatalog.status["arch"] = remoteHostFresh | 60 | d.remoteCatalog.status["arch"] = remoteHostFresh | ||
| 61 | d.remoteCatalog.mu.Unlock() | 61 | d.remoteCatalog.mu.Unlock() | ||
| 62 | - | sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "local") | 62 | + | sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local") |
| 63 | token := sess.attachmentToken(ac, ac.transport()) | 63 | token := sess.attachmentToken(ac, ac.transport()) | ||
| 64 | effect, admitted := ac.beginAttachmentEffect(token) | 64 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 65 | require.True(t, admitted) | 65 | require.True(t, admitted) | ||
| @@ -69,4 +69,9 @@ func TestRemotePickerRichHandoffRejectsReplacedLifecycle(t *testing.T) { | |||||
| 69 | remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"} | 69 | remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"} | ||
| 70 | target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"} | 70 | target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"} | ||
| 71 | require.ErrorIs(t, d.sendRemoteAttachTargetForAttachment(token, target, key, sessionHandoffGuard{}, "picker-select"), errAttachmentTransition) | 71 | require.ErrorIs(t, d.sendRemoteAttachTargetForAttachment(token, target, key, sessionHandoffGuard{}, "picker-select"), errAttachmentTransition) | ||
| 72 | + | select { | |||
| 73 | + | case frame := <-sends: | |||
| 74 | + | t.Fatalf("rejected handoff sent frame type %d", frame.Type) | |||
| 75 | + | default: | |||
| 76 | + | } | |||
| 72 | } | 77 | } | ||