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}