move_observability_test.go RawHistoryFolder

1package daemon
2
3import (
4	"bytes"
5	"context"
6	"log/slog"
7	"testing"
8
9	"github.com/stretchr/testify/require"
10
11	"github.com/bnema/vev/internal/domain"
12)
13
14type moveObservabilityFixture struct {
15	daemon         *Daemon
16	source         *session
17	sourceTab      *tab
18	sourcePane     *pane
19	destination    *session
20	destinationTab *tab
21	logs           *bytes.Buffer
22}
23
24func newMoveObservabilityFixture(t *testing.T) moveObservabilityFixture {
25	t.Helper()
26	logs := &bytes.Buffer{}
27	d, source, _, _ := newManualSessionWithPTYs(t, newQuietPTY())
28	d.log = slog.New(slog.NewJSONHandler(logs, nil))
29
30	sourceTab := source.tabs[0]
31	sourceTab.stableID = "source-tab"
32	sourcePane := sourceTab.focusedPane()
33	sourcePane.stableID = "source-pane"
34
35	destinationCtx, destinationCancel := context.WithCancel(d.serveCtx)
36	t.Cleanup(destinationCancel)
37	destinationTab := newTabWithStableID("destination-tab", "destination-pane", newQuietPTY(), domain.Size{Cols: 80, Rows: 23})
38	destinationTab.ctx, destinationTab.cancel = context.WithCancel(destinationCtx)
39	destination := &session{
40		sessionCore: sessionCore{id: "destination", name: "destination", incarnation: domain.IncarnationID{9}},
41		ctx:         destinationCtx,
42		cancel:      destinationCancel,
43		tabs:        []*tab{destinationTab},
44	}
45	publishTiledPaneOwners(destination, destinationTab)
46	d.mu.Lock()
47	d.sessions[destination.id] = destination
48	d.mu.Unlock()
49
50	return moveObservabilityFixture{
51		daemon: d, source: source, sourceTab: sourceTab, sourcePane: sourcePane,
52		destination: destination, destinationTab: destinationTab, logs: logs,
53	}
54}
55
56func TestMoveLogsCommitAndSourceAttachmentTermination(t *testing.T) {
57	tests := []struct {
58		name      string
59		operation string
60		move      func(moveObservabilityFixture) error
61		assertLog func(*testing.T, string)
62	}{
63		{
64			name:      "tab",
65			operation: "tab",
66			move: func(f moveObservabilityFixture) error {
67				return f.daemon.moveTab(moveTabRequest{
68					Source:      moveSessionLocator{ID: f.source.id, Incarnation: f.source.incarnation},
69					SourceTabID: domain.TabStableID(f.sourceTab.stableID),
70					Destination: moveSessionLocator{ID: f.destination.id, Incarnation: f.destination.incarnation},
71				})
72			},
73			assertLog: func(t *testing.T, output string) {
74				t.Helper()
75				require.Contains(t, output, `"msg":"tab move requested"`)
76			},
77		},
78		{
79			name:      "pane",
80			operation: "pane",
81			move: func(f moveObservabilityFixture) error {
82				return f.daemon.movePane(movePaneRequest{
83					Source:           moveSessionLocator{ID: f.source.id, Incarnation: f.source.incarnation},
84					SourceTabID:      domain.TabStableID(f.sourceTab.stableID),
85					SourcePaneID:     domain.PaneStableID(f.sourcePane.stableID),
86					Destination:      moveSessionLocator{ID: f.destination.id, Incarnation: f.destination.incarnation},
87					DestinationTabID: domain.TabStableID(f.destinationTab.stableID),
88				})
89			},
90			assertLog: func(t *testing.T, output string) {
91				t.Helper()
92				require.Contains(t, output, `"msg":"pane move requested"`)
93				require.Contains(t, output, `"source_pane_id":"source-pane","destination_tab_id":"destination-tab"`)
94			},
95		},
96	}
97
98	for _, tt := range tests {
99		t.Run(tt.name, func(t *testing.T) {
100			fixture := newMoveObservabilityFixture(t)
101			require.NoError(t, tt.move(fixture))
102			fixture.daemon.waitNotifies()
103
104			output := fixture.logs.String()
105			tt.assertLog(t, output)
106			require.Contains(t, output, `"msg":"move committed"`)
107			require.Contains(t, output, `"operation":"`+tt.operation+`"`)
108			require.Contains(t, output, `"source_retired":true`)
109			require.Contains(t, output, `"msg":"move detaching source attachments"`)
110			require.Contains(t, output, `"reason":"session-killed"`)
111			require.Contains(t, output, `"msg":"client detached"`)
112			require.Contains(t, output, `"reason":1`)
113		})
114	}
115}
116
117func TestMoveLogsNormalizedRejection(t *testing.T) {
118	tests := []struct {
119		name      string
120		operation string
121		move      func(*Daemon) error
122		fields    []string
123	}{
124		{
125			name:      "tab",
126			operation: "tab",
127			move: func(d *Daemon) error {
128				return d.moveTab(moveTabRequest{
129					Source:      moveSessionLocator{ID: "missing", Incarnation: domain.IncarnationID{1}},
130					SourceTabID: "tab",
131					Destination: moveSessionLocator{ID: "also-missing", Incarnation: domain.IncarnationID{2}},
132				})
133			},
134			fields: []string{
135				`"source_session_id":"missing"`,
136				`"destination_session_id":"also-missing"`,
137			},
138		},
139		{
140			name:      "pane",
141			operation: "pane",
142			move: func(d *Daemon) error {
143				return d.movePane(movePaneRequest{
144					Source:           moveSessionLocator{ID: "missing", Incarnation: domain.IncarnationID{1}},
145					SourceTabID:      "source-tab",
146					SourcePaneID:     "source-pane",
147					Destination:      moveSessionLocator{ID: "also-missing", Incarnation: domain.IncarnationID{2}},
148					DestinationTabID: "destination-tab",
149				})
150			},
151			fields: []string{
152				`"source_pane_id":"source-pane"`,
153				`"destination_tab_id":"destination-tab"`,
154			},
155		},
156	}
157
158	for _, tt := range tests {
159		t.Run(tt.name, func(t *testing.T) {
160			var logs bytes.Buffer
161			d := New(nil, stubClock{}, slog.New(slog.NewJSONHandler(&logs, nil)))
162
163			require.ErrorIs(t, tt.move(d), errMovePaneInvalid)
164			output := logs.String()
165			require.Contains(t, output, `"msg":"`+tt.operation+` move requested"`)
166			require.Contains(t, output, `"msg":"`+tt.operation+` move rejected"`)
167			for _, field := range tt.fields {
168				require.Contains(t, output, field)
169			}
170		})
171	}
172}

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