test(copy): make lifecycle checks deterministic
- commit
- 07e48e3d55b1b31b53c3c2442b835cac0d1b1a25
- author
- brice · · 2026-07-15 10:19 UTC
- committer
- brice · · 2026-07-15 10:19 UTC
- parent
- 1f016aa
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +78 -28
M internal/usecase/daemon/config_test.go +41 -11
| @@ -51,18 +51,48 @@ func TestApplyConfigHotReloadSwapsBindingsAndCodes(t *testing.T) { | |||
| 51 | 51 | } | |
| 52 | 52 | ||
| 53 | 53 | func TestApplyConfigPublishesCopyConfig(t *testing.T) { | |
| 54 | - | d := newTestDaemon(t, nil, stubClock{}) | |
| 55 | - | require.Equal(t, domain.CopyConfig{WordSeparators: domain.DefaultWordSeparators}, d.currentCopyConfig()) | |
| 56 | - | ||
| 57 | - | configured := domain.Defaults() | |
| 58 | - | configured.Copy = domain.CopyConfig{WordSeparators: "/:"} | |
| 59 | - | d.ApplyConfig(configured) | |
| 60 | - | require.Equal(t, domain.CopyConfig{WordSeparators: "/:"}, d.currentCopyConfig()) | |
| 54 | + | tests := []struct { | |
| 55 | + | name string | |
| 56 | + | apply func(*Daemon) | |
| 57 | + | want domain.CopyConfig | |
| 58 | + | }{ | |
| 59 | + | { | |
| 60 | + | name: "default", | |
| 61 | + | want: domain.CopyConfig{WordSeparators: domain.DefaultWordSeparators}, | |
| 62 | + | }, | |
| 63 | + | { | |
| 64 | + | name: "custom", | |
| 65 | + | apply: func(d *Daemon) { | |
| 66 | + | cfg := domain.Defaults() | |
| 67 | + | cfg.Copy = domain.CopyConfig{WordSeparators: "/:"} | |
| 68 | + | d.ApplyConfig(cfg) | |
| 69 | + | }, | |
| 70 | + | want: domain.CopyConfig{WordSeparators: "/:"}, | |
| 71 | + | }, | |
| 72 | + | { | |
| 73 | + | name: "reload empty", | |
| 74 | + | apply: func(d *Daemon) { | |
| 75 | + | configured := domain.Defaults() | |
| 76 | + | configured.Copy = domain.CopyConfig{WordSeparators: "/:"} | |
| 77 | + | d.ApplyConfig(configured) | |
| 78 | + | ||
| 79 | + | reloaded := domain.Defaults() | |
| 80 | + | reloaded.Copy = domain.CopyConfig{} | |
| 81 | + | d.ApplyConfig(reloaded) | |
| 82 | + | }, | |
| 83 | + | want: domain.CopyConfig{}, | |
| 84 | + | }, | |
| 85 | + | } | |
| 61 | 86 | ||
| 62 | - | reloaded := domain.Defaults() | |
| 63 | - | reloaded.Copy = domain.CopyConfig{} | |
| 64 | - | d.ApplyConfig(reloaded) | |
| 65 | - | require.Equal(t, domain.CopyConfig{}, d.currentCopyConfig()) | |
| 87 | + | for _, tt := range tests { | |
| 88 | + | t.Run(tt.name, func(t *testing.T) { | |
| 89 | + | d := newTestDaemon(t, nil, stubClock{}) | |
| 90 | + | if tt.apply != nil { | |
| 91 | + | tt.apply(d) | |
| 92 | + | } | |
| 93 | + | require.Equal(t, tt.want, d.currentCopyConfig()) | |
| 94 | + | }) | |
| 95 | + | } | |
| 66 | 96 | } | |
| 67 | 97 | ||
| 68 | 98 | func TestApplyConfigPublishesImmutablePaletteSnapshot(t *testing.T) { | |
| @@ -51,18 +51,48 @@ func TestApplyConfigHotReloadSwapsBindingsAndCodes(t *testing.T) { | |||||
| 51 | } | 51 | } | ||
| 52 | 52 | ||||
| 53 | func TestApplyConfigPublishesCopyConfig(t *testing.T) { | 53 | func TestApplyConfigPublishesCopyConfig(t *testing.T) { | ||
| 54 | - | d := newTestDaemon(t, nil, stubClock{}) | 54 | + | tests := []struct { |
| 55 | - | require.Equal(t, domain.CopyConfig{WordSeparators: domain.DefaultWordSeparators}, d.currentCopyConfig()) | 55 | + | name string |
| 56 | - | 56 | + | apply func(*Daemon) | |
| 57 | - | configured := domain.Defaults() | 57 | + | want domain.CopyConfig |
| 58 | - | configured.Copy = domain.CopyConfig{WordSeparators: "/:"} | 58 | + | }{ |
| 59 | - | d.ApplyConfig(configured) | 59 | + | { |
| 60 | - | require.Equal(t, domain.CopyConfig{WordSeparators: "/:"}, d.currentCopyConfig()) | 60 | + | name: "default", |
| 61 | + | want: domain.CopyConfig{WordSeparators: domain.DefaultWordSeparators}, | |||
| 62 | + | }, | |||
| 63 | + | { | |||
| 64 | + | name: "custom", | |||
| 65 | + | apply: func(d *Daemon) { | |||
| 66 | + | cfg := domain.Defaults() | |||
| 67 | + | cfg.Copy = domain.CopyConfig{WordSeparators: "/:"} | |||
| 68 | + | d.ApplyConfig(cfg) | |||
| 69 | + | }, | |||
| 70 | + | want: domain.CopyConfig{WordSeparators: "/:"}, | |||
| 71 | + | }, | |||
| 72 | + | { | |||
| 73 | + | name: "reload empty", | |||
| 74 | + | apply: func(d *Daemon) { | |||
| 75 | + | configured := domain.Defaults() | |||
| 76 | + | configured.Copy = domain.CopyConfig{WordSeparators: "/:"} | |||
| 77 | + | d.ApplyConfig(configured) | |||
| 78 | + | ||||
| 79 | + | reloaded := domain.Defaults() | |||
| 80 | + | reloaded.Copy = domain.CopyConfig{} | |||
| 81 | + | d.ApplyConfig(reloaded) | |||
| 82 | + | }, | |||
| 83 | + | want: domain.CopyConfig{}, | |||
| 84 | + | }, | |||
| 85 | + | } | |||
| 61 | 86 | ||||
| 62 | - | reloaded := domain.Defaults() | 87 | + | for _, tt := range tests { |
| 63 | - | reloaded.Copy = domain.CopyConfig{} | 88 | + | t.Run(tt.name, func(t *testing.T) { |
| 64 | - | d.ApplyConfig(reloaded) | 89 | + | d := newTestDaemon(t, nil, stubClock{}) |
| 65 | - | require.Equal(t, domain.CopyConfig{}, d.currentCopyConfig()) | 90 | + | if tt.apply != nil { |
| 91 | + | tt.apply(d) | |||
| 92 | + | } | |||
| 93 | + | require.Equal(t, tt.want, d.currentCopyConfig()) | |||
| 94 | + | }) | |||
| 95 | + | } | |||
| 66 | } | 96 | } | ||
| 67 | 97 | ||||
| 68 | func TestApplyConfigPublishesImmutablePaletteSnapshot(t *testing.T) { | 98 | func TestApplyConfigPublishesImmutablePaletteSnapshot(t *testing.T) { | ||
M internal/usecase/daemon/copymode_lifecycle_test.go +37 -17
| @@ -5,7 +5,6 @@ import ( | |||
| 5 | 5 | "testing" | |
| 6 | 6 | "time" | |
| 7 | 7 | ||
| 8 | - | "github.com/stretchr/testify/assert" | |
| 9 | 8 | "github.com/stretchr/testify/require" | |
| 10 | 9 | ||
| 11 | 10 | "github.com/bnema/vev/internal/domain" | |
| @@ -117,31 +116,52 @@ func TestCopyModeLifecycleDoesNotRenderCandidateBeforeValidation(t *testing.T) { | |||
| 117 | 116 | document := scopy.NewDocument(scopy.NewSnapshot(p.history, p.screen.Frame), domain.DefaultWordSeparators) | |
| 118 | 117 | p.mu.Unlock() | |
| 119 | 118 | ||
| 120 | - | // Hold session validation after publication and make the captured target | |
| 121 | - | // stale. A render snapshot taken in this window must not expose the | |
| 122 | - | // candidate document. | |
| 123 | - | sess.mu.Lock() | |
| 124 | - | sess.active = 1 | |
| 119 | + | // Pause after candidate publication, then make the captured target stale. | |
| 120 | + | // A render snapshot while paused must not expose the candidate document. | |
| 121 | + | reached := make(chan struct{}) | |
| 122 | + | resume := make(chan struct{}) | |
| 123 | + | done := make(chan struct{}) | |
| 124 | + | d.beforeCopyModeRevalidate = func() { | |
| 125 | + | close(reached) | |
| 126 | + | <-resume | |
| 127 | + | } | |
| 128 | + | resumed := false | |
| 129 | + | defer func() { | |
| 130 | + | if !resumed { | |
| 131 | + | close(resume) | |
| 132 | + | } | |
| 133 | + | select { | |
| 134 | + | case <-done: | |
| 135 | + | d.beforeCopyModeRevalidate = nil | |
| 136 | + | case <-time.After(time.Second): | |
| 137 | + | t.Error("copy mode publication did not finish after revalidation gate was released") | |
| 138 | + | } | |
| 139 | + | }() | |
| 140 | + | ||
| 125 | 141 | result := make(chan bool, 1) | |
| 126 | 142 | go func() { | |
| 143 | + | defer close(done) | |
| 127 | 144 | result <- d.publishCopyMode(sess, ac, tb, p, document, nil, nil) | |
| 128 | 145 | }() | |
| 129 | - | published := assert.Eventually(t, func() bool { | |
| 130 | - | ac.overlays.copyMu.Lock() | |
| 131 | - | defer ac.overlays.copyMu.Unlock() | |
| 132 | - | return ac.overlays.copyPane == p && ac.overlays.copyDocument != nil | |
| 133 | - | }, time.Second, time.Millisecond) | |
| 134 | - | if !published { | |
| 135 | - | sess.mu.Unlock() | |
| 136 | - | <-result | |
| 137 | - | return | |
| 146 | + | select { | |
| 147 | + | case <-reached: | |
| 148 | + | case <-time.After(time.Second): | |
| 149 | + | t.Fatal("copy mode publication did not reach revalidation gate") | |
| 138 | 150 | } | |
| 151 | + | ||
| 139 | 152 | renderSnapshot := ac.overlays.SnapshotForRender() | |
| 140 | 153 | candidateWasRenderable := renderSnapshot.copyActive | |
| 141 | 154 | renderSnapshot.Unlock() | |
| 142 | - | sess.mu.Unlock() | |
| 155 | + | require.True(t, sess.switchTab(1)) | |
| 156 | + | close(resume) | |
| 157 | + | resumed = true | |
| 143 | 158 | ||
| 144 | - | require.False(t, <-result) | |
| 159 | + | select { | |
| 160 | + | case published := <-result: | |
| 161 | + | require.False(t, published) | |
| 162 | + | case <-time.After(time.Second): | |
| 163 | + | t.Fatal("copy mode publication did not finish after revalidation gate was released") | |
| 164 | + | } | |
| 145 | 165 | require.False(t, candidateWasRenderable) | |
| 146 | 166 | require.False(t, ac.overlays.copyActive()) | |
| 147 | 167 | require.Nil(t, copyTargetPane(ac.overlays)) | |
| @@ -5,7 +5,6 @@ import ( | |||||
| 5 | "testing" | 5 | "testing" | ||
| 6 | "time" | 6 | "time" | ||
| 7 | 7 | ||||
| 8 | - | "github.com/stretchr/testify/assert" | |||
| 9 | "github.com/stretchr/testify/require" | 8 | "github.com/stretchr/testify/require" | ||
| 10 | 9 | ||||
| 11 | "github.com/bnema/vev/internal/domain" | 10 | "github.com/bnema/vev/internal/domain" | ||
| @@ -117,31 +116,52 @@ func TestCopyModeLifecycleDoesNotRenderCandidateBeforeValidation(t *testing.T) { | |||||
| 117 | document := scopy.NewDocument(scopy.NewSnapshot(p.history, p.screen.Frame), domain.DefaultWordSeparators) | 116 | document := scopy.NewDocument(scopy.NewSnapshot(p.history, p.screen.Frame), domain.DefaultWordSeparators) | ||
| 118 | p.mu.Unlock() | 117 | p.mu.Unlock() | ||
| 119 | 118 | ||||
| 120 | - | // Hold session validation after publication and make the captured target | 119 | + | // Pause after candidate publication, then make the captured target stale. |
| 121 | - | // stale. A render snapshot taken in this window must not expose the | 120 | + | // A render snapshot while paused must not expose the candidate document. |
| 122 | - | // candidate document. | 121 | + | reached := make(chan struct{}) |
| 123 | - | sess.mu.Lock() | 122 | + | resume := make(chan struct{}) |
| 124 | - | sess.active = 1 | 123 | + | done := make(chan struct{}) |
| 124 | + | d.beforeCopyModeRevalidate = func() { | |||
| 125 | + | close(reached) | |||
| 126 | + | <-resume | |||
| 127 | + | } | |||
| 128 | + | resumed := false | |||
| 129 | + | defer func() { | |||
| 130 | + | if !resumed { | |||
| 131 | + | close(resume) | |||
| 132 | + | } | |||
| 133 | + | select { | |||
| 134 | + | case <-done: | |||
| 135 | + | d.beforeCopyModeRevalidate = nil | |||
| 136 | + | case <-time.After(time.Second): | |||
| 137 | + | t.Error("copy mode publication did not finish after revalidation gate was released") | |||
| 138 | + | } | |||
| 139 | + | }() | |||
| 140 | + | ||||
| 125 | result := make(chan bool, 1) | 141 | result := make(chan bool, 1) | ||
| 126 | go func() { | 142 | go func() { | ||
| 143 | + | defer close(done) | |||
| 127 | result <- d.publishCopyMode(sess, ac, tb, p, document, nil, nil) | 144 | result <- d.publishCopyMode(sess, ac, tb, p, document, nil, nil) | ||
| 128 | }() | 145 | }() | ||
| 129 | - | published := assert.Eventually(t, func() bool { | 146 | + | select { |
| 130 | - | ac.overlays.copyMu.Lock() | 147 | + | case <-reached: |
| 131 | - | defer ac.overlays.copyMu.Unlock() | 148 | + | case <-time.After(time.Second): |
| 132 | - | return ac.overlays.copyPane == p && ac.overlays.copyDocument != nil | 149 | + | t.Fatal("copy mode publication did not reach revalidation gate") |
| 133 | - | }, time.Second, time.Millisecond) | |||
| 134 | - | if !published { | |||
| 135 | - | sess.mu.Unlock() | |||
| 136 | - | <-result | |||
| 137 | - | return | |||
| 138 | } | 150 | } | ||
| 151 | + | ||||
| 139 | renderSnapshot := ac.overlays.SnapshotForRender() | 152 | renderSnapshot := ac.overlays.SnapshotForRender() | ||
| 140 | candidateWasRenderable := renderSnapshot.copyActive | 153 | candidateWasRenderable := renderSnapshot.copyActive | ||
| 141 | renderSnapshot.Unlock() | 154 | renderSnapshot.Unlock() | ||
| 142 | - | sess.mu.Unlock() | 155 | + | require.True(t, sess.switchTab(1)) |
| 156 | + | close(resume) | |||
| 157 | + | resumed = true | |||
| 143 | 158 | ||||
| 144 | - | require.False(t, <-result) | 159 | + | select { |
| 160 | + | case published := <-result: | |||
| 161 | + | require.False(t, published) | |||
| 162 | + | case <-time.After(time.Second): | |||
| 163 | + | t.Fatal("copy mode publication did not finish after revalidation gate was released") | |||
| 164 | + | } | |||
| 145 | require.False(t, candidateWasRenderable) | 165 | require.False(t, candidateWasRenderable) | ||
| 146 | require.False(t, ac.overlays.copyActive()) | 166 | require.False(t, ac.overlays.copyActive()) | ||
| 147 | require.Nil(t, copyTargetPane(ac.overlays)) | 167 | require.Nil(t, copyTargetPane(ac.overlays)) | ||