client_test.go RawHistoryFolder

1package sessionwire
2
3import (
4	"testing"
5	"time"
6
7	"github.com/bnema/vev/internal/domain"
8	"github.com/bnema/vev/internal/ports"
9	"github.com/bnema/vev/internal/protocol"
10	"github.com/bnema/vev/internal/protocol/wire"
11	"github.com/stretchr/testify/require"
12)
13
14func TestClientConnectionEncodesEveryClientMessage(t *testing.T) {
15	target := testTarget()
16	exact := protocol.ExactSessionTarget{LifecycleID: target.LifecycleID, SessionName: target.SessionName}
17	tests := []struct {
18		name    string
19		message protocol.ClientMessage
20	}{
21		{name: "hello", message: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}},
22		{name: "input", message: protocol.Input{InputSeq: 1, Data: []byte("x")}},
23		{name: "resize", message: protocol.Resize{Size: domain.Size{Cols: 80, Rows: 24}}},
24		{name: "detach", message: protocol.Detach{}},
25		{name: "closed detach", message: protocol.Detach{Closed: true}},
26		{name: "ping", message: protocol.Ping{}},
27		{name: "list", message: protocol.List{}},
28		{name: "kill", message: protocol.Kill{RequestID: 1, Name: "work"}},
29		{name: "theme", message: protocol.Theme{TrueColor: true}},
30		{name: "ack", message: protocol.Ack{Epoch: 1, State: 1}},
31		{name: "image", message: protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}}},
32		{name: "notice", message: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}},
33		{name: "command", message: protocol.CommandRequest{Version: protocol.Version, RequestID: 1, Slug: "list-sessions"}},
34		{name: "reset", message: protocol.OutputResetRequest{}},
35		{name: "UI fence", message: protocol.UIFence{ActionID: 7}},
36		{name: "UI fence pointer", message: &protocol.UIFence{ActionID: 7}},
37		{name: "select tab", message: protocol.SelectTab{TabID: "tab-2"}},
38		{name: "preview", message: protocol.RemotePreviewWatch{Request: protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1}, MinInterval: 33 * time.Millisecond}},
39		{name: "attention", message: protocol.RouteAttentionSubscription{}},
40		{name: "same peer", message: protocol.SamePeerSwitchRequest{RequestID: 1, Target: exact}},
41		{name: "snapshot", message: protocol.RecentRouteSnapshot{}},
42		{name: "route failure", message: protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable}},
43		{name: "creation failure", message: protocol.SessionCreationFailure{RequestID: 1, Code: protocol.RouteFailureUnavailable}},
44		{name: "creation failure pointer", message: &protocol.SessionCreationFailure{RequestID: 1, Code: protocol.RouteFailureUnavailable}},
45	}
46	for _, tt := range tests {
47		t.Run(tt.name, func(t *testing.T) {
48			raw := &scriptedTransport{recv: []wire.Envelope{mustPreambleResponse(t)}}
49			require.NoError(t, NewClientConnection(raw).SendClient(tt.message))
50			got, err := DecodeClientEnvelope(mustSingleAppPayload(t, raw))
51			require.NoError(t, err)
52			switch message := tt.message.(type) {
53			case *protocol.SessionCreationFailure:
54				require.Equal(t, *message, got)
55			case *protocol.UIFence:
56				require.Equal(t, *message, got)
57			default:
58				require.Equal(t, tt.message, got)
59			}
60		})
61	}
62}
63
64func TestClientConnectionDecodesEveryServerMessage(t *testing.T) {
65	exact := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}
66	tests := []struct {
67		name    string
68		message protocol.ServerMessage
69	}{
70		{name: "welcome", message: protocol.Welcome{SessionID: "session"}},
71		{name: "error", message: protocol.ErrorMsg{Code: protocol.ErrInternal, Text: "error"}},
72		{name: "output", message: protocol.Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}, Context: testUIOutputContext()}},
73		{name: "detached", message: protocol.Detached{Reason: protocol.ReasonDetach}},
74		{name: "pong", message: protocol.Pong{}},
75		{name: "sessions", message: protocol.Sessions{}},
76		{name: "command result", message: protocol.CommandResult{RequestID: 1, Outcome: protocol.CommandSucceeded}},
77		{name: "kill result", message: protocol.KillResult{RequestID: 1, Outcome: protocol.KillSucceeded}},
78		{name: "kill result partial failures", message: protocol.KillResult{RequestID: 2, Outcome: protocol.KillFailed, Code: protocol.ErrInternal, Text: "partial", Failures: []protocol.KillFailure{{Class: "stopped", Name: "work", Text: "boom"}}}},
79		{name: "attach target", message: protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}},
80		{name: "preview", message: protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}},
81		{name: "identity", message: protocol.CommittedRouteIdentity{Target: exact}},
82		{name: "route action", message: protocol.RouteNavigationAction{SnapshotGeneration: 1, Key: 1, Generation: 1}},
83		{name: "route create", message: protocol.RouteCreateSessionAction{RequestID: 1, SnapshotGeneration: 1, Key: 1, Generation: 1, SessionName: "example"}},
84		{name: "route failure", message: protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable}},
85		{name: "route position", message: protocol.RoutePosition{Target: exact, ActiveTabID: "tab-1"}},
86		{name: "switch failure", message: protocol.SamePeerSwitchFailure{RequestID: 1, Code: protocol.SamePeerSwitchUnavailable}},
87	}
88	for _, tt := range tests {
89		t.Run(tt.name, func(t *testing.T) {
90			raw := &scriptedTransport{recv: mustClientPreambleQueue(t, mustEncodeServer(t, tt.message))}
91			got, err := NewClientConnection(raw).ReceiveServer()
92			require.NoError(t, err)
93			require.Equal(t, tt.message, got)
94		})
95	}
96}
97
98func TestClientConnectionClassifiesFailuresAndPreservesCapabilities(t *testing.T) {
99	t.Run("wrong direction", func(t *testing.T) {
100		// A client envelope on the server-to-client path is rejected
101		// before any mutation: its fields are unknown to the server
102		// envelope, so decoding reports the wrong-direction category.
103		raw := &scriptedTransport{recv: mustClientPreambleQueue(t, mustEncodeClient(t, protocol.Input{InputSeq: 1, Data: []byte("x")}))}
104		_, err := NewClientConnection(raw).ReceiveServer()
105		var failure *protocol.DecodeFailure
106		require.ErrorAs(t, err, &failure)
107		require.Equal(t, protocol.DecodeUnknownType, failure.Category)
108	})
109
110	t.Run("truncated payload", func(t *testing.T) {
111		valid := mustEncodeServer(t, protocol.ErrorMsg{Code: protocol.ErrInternal, Text: "error"})
112		raw := &scriptedTransport{recv: mustClientPreambleQueue(t, valid[:1])}
113		_, err := NewClientConnection(raw).ReceiveServer()
114		var failure *protocol.DecodeFailure
115		require.ErrorAs(t, err, &failure)
116		require.Equal(t, protocol.DecodeMalformed, failure.Category)
117	})
118
119	t.Run("trailing garbage", func(t *testing.T) {
120		valid := mustEncodeServer(t, protocol.ErrorMsg{Code: protocol.ErrInternal, Text: "error"})
121		// A second top-level variant occurrence is trailing data, not
122		// a longer message: the scanner rejects duplicate variants.
123		duplicate := mustEncodeServer(t, protocol.Pong{})
124		raw := &scriptedTransport{recv: mustClientPreambleQueue(t, append(append([]byte(nil), valid...), duplicate...))}
125		_, err := NewClientConnection(raw).ReceiveServer()
126		var failure *protocol.DecodeFailure
127		require.ErrorAs(t, err, &failure)
128		require.Equal(t, protocol.DecodeMalformed, failure.Category)
129		require.ErrorIs(t, failure.Err, wire.ErrScanDuplicate)
130	})
131
132	t.Run("capabilities", func(t *testing.T) {
133		events := make(chan ports.LinkEvent, 1)
134		raw := &capableTransport{events: events}
135		connection := NewClientConnection(raw)
136		require.Equal(t, uint8(1), connection.Capabilities().PreferredOutputWindow)
137		require.True(t, connection.Capabilities().LinkState)
138		require.Equal(t, ports.LinkStateDegraded, connection.LinkState())
139		require.Equal(t, (<-chan ports.LinkEvent)(events), connection.LinkEvents())
140	})
141}

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