test(daemon): complete transactional resize red matrix
- commit
- d74f5ba24321f09dccac4d816daa6c16520ac6de
- author
- brice · · 2026-07-12 07:56 UTC
- parent
- e24215f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +117 -30
M internal/usecase/daemon/transactional_resize_test.go +117 -30
| @@ -5,11 +5,14 @@ import ( | |||
| 5 | 5 | "io" | |
| 6 | 6 | "sync" | |
| 7 | 7 | "testing" | |
| 8 | + | "time" | |
| 8 | 9 | ||
| 9 | 10 | "github.com/stretchr/testify/require" | |
| 10 | 11 | ||
| 11 | 12 | "github.com/bnema/vev/internal/domain" | |
| 13 | + | "github.com/bnema/vev/internal/ports" | |
| 12 | 14 | "github.com/bnema/vev/internal/usecase/layout" | |
| 15 | + | "github.com/bnema/vev/internal/usecase/visualsearch" | |
| 13 | 16 | ) | |
| 14 | 17 | ||
| 15 | 18 | // transactionalResizePTY is deliberately channel-scripted: a test can stop a | |
| @@ -104,24 +107,94 @@ func TestTransactionalResizePartialFailureCommitsOnlySuccessfulPTYState(t *testi | |||
| 104 | 107 | require.NotEqual(t, oldFailedRect, second.rect, "failed PTY rectangle still follows the committed layout") | |
| 105 | 108 | } | |
| 106 | 109 | ||
| 110 | + | // S3 acceptance: a failed member is retried only for the newest committed | |
| 111 | + | // epoch. An intervening request supersedes the old retry, and a successful | |
| 112 | + | // retry must resize its VT before the forced full-frame emission. | |
| 113 | + | func TestTransactionalResizeRetryTargetsNewestCommittedEpoch(t *testing.T) { | |
| 114 | + | ok := &transactionalResizePTY{} | |
| 115 | + | failed := &transactionalResizePTY{errs: []error{errors.New("first epoch fails"), nil}} | |
| 116 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, ok) | |
| 117 | + | tb := sess.activeTab() | |
| 118 | + | retry := newPane("retry", failed, domain.Size{Cols: 80, Rows: 23}) | |
| 119 | + | tb.mu.Lock() | |
| 120 | + | tb.panes[retry.id] = retry | |
| 121 | + | tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("retry")}}, Focus: "pane-1"} | |
| 122 | + | tb.mu.Unlock() | |
| 123 | + | ||
| 124 | + | d.resize(sess, ac, domain.Size{Cols: 100, Rows: 30}) // failed epoch | |
| 125 | + | d.resize(sess, ac, domain.Size{Cols: 120, Rows: 34}) // supersedes it | |
| 126 | + | ||
| 127 | + | // The failed pane must not be retried at 49x28 from the obsolete epoch. | |
| 128 | + | // S3 retries 59x32, then commits the parser/screen and requests a reset. | |
| 129 | + | require.Equal(t, []domain.Size{{Cols: 59, Rows: 32}}, failed.requested()) | |
| 130 | + | require.Equal(t, domain.Size{Cols: 59, Rows: 32}, domain.Size{Cols: retry.screen.Frame.Width, Rows: retry.screen.Frame.Height}) | |
| 131 | + | ac.sendMu.Lock() | |
| 132 | + | pending := ac.resizePaintPending | |
| 133 | + | ac.sendMu.Unlock() | |
| 134 | + | require.True(t, pending, "successful retry forces a final full frame") | |
| 135 | + | } | |
| 136 | + | ||
| 107 | 137 | // Stale lifecycle callbacks may never advance a transaction owned by a | |
| 108 | 138 | // replacement attachment. The coordinator test clock drives callbacks through | |
| 109 | 139 | // channels elsewhere in this package; this ownership assertion is deliberately | |
| 110 | 140 | // synchronous so it cannot hide a race behind a sleep. | |
| 111 | 141 | func TestTransactionalResizeEpochLifecycleAndRetryContract(t *testing.T) { | |
| 112 | - | rc := newRenderCoordinator(renderCoordinatorOptions{}) | |
| 113 | - | old, replacement := &attachedClient{}, &attachedClient{} | |
| 114 | - | rc.attach(old) | |
| 115 | - | require.Equal(t, uint64(1), rc.recordResizeRequest(domain.Size{Cols: 90, Rows: 24}, old)) | |
| 116 | - | require.Equal(t, uint64(2), rc.recordResizeRequest(domain.Size{Cols: 100, Rows: 26}, old)) | |
| 117 | - | require.Equal(t, uint64(3), rc.recordResizeRequest(domain.Size{Cols: 120, Rows: 30}, old)) | |
| 118 | - | rc.noteReplace(old, replacement) | |
| 119 | - | rc.attach(replacement) | |
| 120 | - | require.Zero(t, rc.recordResizeRequest(domain.Size{Cols: 20, Rows: 5}, old), | |
| 121 | - | "obsolete attach/replace/detach/park/resume callbacks cannot publish") | |
| 122 | - | snap := rc.resizeSnapshot() | |
| 123 | - | require.Equal(t, uint64(3), snap.epoch) | |
| 124 | - | require.Equal(t, domain.Size{Cols: 120, Rows: 30}, snap.size) | |
| 142 | + | // Each closure is a callback captured before its lifecycle transition. The | |
| 143 | + | // coordinator must reject it, rather than merely coalescing its paint. | |
| 144 | + | for _, transition := range []struct { | |
| 145 | + | name string | |
| 146 | + | run func(*renderCoordinator, *attachedClient, *attachedClient) | |
| 147 | + | }{ | |
| 148 | + | {"replace", func(rc *renderCoordinator, old, next *attachedClient) { rc.noteReplace(old, next) }}, | |
| 149 | + | {"detach", func(rc *renderCoordinator, old, _ *attachedClient) { rc.noteDetach(old) }}, | |
| 150 | + | {"park", func(rc *renderCoordinator, old, _ *attachedClient) { rc.notePark(old) }}, | |
| 151 | + | {"resume", func(rc *renderCoordinator, old, next *attachedClient) { rc.notePark(old); rc.attach(next) }}, | |
| 152 | + | } { | |
| 153 | + | t.Run(transition.name, func(t *testing.T) { | |
| 154 | + | rc := newRenderCoordinator(renderCoordinatorOptions{}) | |
| 155 | + | old, next := &attachedClient{}, &attachedClient{} | |
| 156 | + | rc.attach(old) | |
| 157 | + | require.Equal(t, uint64(1), rc.recordResizeRequest(domain.Size{Cols: 90, Rows: 24}, old)) | |
| 158 | + | stale := func() uint64 { return rc.recordResizeRequest(domain.Size{Cols: 20, Rows: 5}, old) } | |
| 159 | + | transition.run(rc, old, next) | |
| 160 | + | require.Zero(t, stale(), "a stale %s callback must not publish", transition.name) | |
| 161 | + | }) | |
| 162 | + | } | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | // S3 acceptance: three callbacks can be delivered even after their timers were | |
| 166 | + | // stopped. Only the newest prepared epoch may apply, commit, and emit one full | |
| 167 | + | // frame. coordinatorMockClock is a generated MockClock/MockTimer harness; its | |
| 168 | + | // channels make callback order explicit and do not use wall-clock waits. | |
| 169 | + | func TestTransactionalResizeObsoleteTimerCallbacksCommitOnlyLatestEpoch(t *testing.T) { | |
| 170 | + | clock := newCoordinatorMockClock(t, 8) | |
| 171 | + | pty := &transactionalResizePTY{} | |
| 172 | + | d, sess, ac, frames := newManualSessionWithPTYs(t, pty) | |
| 173 | + | d.clock = clock.clock | |
| 174 | + | ||
| 175 | + | for _, size := range []domain.Size{{Cols: 90, Rows: 24}, {Cols: 100, Rows: 28}, {Cols: 120, Rows: 32}} { | |
| 176 | + | d.resize(sess, ac, size) | |
| 177 | + | } | |
| 178 | + | // Current PR #71 applies each resize before its debounce callback. S3 must | |
| 179 | + | // leave this empty until the final callback's prepare/apply/commit epoch. | |
| 180 | + | require.Empty(t, pty.requested(), "obsolete requests must not resize the PTY") | |
| 181 | + | ||
| 182 | + | var timers []*coordinatorMockTimer | |
| 183 | + | for len(timers) < 3 { | |
| 184 | + | timers = append(timers, <-clock.timers) | |
| 185 | + | } | |
| 186 | + | for _, timer := range timers { | |
| 187 | + | timer.ch <- time.Time{} | |
| 188 | + | } | |
| 189 | + | require.Equal(t, []domain.Size{{Cols: 120, Rows: 30}}, pty.requested()) | |
| 190 | + | require.Equal(t, uint64(3), sess.renderCoordinator().resizeSnapshot().epoch) | |
| 191 | + | frame := <-frames | |
| 192 | + | require.Equal(t, ports.MsgOutput, frame.Type, "the committed epoch emits a full frame") | |
| 193 | + | select { | |
| 194 | + | case extra := <-frames: | |
| 195 | + | t.Fatalf("obsolete epoch emitted an additional frame: %#v", extra) | |
| 196 | + | default: | |
| 197 | + | } | |
| 125 | 198 | } | |
| 126 | 199 | ||
| 127 | 200 | // Entering copy mode is invalidated at transaction prepare time, while a | |
| @@ -129,21 +202,35 @@ func TestTransactionalResizeEpochLifecycleAndRetryContract(t *testing.T) { | |||
| 129 | 202 | // composition. Channels, rather than a delay, make the publication boundary | |
| 130 | 203 | // deterministic. | |
| 131 | 204 | func TestTransactionalResizeFloatingMouseCopyAndSearchContract(t *testing.T) { | |
| 132 | - | entered, release := make(chan struct{}), make(chan struct{}) | |
| 133 | - | pty := &transactionalResizePTY{onResize: func() { close(entered); <-release }} | |
| 134 | - | d, sess, ac, _ := newManualSessionWithPTYs(t, pty) | |
| 135 | - | tb := sess.activeTab() | |
| 136 | - | p := tb.focusedPane() | |
| 137 | - | old := p.rect | |
| 138 | - | d.enterCopyMode(sess, ac) | |
| 139 | - | done := make(chan struct{}) | |
| 140 | - | go func() { | |
| 141 | - | d.resize(sess, ac, domain.Size{Cols: 100, Rows: 30}) | |
| 142 | - | close(done) | |
| 143 | - | }() | |
| 144 | - | <-entered | |
| 145 | - | require.False(t, ac.overlays.copyActive(), "prepare invalidates copy mode and visual search") | |
| 146 | - | require.Equal(t, old, p.rect, "mouse uses the old committed geometry before publication") | |
| 147 | - | close(release) | |
| 148 | - | <-done | |
| 205 | + | for _, tc := range []struct { | |
| 206 | + | name string | |
| 207 | + | errs []error | |
| 208 | + | }{ | |
| 209 | + | {name: "success"}, | |
| 210 | + | {name: "partial failure", errs: []error{errors.New("resize failed")}}, | |
| 211 | + | } { | |
| 212 | + | t.Run(tc.name, func(t *testing.T) { | |
| 213 | + | entered, release := make(chan struct{}), make(chan struct{}) | |
| 214 | + | pty := &transactionalResizePTY{errs: tc.errs, onResize: func() { close(entered); <-release }} | |
| 215 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, pty) | |
| 216 | + | tb := sess.activeTab() | |
| 217 | + | p := tb.focusedPane() | |
| 218 | + | old := p.rect | |
| 219 | + | d.enterCopyMode(sess, ac) | |
| 220 | + | ac.overlays.copyMu.Lock() | |
| 221 | + | ac.overlays.copySearch = &visualsearch.Model{} | |
| 222 | + | ac.overlays.copyMu.Unlock() | |
| 223 | + | done := make(chan struct{}) | |
| 224 | + | go func() { | |
| 225 | + | d.resize(sess, ac, domain.Size{Cols: 100, Rows: 30}) | |
| 226 | + | close(done) | |
| 227 | + | }() | |
| 228 | + | <-entered | |
| 229 | + | require.False(t, ac.overlays.copyActive(), "prepare invalidates copy mode") | |
| 230 | + | require.False(t, ac.overlays.copySearchActive(), "prepare invalidates visual search") | |
| 231 | + | require.Equal(t, old, p.rect, "mouse uses old committed geometry before publication") | |
| 232 | + | close(release) | |
| 233 | + | <-done | |
| 234 | + | }) | |
| 235 | + | } | |
| 149 | 236 | } | |
| @@ -5,11 +5,14 @@ import ( | |||||
| 5 | "io" | 5 | "io" | ||
| 6 | "sync" | 6 | "sync" | ||
| 7 | "testing" | 7 | "testing" | ||
| 8 | + | "time" | |||
| 8 | 9 | ||||
| 9 | "github.com/stretchr/testify/require" | 10 | "github.com/stretchr/testify/require" | ||
| 10 | 11 | ||||
| 11 | "github.com/bnema/vev/internal/domain" | 12 | "github.com/bnema/vev/internal/domain" | ||
| 13 | + | "github.com/bnema/vev/internal/ports" | |||
| 12 | "github.com/bnema/vev/internal/usecase/layout" | 14 | "github.com/bnema/vev/internal/usecase/layout" | ||
| 15 | + | "github.com/bnema/vev/internal/usecase/visualsearch" | |||
| 13 | ) | 16 | ) | ||
| 14 | 17 | ||||
| 15 | // transactionalResizePTY is deliberately channel-scripted: a test can stop a | 18 | // transactionalResizePTY is deliberately channel-scripted: a test can stop a | ||
| @@ -104,24 +107,94 @@ func TestTransactionalResizePartialFailureCommitsOnlySuccessfulPTYState(t *testi | |||||
| 104 | require.NotEqual(t, oldFailedRect, second.rect, "failed PTY rectangle still follows the committed layout") | 107 | require.NotEqual(t, oldFailedRect, second.rect, "failed PTY rectangle still follows the committed layout") | ||
| 105 | } | 108 | } | ||
| 106 | 109 | ||||
| 110 | + | // S3 acceptance: a failed member is retried only for the newest committed | |||
| 111 | + | // epoch. An intervening request supersedes the old retry, and a successful | |||
| 112 | + | // retry must resize its VT before the forced full-frame emission. | |||
| 113 | + | func TestTransactionalResizeRetryTargetsNewestCommittedEpoch(t *testing.T) { | |||
| 114 | + | ok := &transactionalResizePTY{} | |||
| 115 | + | failed := &transactionalResizePTY{errs: []error{errors.New("first epoch fails"), nil}} | |||
| 116 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, ok) | |||
| 117 | + | tb := sess.activeTab() | |||
| 118 | + | retry := newPane("retry", failed, domain.Size{Cols: 80, Rows: 23}) | |||
| 119 | + | tb.mu.Lock() | |||
| 120 | + | tb.panes[retry.id] = retry | |||
| 121 | + | tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("retry")}}, Focus: "pane-1"} | |||
| 122 | + | tb.mu.Unlock() | |||
| 123 | + | ||||
| 124 | + | d.resize(sess, ac, domain.Size{Cols: 100, Rows: 30}) // failed epoch | |||
| 125 | + | d.resize(sess, ac, domain.Size{Cols: 120, Rows: 34}) // supersedes it | |||
| 126 | + | ||||
| 127 | + | // The failed pane must not be retried at 49x28 from the obsolete epoch. | |||
| 128 | + | // S3 retries 59x32, then commits the parser/screen and requests a reset. | |||
| 129 | + | require.Equal(t, []domain.Size{{Cols: 59, Rows: 32}}, failed.requested()) | |||
| 130 | + | require.Equal(t, domain.Size{Cols: 59, Rows: 32}, domain.Size{Cols: retry.screen.Frame.Width, Rows: retry.screen.Frame.Height}) | |||
| 131 | + | ac.sendMu.Lock() | |||
| 132 | + | pending := ac.resizePaintPending | |||
| 133 | + | ac.sendMu.Unlock() | |||
| 134 | + | require.True(t, pending, "successful retry forces a final full frame") | |||
| 135 | + | } | |||
| 136 | + | ||||
| 107 | // Stale lifecycle callbacks may never advance a transaction owned by a | 137 | // Stale lifecycle callbacks may never advance a transaction owned by a | ||
| 108 | // replacement attachment. The coordinator test clock drives callbacks through | 138 | // replacement attachment. The coordinator test clock drives callbacks through | ||
| 109 | // channels elsewhere in this package; this ownership assertion is deliberately | 139 | // channels elsewhere in this package; this ownership assertion is deliberately | ||
| 110 | // synchronous so it cannot hide a race behind a sleep. | 140 | // synchronous so it cannot hide a race behind a sleep. | ||
| 111 | func TestTransactionalResizeEpochLifecycleAndRetryContract(t *testing.T) { | 141 | func TestTransactionalResizeEpochLifecycleAndRetryContract(t *testing.T) { | ||
| 112 | - | rc := newRenderCoordinator(renderCoordinatorOptions{}) | 142 | + | // Each closure is a callback captured before its lifecycle transition. The |
| 113 | - | old, replacement := &attachedClient{}, &attachedClient{} | 143 | + | // coordinator must reject it, rather than merely coalescing its paint. |
| 114 | - | rc.attach(old) | 144 | + | for _, transition := range []struct { |
| 115 | - | require.Equal(t, uint64(1), rc.recordResizeRequest(domain.Size{Cols: 90, Rows: 24}, old)) | 145 | + | name string |
| 116 | - | require.Equal(t, uint64(2), rc.recordResizeRequest(domain.Size{Cols: 100, Rows: 26}, old)) | 146 | + | run func(*renderCoordinator, *attachedClient, *attachedClient) |
| 117 | - | require.Equal(t, uint64(3), rc.recordResizeRequest(domain.Size{Cols: 120, Rows: 30}, old)) | 147 | + | }{ |
| 118 | - | rc.noteReplace(old, replacement) | 148 | + | {"replace", func(rc *renderCoordinator, old, next *attachedClient) { rc.noteReplace(old, next) }}, |
| 119 | - | rc.attach(replacement) | 149 | + | {"detach", func(rc *renderCoordinator, old, _ *attachedClient) { rc.noteDetach(old) }}, |
| 120 | - | require.Zero(t, rc.recordResizeRequest(domain.Size{Cols: 20, Rows: 5}, old), | 150 | + | {"park", func(rc *renderCoordinator, old, _ *attachedClient) { rc.notePark(old) }}, |
| 121 | - | "obsolete attach/replace/detach/park/resume callbacks cannot publish") | 151 | + | {"resume", func(rc *renderCoordinator, old, next *attachedClient) { rc.notePark(old); rc.attach(next) }}, |
| 122 | - | snap := rc.resizeSnapshot() | 152 | + | } { |
| 123 | - | require.Equal(t, uint64(3), snap.epoch) | 153 | + | t.Run(transition.name, func(t *testing.T) { |
| 124 | - | require.Equal(t, domain.Size{Cols: 120, Rows: 30}, snap.size) | 154 | + | rc := newRenderCoordinator(renderCoordinatorOptions{}) |
| 155 | + | old, next := &attachedClient{}, &attachedClient{} | |||
| 156 | + | rc.attach(old) | |||
| 157 | + | require.Equal(t, uint64(1), rc.recordResizeRequest(domain.Size{Cols: 90, Rows: 24}, old)) | |||
| 158 | + | stale := func() uint64 { return rc.recordResizeRequest(domain.Size{Cols: 20, Rows: 5}, old) } | |||
| 159 | + | transition.run(rc, old, next) | |||
| 160 | + | require.Zero(t, stale(), "a stale %s callback must not publish", transition.name) | |||
| 161 | + | }) | |||
| 162 | + | } | |||
| 163 | + | } | |||
| 164 | + | ||||
| 165 | + | // S3 acceptance: three callbacks can be delivered even after their timers were | |||
| 166 | + | // stopped. Only the newest prepared epoch may apply, commit, and emit one full | |||
| 167 | + | // frame. coordinatorMockClock is a generated MockClock/MockTimer harness; its | |||
| 168 | + | // channels make callback order explicit and do not use wall-clock waits. | |||
| 169 | + | func TestTransactionalResizeObsoleteTimerCallbacksCommitOnlyLatestEpoch(t *testing.T) { | |||
| 170 | + | clock := newCoordinatorMockClock(t, 8) | |||
| 171 | + | pty := &transactionalResizePTY{} | |||
| 172 | + | d, sess, ac, frames := newManualSessionWithPTYs(t, pty) | |||
| 173 | + | d.clock = clock.clock | |||
| 174 | + | ||||
| 175 | + | for _, size := range []domain.Size{{Cols: 90, Rows: 24}, {Cols: 100, Rows: 28}, {Cols: 120, Rows: 32}} { | |||
| 176 | + | d.resize(sess, ac, size) | |||
| 177 | + | } | |||
| 178 | + | // Current PR #71 applies each resize before its debounce callback. S3 must | |||
| 179 | + | // leave this empty until the final callback's prepare/apply/commit epoch. | |||
| 180 | + | require.Empty(t, pty.requested(), "obsolete requests must not resize the PTY") | |||
| 181 | + | ||||
| 182 | + | var timers []*coordinatorMockTimer | |||
| 183 | + | for len(timers) < 3 { | |||
| 184 | + | timers = append(timers, <-clock.timers) | |||
| 185 | + | } | |||
| 186 | + | for _, timer := range timers { | |||
| 187 | + | timer.ch <- time.Time{} | |||
| 188 | + | } | |||
| 189 | + | require.Equal(t, []domain.Size{{Cols: 120, Rows: 30}}, pty.requested()) | |||
| 190 | + | require.Equal(t, uint64(3), sess.renderCoordinator().resizeSnapshot().epoch) | |||
| 191 | + | frame := <-frames | |||
| 192 | + | require.Equal(t, ports.MsgOutput, frame.Type, "the committed epoch emits a full frame") | |||
| 193 | + | select { | |||
| 194 | + | case extra := <-frames: | |||
| 195 | + | t.Fatalf("obsolete epoch emitted an additional frame: %#v", extra) | |||
| 196 | + | default: | |||
| 197 | + | } | |||
| 125 | } | 198 | } | ||
| 126 | 199 | ||||
| 127 | // Entering copy mode is invalidated at transaction prepare time, while a | 200 | // Entering copy mode is invalidated at transaction prepare time, while a | ||
| @@ -129,21 +202,35 @@ func TestTransactionalResizeEpochLifecycleAndRetryContract(t *testing.T) { | |||||
| 129 | // composition. Channels, rather than a delay, make the publication boundary | 202 | // composition. Channels, rather than a delay, make the publication boundary | ||
| 130 | // deterministic. | 203 | // deterministic. | ||
| 131 | func TestTransactionalResizeFloatingMouseCopyAndSearchContract(t *testing.T) { | 204 | func TestTransactionalResizeFloatingMouseCopyAndSearchContract(t *testing.T) { | ||
| 132 | - | entered, release := make(chan struct{}), make(chan struct{}) | 205 | + | for _, tc := range []struct { |
| 133 | - | pty := &transactionalResizePTY{onResize: func() { close(entered); <-release }} | 206 | + | name string |
| 134 | - | d, sess, ac, _ := newManualSessionWithPTYs(t, pty) | 207 | + | errs []error |
| 135 | - | tb := sess.activeTab() | 208 | + | }{ |
| 136 | - | p := tb.focusedPane() | 209 | + | {name: "success"}, |
| 137 | - | old := p.rect | 210 | + | {name: "partial failure", errs: []error{errors.New("resize failed")}}, |
| 138 | - | d.enterCopyMode(sess, ac) | 211 | + | } { |
| 139 | - | done := make(chan struct{}) | 212 | + | t.Run(tc.name, func(t *testing.T) { |
| 140 | - | go func() { | 213 | + | entered, release := make(chan struct{}), make(chan struct{}) |
| 141 | - | d.resize(sess, ac, domain.Size{Cols: 100, Rows: 30}) | 214 | + | pty := &transactionalResizePTY{errs: tc.errs, onResize: func() { close(entered); <-release }} |
| 142 | - | close(done) | 215 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, pty) |
| 143 | - | }() | 216 | + | tb := sess.activeTab() |
| 144 | - | <-entered | 217 | + | p := tb.focusedPane() |
| 145 | - | require.False(t, ac.overlays.copyActive(), "prepare invalidates copy mode and visual search") | 218 | + | old := p.rect |
| 146 | - | require.Equal(t, old, p.rect, "mouse uses the old committed geometry before publication") | 219 | + | d.enterCopyMode(sess, ac) |
| 147 | - | close(release) | 220 | + | ac.overlays.copyMu.Lock() |
| 148 | - | <-done | 221 | + | ac.overlays.copySearch = &visualsearch.Model{} |
| 222 | + | ac.overlays.copyMu.Unlock() | |||
| 223 | + | done := make(chan struct{}) | |||
| 224 | + | go func() { | |||
| 225 | + | d.resize(sess, ac, domain.Size{Cols: 100, Rows: 30}) | |||
| 226 | + | close(done) | |||
| 227 | + | }() | |||
| 228 | + | <-entered | |||
| 229 | + | require.False(t, ac.overlays.copyActive(), "prepare invalidates copy mode") | |||
| 230 | + | require.False(t, ac.overlays.copySearchActive(), "prepare invalidates visual search") | |||
| 231 | + | require.Equal(t, old, p.rect, "mouse uses old committed geometry before publication") | |||
| 232 | + | close(release) | |||
| 233 | + | <-done | |||
| 234 | + | }) | |||
| 235 | + | } | |||
| 149 | } | 236 | } | ||