detached 610e63d

fix(remote): unify target validation errors

commit
610e63d1fe5f0dbb25e24120a037d661c1470d90
author
brice · · 2026-08-08 18:10 UTC
parent
417c304
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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
9191 }
9292 home := capabilities&NavigationCapabilityHomePicker != 0
9393 back := capabilities&NavigationCapabilityBack != 0
94- if home && !remoteTarget || back != (overlay == StartupOverlaySessionPicker) {
94+ if (home && !remoteTarget) || back != (overlay == StartupOverlaySessionPicker) {
9595 return ErrInvalidNavigation
9696 }
9797 if remoteTarget && back {
@@ -91,7 +91,7 @@ func ValidateNavigation(capabilities NavigationCapabilities, overlay StartupOver
91 }91 }
92 home := capabilities&NavigationCapabilityHomePicker != 092 home := capabilities&NavigationCapabilityHomePicker != 0
93 back := capabilities&NavigationCapabilityBack != 093 back := capabilities&NavigationCapabilityBack != 0
94- if home && !remoteTarget || back != (overlay == StartupOverlaySessionPicker) {94+ if (home && !remoteTarget) || back != (overlay == StartupOverlaySessionPicker) {
95 return ErrInvalidNavigation95 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 {
236236 }
237237
238238 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)
255241 }
256242 if err := ports.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.RemoteTarget != nil || request.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned); err != nil {
257243 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 (
88 "github.com/bnema/vev/internal/ports"
99 )
1010
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+)
1215
1316 // SessionTarget is the validated daemon-facing session selection. Endpoint
1417 // details are resolved before a transport is opened and never enter this type.
@@ -21,7 +24,7 @@ func (t SessionTarget) validate() error {
2124 switch t.Intent {
2225 case ports.IntentEphemeral:
2326 if t.SessionName != "" {
24- return domain.ErrInvalidSessionName
27+ return ErrEphemeralSessionName
2528 }
2629 return nil
2730 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. Endpoint16 // 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.ErrInvalidSessionName27+ return ErrEphemeralSessionName
25 }28 }
26 return nil29 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) {
5959 d.remoteCatalog.mu.Lock()
6060 d.remoteCatalog.status["arch"] = remoteHostFresh
6161 d.remoteCatalog.mu.Unlock()
62- sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "local")
62+ sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local")
6363 token := sess.attachmentToken(ac, ac.transport())
6464 effect, admitted := ac.beginAttachmentEffect(token)
6565 require.True(t, admitted)
@@ -69,4 +69,9 @@ func TestRemotePickerRichHandoffRejectsReplacedLifecycle(t *testing.T) {
6969 remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"}
7070 target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"}
7171 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+ }
7277 }
@@ -59,7 +59,7 @@ func TestRemotePickerRichHandoffRejectsReplacedLifecycle(t *testing.T) {
59 d.remoteCatalog.mu.Lock()59 d.remoteCatalog.mu.Lock()
60 d.remoteCatalog.status["arch"] = remoteHostFresh60 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 }

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