detached d6fec55

test(daemon): cover pane move observability

commit
d6fec558e8d96b4d346b084c134ce3135147bb60
author
brice · · 2026-08-20 14:16 UTC
parent
80df2be
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +145 -36
M internal/usecase/daemon/move_observability_test.go +136 -34
@@ -11,10 +11,26 @@ import (
1111 "github.com/bnema/vev/internal/domain"
1212 )
1313
14-func TestMoveTabLogsCommitAndSourceAttachmentTermination(t *testing.T) {
15- var logs bytes.Buffer
14+type 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+
24+func newMoveObservabilityFixture(t *testing.T) moveObservabilityFixture {
25+ t.Helper()
26+ logs := &bytes.Buffer{}
1627 d, source, _, _ := newManualSessionWithPTYs(t, newQuietPTY())
17- d.log = slog.New(slog.NewJSONHandler(&logs, nil))
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"
1834
1935 destinationCtx, destinationCancel := context.WithCancel(d.serveCtx)
2036 t.Cleanup(destinationCancel)
@@ -31,40 +47,126 @@ func TestMoveTabLogsCommitAndSourceAttachmentTermination(t *testing.T) {
3147 d.sessions[destination.id] = destination
3248 d.mu.Unlock()
3349
34- sourceTab := source.tabs[0]
35- sourceTab.stableID = "source-tab"
36- require.NoError(t, d.moveTab(moveTabRequest{
37- Source: moveSessionLocator{ID: source.id, Incarnation: source.incarnation},
38- SourceTabID: domain.TabStableID(sourceTab.stableID),
39- Destination: moveSessionLocator{ID: destination.id, Incarnation: destination.incarnation},
40- }))
41- d.waitNotifies()
50+ return moveObservabilityFixture{
51+ daemon: d, source: source, sourceTab: sourceTab, sourcePane: sourcePane,
52+ destination: destination, destinationTab: destinationTab, logs: logs,
53+ }
54+}
55+
56+func 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()
42103
43- output := logs.String()
44- require.Contains(t, output, `"msg":"tab move requested"`)
45- require.Contains(t, output, `"msg":"move committed"`)
46- require.Contains(t, output, `"operation":"tab"`)
47- require.Contains(t, output, `"source_retired":true`)
48- require.Contains(t, output, `"msg":"move detaching source attachments"`)
49- require.Contains(t, output, `"reason":"session-killed"`)
50- require.Contains(t, output, `"msg":"client detached"`)
51- require.Contains(t, output, `"reason":1`)
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+ }
52115 }
53116
54-func TestMoveTabLogsNormalizedRejection(t *testing.T) {
55- var logs bytes.Buffer
56- d := New(nil, stubClock{}, slog.New(slog.NewJSONHandler(&logs, nil)))
117+func 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+ }
57157
58- err := d.moveTab(moveTabRequest{
59- Source: moveSessionLocator{ID: "missing", Incarnation: domain.IncarnationID{1}},
60- SourceTabID: "tab",
61- Destination: moveSessionLocator{ID: "also-missing", Incarnation: domain.IncarnationID{2}},
62- })
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)))
63162
64- require.ErrorIs(t, err, errMovePaneInvalid)
65- output := logs.String()
66- require.Contains(t, output, `"msg":"tab move requested"`)
67- require.Contains(t, output, `"msg":"tab move rejected"`)
68- require.Contains(t, output, `"source_session_id":"missing"`)
69- require.Contains(t, output, `"destination_session_id":"also-missing"`)
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+ }
70172 }
@@ -11,10 +11,26 @@ import (
11 "github.com/bnema/vev/internal/domain"11 "github.com/bnema/vev/internal/domain"
12 )12 )
13 13
14-func TestMoveTabLogsCommitAndSourceAttachmentTermination(t *testing.T) {14+type moveObservabilityFixture struct {
15- var logs bytes.Buffer15+ daemon *Daemon
16+ source *session
17+ sourceTab *tab
18+ sourcePane *pane
19+ destination *session
20+ destinationTab *tab
21+ logs *bytes.Buffer
22+}
23+
24+func newMoveObservabilityFixture(t *testing.T) moveObservabilityFixture {
25+ t.Helper()
26+ logs := &bytes.Buffer{}
16 d, source, _, _ := newManualSessionWithPTYs(t, newQuietPTY())27 d, source, _, _ := newManualSessionWithPTYs(t, newQuietPTY())
17- d.log = slog.New(slog.NewJSONHandler(&logs, nil))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"
18 34
19 destinationCtx, destinationCancel := context.WithCancel(d.serveCtx)35 destinationCtx, destinationCancel := context.WithCancel(d.serveCtx)
20 t.Cleanup(destinationCancel)36 t.Cleanup(destinationCancel)
@@ -31,40 +47,126 @@ func TestMoveTabLogsCommitAndSourceAttachmentTermination(t *testing.T) {
31 d.sessions[destination.id] = destination47 d.sessions[destination.id] = destination
32 d.mu.Unlock()48 d.mu.Unlock()
33 49
34- sourceTab := source.tabs[0]50+ return moveObservabilityFixture{
35- sourceTab.stableID = "source-tab"51+ daemon: d, source: source, sourceTab: sourceTab, sourcePane: sourcePane,
36- require.NoError(t, d.moveTab(moveTabRequest{52+ destination: destination, destinationTab: destinationTab, logs: logs,
37- Source: moveSessionLocator{ID: source.id, Incarnation: source.incarnation},53+ }
38- SourceTabID: domain.TabStableID(sourceTab.stableID),54+}
39- Destination: moveSessionLocator{ID: destination.id, Incarnation: destination.incarnation},55+
40- }))56+func TestMoveLogsCommitAndSourceAttachmentTermination(t *testing.T) {
41- d.waitNotifies()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()
42 103
43- output := logs.String()104+ output := fixture.logs.String()
44- require.Contains(t, output, `"msg":"tab move requested"`)105+ tt.assertLog(t, output)
45- require.Contains(t, output, `"msg":"move committed"`)106+ require.Contains(t, output, `"msg":"move committed"`)
46- require.Contains(t, output, `"operation":"tab"`)107+ require.Contains(t, output, `"operation":"`+tt.operation+`"`)
47- require.Contains(t, output, `"source_retired":true`)108+ require.Contains(t, output, `"source_retired":true`)
48- require.Contains(t, output, `"msg":"move detaching source attachments"`)109+ require.Contains(t, output, `"msg":"move detaching source attachments"`)
49- require.Contains(t, output, `"reason":"session-killed"`)110+ require.Contains(t, output, `"reason":"session-killed"`)
50- require.Contains(t, output, `"msg":"client detached"`)111+ require.Contains(t, output, `"msg":"client detached"`)
51- require.Contains(t, output, `"reason":1`)112+ require.Contains(t, output, `"reason":1`)
113+ })
114+ }
52 }115 }
53 116
54-func TestMoveTabLogsNormalizedRejection(t *testing.T) {117+func TestMoveLogsNormalizedRejection(t *testing.T) {
55- var logs bytes.Buffer118+ tests := []struct {
56- d := New(nil, stubClock{}, slog.New(slog.NewJSONHandler(&logs, nil)))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+ }
57 157
58- err := d.moveTab(moveTabRequest{158+ for _, tt := range tests {
59- Source: moveSessionLocator{ID: "missing", Incarnation: domain.IncarnationID{1}},159+ t.Run(tt.name, func(t *testing.T) {
60- SourceTabID: "tab",160+ var logs bytes.Buffer
61- Destination: moveSessionLocator{ID: "also-missing", Incarnation: domain.IncarnationID{2}},161+ d := New(nil, stubClock{}, slog.New(slog.NewJSONHandler(&logs, nil)))
62- })
63 162
64- require.ErrorIs(t, err, errMovePaneInvalid)163+ require.ErrorIs(t, tt.move(d), errMovePaneInvalid)
65- output := logs.String()164+ output := logs.String()
66- require.Contains(t, output, `"msg":"tab move requested"`)165+ require.Contains(t, output, `"msg":"`+tt.operation+` move requested"`)
67- require.Contains(t, output, `"msg":"tab move rejected"`)166+ require.Contains(t, output, `"msg":"`+tt.operation+` move rejected"`)
68- require.Contains(t, output, `"source_session_id":"missing"`)167+ for _, field := range tt.fields {
69- require.Contains(t, output, `"destination_session_id":"also-missing"`)168+ require.Contains(t, output, field)
169+ }
170+ })
171+ }
70 }172 }
M internal/usecase/daemon/move_pane_postcommit.go +9 -2
@@ -80,7 +80,7 @@ func (p movePanePostcommitPlan) execute(d *Daemon) {
8080 }
8181 p.unlockDispatch()
8282 p.reservation.Release()
83- d.log.Info("move committed",
83+ attrs := []any{
8484 "operation", p.operation,
8585 "source_session", p.source.name,
8686 "source_session_id", p.source.id,
@@ -88,7 +88,14 @@ func (p movePanePostcommitPlan) execute(d *Daemon) {
8888 "destination_session", p.destination.name,
8989 "destination_session_id", p.destination.id,
9090 "source_retired", p.sourceEmpty,
91- )
91+ }
92+ if p.operation == "pane" {
93+ attrs = append(attrs,
94+ "source_pane_id", p.movedPane.stableID,
95+ "destination_tab_id", p.destinationTab.stableID,
96+ )
97+ }
98+ d.log.Info("move committed", attrs...)
9299
93100 // Repair attachment-local stable targets after shared topology publication
94101 // and after the move dispatch locks have been released.
@@ -80,7 +80,7 @@ func (p movePanePostcommitPlan) execute(d *Daemon) {
80 }80 }
81 p.unlockDispatch()81 p.unlockDispatch()
82 p.reservation.Release()82 p.reservation.Release()
83- d.log.Info("move committed",83+ attrs := []any{
84 "operation", p.operation,84 "operation", p.operation,
85 "source_session", p.source.name,85 "source_session", p.source.name,
86 "source_session_id", p.source.id,86 "source_session_id", p.source.id,
@@ -88,7 +88,14 @@ func (p movePanePostcommitPlan) execute(d *Daemon) {
88 "destination_session", p.destination.name,88 "destination_session", p.destination.name,
89 "destination_session_id", p.destination.id,89 "destination_session_id", p.destination.id,
90 "source_retired", p.sourceEmpty,90 "source_retired", p.sourceEmpty,
91- )91+ }
92+ if p.operation == "pane" {
93+ attrs = append(attrs,
94+ "source_pane_id", p.movedPane.stableID,
95+ "destination_tab_id", p.destinationTab.stableID,
96+ )
97+ }
98+ d.log.Info("move committed", attrs...)
92 99
93 // Repair attachment-local stable targets after shared topology publication100 // Repair attachment-local stable targets after shared topology publication
94 // and after the move dispatch locks have been released.101 // and after the move dispatch locks have been released.

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