Merge pull request #208 from bnema/fix/session-transfer-observability
fix(daemon): trace tab and pane move outcomes
4 files · +244 -4
M internal/usecase/daemon/move.go +25 -2
| @@ -32,8 +32,30 @@ type moveTabRequest struct { | |||
| 32 | 32 | // section starts, the caller holds the ordered architecture locks and only | |
| 33 | 33 | // non-failing in-memory writes are permitted. | |
| 34 | 34 | func (d *Daemon) movePane(req movePaneRequest) (result error) { | |
| 35 | - | defer func() { result = normalizeMoveRejection(result) }() | |
| 36 | - | if d == nil || req.Source.ID == "" || req.Destination.ID == "" || | |
| 35 | + | if d == nil { | |
| 36 | + | return errMovePaneInvalid | |
| 37 | + | } | |
| 38 | + | d.log.Info("pane move requested", | |
| 39 | + | "source_session_id", req.Source.ID, | |
| 40 | + | "source_tab_id", req.SourceTabID, | |
| 41 | + | "source_pane_id", req.SourcePaneID, | |
| 42 | + | "destination_session_id", req.Destination.ID, | |
| 43 | + | "destination_tab_id", req.DestinationTabID, | |
| 44 | + | ) | |
| 45 | + | defer func() { | |
| 46 | + | result = normalizeMoveRejection(result) | |
| 47 | + | if result != nil { | |
| 48 | + | d.log.Warn("pane move rejected", | |
| 49 | + | "err", result, | |
| 50 | + | "source_session_id", req.Source.ID, | |
| 51 | + | "source_tab_id", req.SourceTabID, | |
| 52 | + | "source_pane_id", req.SourcePaneID, | |
| 53 | + | "destination_session_id", req.Destination.ID, | |
| 54 | + | "destination_tab_id", req.DestinationTabID, | |
| 55 | + | ) | |
| 56 | + | } | |
| 57 | + | }() | |
| 58 | + | if req.Source.ID == "" || req.Destination.ID == "" || | |
| 37 | 59 | req.SourceTabID == "" || req.SourcePaneID == "" || req.DestinationTabID == "" { | |
| 38 | 60 | return errMovePaneInvalid | |
| 39 | 61 | } | |
| @@ -150,6 +172,7 @@ func (d *Daemon) movePane(req movePaneRequest) (result error) { | |||
| 150 | 172 | sourceTab: sourceTab, | |
| 151 | 173 | destinationTab: destinationTab, | |
| 152 | 174 | movedPane: movedPane, | |
| 175 | + | operation: "pane", | |
| 153 | 176 | sourceAttachments: admission.sourceAttachments, | |
| 154 | 177 | syncCleanup: commit.syncCleanup, | |
| 155 | 178 | frozenEffects: frozen, | |
| @@ -32,8 +32,30 @@ type moveTabRequest struct { | |||||
| 32 | // section starts, the caller holds the ordered architecture locks and only | 32 | // section starts, the caller holds the ordered architecture locks and only | ||
| 33 | // non-failing in-memory writes are permitted. | 33 | // non-failing in-memory writes are permitted. | ||
| 34 | func (d *Daemon) movePane(req movePaneRequest) (result error) { | 34 | func (d *Daemon) movePane(req movePaneRequest) (result error) { | ||
| 35 | - | defer func() { result = normalizeMoveRejection(result) }() | 35 | + | if d == nil { |
| 36 | - | if d == nil || req.Source.ID == "" || req.Destination.ID == "" || | 36 | + | return errMovePaneInvalid |
| 37 | + | } | |||
| 38 | + | d.log.Info("pane move requested", | |||
| 39 | + | "source_session_id", req.Source.ID, | |||
| 40 | + | "source_tab_id", req.SourceTabID, | |||
| 41 | + | "source_pane_id", req.SourcePaneID, | |||
| 42 | + | "destination_session_id", req.Destination.ID, | |||
| 43 | + | "destination_tab_id", req.DestinationTabID, | |||
| 44 | + | ) | |||
| 45 | + | defer func() { | |||
| 46 | + | result = normalizeMoveRejection(result) | |||
| 47 | + | if result != nil { | |||
| 48 | + | d.log.Warn("pane move rejected", | |||
| 49 | + | "err", result, | |||
| 50 | + | "source_session_id", req.Source.ID, | |||
| 51 | + | "source_tab_id", req.SourceTabID, | |||
| 52 | + | "source_pane_id", req.SourcePaneID, | |||
| 53 | + | "destination_session_id", req.Destination.ID, | |||
| 54 | + | "destination_tab_id", req.DestinationTabID, | |||
| 55 | + | ) | |||
| 56 | + | } | |||
| 57 | + | }() | |||
| 58 | + | if req.Source.ID == "" || req.Destination.ID == "" || | |||
| 37 | req.SourceTabID == "" || req.SourcePaneID == "" || req.DestinationTabID == "" { | 59 | req.SourceTabID == "" || req.SourcePaneID == "" || req.DestinationTabID == "" { | ||
| 38 | return errMovePaneInvalid | 60 | return errMovePaneInvalid | ||
| 39 | } | 61 | } | ||
| @@ -150,6 +172,7 @@ func (d *Daemon) movePane(req movePaneRequest) (result error) { | |||||
| 150 | sourceTab: sourceTab, | 172 | sourceTab: sourceTab, | ||
| 151 | destinationTab: destinationTab, | 173 | destinationTab: destinationTab, | ||
| 152 | movedPane: movedPane, | 174 | movedPane: movedPane, | ||
| 175 | + | operation: "pane", | |||
| 153 | sourceAttachments: admission.sourceAttachments, | 176 | sourceAttachments: admission.sourceAttachments, | ||
| 154 | syncCleanup: commit.syncCleanup, | 177 | syncCleanup: commit.syncCleanup, | ||
| 155 | frozenEffects: frozen, | 178 | frozenEffects: frozen, | ||
A internal/usecase/daemon/move_observability_test.go +172 -0
| @@ -0,0 +1,172 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 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 | + | ||
| 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{} | |
| 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 | + | ||
| 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() | |
| 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 | + | ||
| 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 | + | } | |
| 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 | + | } | |
| @@ -0,0 +1,172 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 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 | + | ||||
| 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{} | |||
| 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 | + | ||||
| 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() | |||
| 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 | + | ||||
| 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 | + | } | |||
| 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 | + | } | |||
M internal/usecase/daemon/move_pane_postcommit.go +26 -0
| @@ -26,6 +26,7 @@ type movePanePostcommitPlan struct { | |||
| 26 | 26 | destinationTab *tab | |
| 27 | 27 | movedPane *pane | |
| 28 | 28 | movedPanes []*pane | |
| 29 | + | operation string | |
| 29 | 30 | sourceAttachments []*attachedClient | |
| 30 | 31 | syncCleanup syncTimerCleanup | |
| 31 | 32 | frozenEffects frozenAttachmentEffectGates | |
| @@ -79,6 +80,22 @@ func (p movePanePostcommitPlan) execute(d *Daemon) { | |||
| 79 | 80 | } | |
| 80 | 81 | p.unlockDispatch() | |
| 81 | 82 | p.reservation.Release() | |
| 83 | + | attrs := []any{ | |
| 84 | + | "operation", p.operation, | |
| 85 | + | "source_session", p.source.name, | |
| 86 | + | "source_session_id", p.source.id, | |
| 87 | + | "source_tab_id", p.sourceTab.stableID, | |
| 88 | + | "destination_session", p.destination.name, | |
| 89 | + | "destination_session_id", p.destination.id, | |
| 90 | + | "source_retired", p.sourceEmpty, | |
| 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...) | |
| 82 | 99 | ||
| 83 | 100 | // Repair attachment-local stable targets after shared topology publication | |
| 84 | 101 | // and after the move dispatch locks have been released. | |
| @@ -114,6 +131,15 @@ func (p movePanePostcommitPlan) execute(d *Daemon) { | |||
| 114 | 131 | if p.sourceEmpty { | |
| 115 | 132 | retireEmptySessionAfterMove(d, p.source) | |
| 116 | 133 | } | |
| 134 | + | if len(p.retiredAttachments) != 0 { | |
| 135 | + | d.log.Info("move detaching source attachments", | |
| 136 | + | "operation", p.operation, | |
| 137 | + | "source_session", p.source.name, | |
| 138 | + | "source_session_id", p.source.id, | |
| 139 | + | "reason", "session-killed", | |
| 140 | + | "attachments", len(p.retiredAttachments), | |
| 141 | + | ) | |
| 142 | + | } | |
| 117 | 143 | for _, attachment := range p.retiredAttachments { | |
| 118 | 144 | d.unregisterPreview(attachment.ac) | |
| 119 | 145 | attachment.ac.clearCaptureFrames() | |
| @@ -26,6 +26,7 @@ type movePanePostcommitPlan struct { | |||||
| 26 | destinationTab *tab | 26 | destinationTab *tab | ||
| 27 | movedPane *pane | 27 | movedPane *pane | ||
| 28 | movedPanes []*pane | 28 | movedPanes []*pane | ||
| 29 | + | operation string | |||
| 29 | sourceAttachments []*attachedClient | 30 | sourceAttachments []*attachedClient | ||
| 30 | syncCleanup syncTimerCleanup | 31 | syncCleanup syncTimerCleanup | ||
| 31 | frozenEffects frozenAttachmentEffectGates | 32 | frozenEffects frozenAttachmentEffectGates | ||
| @@ -79,6 +80,22 @@ func (p movePanePostcommitPlan) execute(d *Daemon) { | |||||
| 79 | } | 80 | } | ||
| 80 | p.unlockDispatch() | 81 | p.unlockDispatch() | ||
| 81 | p.reservation.Release() | 82 | p.reservation.Release() | ||
| 83 | + | attrs := []any{ | |||
| 84 | + | "operation", p.operation, | |||
| 85 | + | "source_session", p.source.name, | |||
| 86 | + | "source_session_id", p.source.id, | |||
| 87 | + | "source_tab_id", p.sourceTab.stableID, | |||
| 88 | + | "destination_session", p.destination.name, | |||
| 89 | + | "destination_session_id", p.destination.id, | |||
| 90 | + | "source_retired", p.sourceEmpty, | |||
| 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...) | |||
| 82 | 99 | ||||
| 83 | // Repair attachment-local stable targets after shared topology publication | 100 | // Repair attachment-local stable targets after shared topology publication | ||
| 84 | // and after the move dispatch locks have been released. | 101 | // and after the move dispatch locks have been released. | ||
| @@ -114,6 +131,15 @@ func (p movePanePostcommitPlan) execute(d *Daemon) { | |||||
| 114 | if p.sourceEmpty { | 131 | if p.sourceEmpty { | ||
| 115 | retireEmptySessionAfterMove(d, p.source) | 132 | retireEmptySessionAfterMove(d, p.source) | ||
| 116 | } | 133 | } | ||
| 134 | + | if len(p.retiredAttachments) != 0 { | |||
| 135 | + | d.log.Info("move detaching source attachments", | |||
| 136 | + | "operation", p.operation, | |||
| 137 | + | "source_session", p.source.name, | |||
| 138 | + | "source_session_id", p.source.id, | |||
| 139 | + | "reason", "session-killed", | |||
| 140 | + | "attachments", len(p.retiredAttachments), | |||
| 141 | + | ) | |||
| 142 | + | } | |||
| 117 | for _, attachment := range p.retiredAttachments { | 143 | for _, attachment := range p.retiredAttachments { | ||
| 118 | d.unregisterPreview(attachment.ac) | 144 | d.unregisterPreview(attachment.ac) | ||
| 119 | attachment.ac.clearCaptureFrames() | 145 | attachment.ac.clearCaptureFrames() | ||
M internal/usecase/daemon/move_tab.go +21 -2
| @@ -42,8 +42,26 @@ type moveTabCommit struct { | |||
| 42 | 42 | } | |
| 43 | 43 | ||
| 44 | 44 | func (d *Daemon) moveTab(req moveTabRequest) (result error) { | |
| 45 | - | defer func() { result = normalizeMoveRejection(result) }() | |
| 46 | - | if d == nil || req.Source.ID == "" || req.Destination.ID == "" || req.SourceTabID == "" || req.Source.ID == req.Destination.ID { | |
| 45 | + | if d == nil { | |
| 46 | + | return errMovePaneInvalid | |
| 47 | + | } | |
| 48 | + | d.log.Info("tab move requested", | |
| 49 | + | "source_session_id", req.Source.ID, | |
| 50 | + | "source_tab_id", req.SourceTabID, | |
| 51 | + | "destination_session_id", req.Destination.ID, | |
| 52 | + | ) | |
| 53 | + | defer func() { | |
| 54 | + | result = normalizeMoveRejection(result) | |
| 55 | + | if result != nil { | |
| 56 | + | d.log.Warn("tab move rejected", | |
| 57 | + | "err", result, | |
| 58 | + | "source_session_id", req.Source.ID, | |
| 59 | + | "source_tab_id", req.SourceTabID, | |
| 60 | + | "destination_session_id", req.Destination.ID, | |
| 61 | + | ) | |
| 62 | + | } | |
| 63 | + | }() | |
| 64 | + | if req.Source.ID == "" || req.Destination.ID == "" || req.SourceTabID == "" || req.Source.ID == req.Destination.ID { | |
| 47 | 65 | return errMovePaneInvalid | |
| 48 | 66 | } | |
| 49 | 67 | d.mu.Lock() | |
| @@ -134,6 +152,7 @@ func (d *Daemon) moveTab(req moveTabRequest) (result error) { | |||
| 134 | 152 | destinationTab: admission.tab, | |
| 135 | 153 | movedPane: firstMovePane(admission.panes), | |
| 136 | 154 | movedPanes: admission.panes, | |
| 155 | + | operation: "tab", | |
| 137 | 156 | sourceAttachments: admission.sourceAttachments, | |
| 138 | 157 | syncCleanup: commit.syncCleanup, | |
| 139 | 158 | frozenEffects: frozen, | |
| @@ -42,8 +42,26 @@ type moveTabCommit struct { | |||||
| 42 | } | 42 | } | ||
| 43 | 43 | ||||
| 44 | func (d *Daemon) moveTab(req moveTabRequest) (result error) { | 44 | func (d *Daemon) moveTab(req moveTabRequest) (result error) { | ||
| 45 | - | defer func() { result = normalizeMoveRejection(result) }() | 45 | + | if d == nil { |
| 46 | - | if d == nil || req.Source.ID == "" || req.Destination.ID == "" || req.SourceTabID == "" || req.Source.ID == req.Destination.ID { | 46 | + | return errMovePaneInvalid |
| 47 | + | } | |||
| 48 | + | d.log.Info("tab move requested", | |||
| 49 | + | "source_session_id", req.Source.ID, | |||
| 50 | + | "source_tab_id", req.SourceTabID, | |||
| 51 | + | "destination_session_id", req.Destination.ID, | |||
| 52 | + | ) | |||
| 53 | + | defer func() { | |||
| 54 | + | result = normalizeMoveRejection(result) | |||
| 55 | + | if result != nil { | |||
| 56 | + | d.log.Warn("tab move rejected", | |||
| 57 | + | "err", result, | |||
| 58 | + | "source_session_id", req.Source.ID, | |||
| 59 | + | "source_tab_id", req.SourceTabID, | |||
| 60 | + | "destination_session_id", req.Destination.ID, | |||
| 61 | + | ) | |||
| 62 | + | } | |||
| 63 | + | }() | |||
| 64 | + | if req.Source.ID == "" || req.Destination.ID == "" || req.SourceTabID == "" || req.Source.ID == req.Destination.ID { | |||
| 47 | return errMovePaneInvalid | 65 | return errMovePaneInvalid | ||
| 48 | } | 66 | } | ||
| 49 | d.mu.Lock() | 67 | d.mu.Lock() | ||
| @@ -134,6 +152,7 @@ func (d *Daemon) moveTab(req moveTabRequest) (result error) { | |||||
| 134 | destinationTab: admission.tab, | 152 | destinationTab: admission.tab, | ||
| 135 | movedPane: firstMovePane(admission.panes), | 153 | movedPane: firstMovePane(admission.panes), | ||
| 136 | movedPanes: admission.panes, | 154 | movedPanes: admission.panes, | ||
| 155 | + | operation: "tab", | |||
| 137 | sourceAttachments: admission.sourceAttachments, | 156 | sourceAttachments: admission.sourceAttachments, | ||
| 138 | syncCleanup: commit.syncCleanup, | 157 | syncCleanup: commit.syncCleanup, | ||
| 139 | frozenEffects: frozen, | 158 | frozenEffects: frozen, | ||