detached 2824f92

test(daemon): drive deferred paint flush through the MsgAck handler

Review findings on PR #47: exercise the flush end-to-end via runConnLoop instead of calling advanceOutputAck/takeDeferredPaint directly, and make requireNoOutputFrame drain non-output frames for the full window instead of returning on the first frame.
commit
2824f92be0a9c1f29a1736007e57d0acaaedabb5
author
brice · · 2026-07-08 17:26 UTC
parent
7e0e278
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +31 -11
M internal/usecase/daemon/paint_gate_test.go +31 -11
@@ -1,21 +1,27 @@
11 package daemon
22
33 import (
4+ "io"
45 "testing"
56 "time"
67
78 "github.com/bnema/vev/internal/ports"
9+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
810 "github.com/stretchr/testify/require"
911 )
1012
1113 func requireNoOutputFrame(t *testing.T, sends chan ports.Frame) {
1214 t.Helper()
13- select {
14- case f := <-sends:
15- if f.Type == ports.MsgOutput {
16- t.Fatalf("unexpected output frame: %+v", f)
15+ deadline := time.After(50 * time.Millisecond)
16+ for {
17+ select {
18+ case f := <-sends:
19+ if f.Type == ports.MsgOutput {
20+ t.Fatalf("unexpected output frame: %+v", f)
21+ }
22+ case <-deadline:
23+ return
1724 }
18- case <-time.After(50 * time.Millisecond):
1925 }
2026 }
2127
@@ -44,13 +50,27 @@ func TestDeferredPaintFlushesOnceWhenAcksCatchUp(t *testing.T) {
4450 d.paint(sess, ac, false)
4551 requireNoOutputFrame(t, sends)
4652
47- // Acks catch up: the deferred paint becomes flushable and emits exactly one
48- // cumulative frame (the renderer shadow only advanced on send, so the skip
49- // coalesces into this paint).
50- ac.advanceOutputAck(maxUnackedOutputStates)
51- require.True(t, ac.takeDeferredPaint(), "deferred paint must flush once acks catch up")
52- d.paint(sess, ac, false)
53+ // Drive a real MsgAck through the connection loop so the flush is exercised
54+ // end-to-end: UnmarshalAck → advanceOutputAck → takeDeferredPaint → paint.
55+ // One ack frame, then Recv blocks until the test ends.
56+ recvCh := make(chan ports.Frame, 1)
57+ done := make(chan struct{})
58+ t.Cleanup(func() { close(done) })
59+ tr, ok := ac.transport().(*portsmocks.MockTransport)
60+ require.True(t, ok, "manual session transport must be the capturing mock")
61+ tr.EXPECT().Recv().RunAndReturn(func() (ports.Frame, error) {
62+ select {
63+ case f := <-recvCh:
64+ return f, nil
65+ case <-done:
66+ return ports.Frame{}, io.EOF
67+ }
68+ }).Maybe()
69+ recvCh <- ports.Frame{Type: ports.MsgAck, Payload: ports.MarshalAck(ports.Ack{AckedStateNum: maxUnackedOutputStates})}
70+ go d.runConnLoop(ac)
5371
72+ // The cumulative flush paint must be triggered by the MsgAck handler itself
73+ // (the renderer shadow only advanced on send, so the skip coalesces here).
5474 awaitFrame(t, sends, ports.MsgOutput)
5575 requireNoOutputFrame(t, sends)
5676 ac.sendMu.Lock()
@@ -1,21 +1,27 @@
1 package daemon1 package daemon
2 2
3 import (3 import (
4+ "io"
4 "testing"5 "testing"
5 "time"6 "time"
6 7
7 "github.com/bnema/vev/internal/ports"8 "github.com/bnema/vev/internal/ports"
9+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
8 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
9 )11 )
10 12
11 func requireNoOutputFrame(t *testing.T, sends chan ports.Frame) {13 func requireNoOutputFrame(t *testing.T, sends chan ports.Frame) {
12 t.Helper()14 t.Helper()
13- select {15+ deadline := time.After(50 * time.Millisecond)
14- case f := <-sends:16+ for {
15- if f.Type == ports.MsgOutput {17+ select {
16- t.Fatalf("unexpected output frame: %+v", f)18+ case f := <-sends:
19+ if f.Type == ports.MsgOutput {
20+ t.Fatalf("unexpected output frame: %+v", f)
21+ }
22+ case <-deadline:
23+ return
17 }24 }
18- case <-time.After(50 * time.Millisecond):
19 }25 }
20 }26 }
21 27
@@ -44,13 +50,27 @@ func TestDeferredPaintFlushesOnceWhenAcksCatchUp(t *testing.T) {
44 d.paint(sess, ac, false)50 d.paint(sess, ac, false)
45 requireNoOutputFrame(t, sends)51 requireNoOutputFrame(t, sends)
46 52
47- // Acks catch up: the deferred paint becomes flushable and emits exactly one53+ // Drive a real MsgAck through the connection loop so the flush is exercised
48- // cumulative frame (the renderer shadow only advanced on send, so the skip54+ // end-to-end: UnmarshalAck → advanceOutputAck → takeDeferredPaint → paint.
49- // coalesces into this paint).55+ // One ack frame, then Recv blocks until the test ends.
50- ac.advanceOutputAck(maxUnackedOutputStates)56+ recvCh := make(chan ports.Frame, 1)
51- require.True(t, ac.takeDeferredPaint(), "deferred paint must flush once acks catch up")57+ done := make(chan struct{})
52- d.paint(sess, ac, false)58+ t.Cleanup(func() { close(done) })
59+ tr, ok := ac.transport().(*portsmocks.MockTransport)
60+ require.True(t, ok, "manual session transport must be the capturing mock")
61+ tr.EXPECT().Recv().RunAndReturn(func() (ports.Frame, error) {
62+ select {
63+ case f := <-recvCh:
64+ return f, nil
65+ case <-done:
66+ return ports.Frame{}, io.EOF
67+ }
68+ }).Maybe()
69+ recvCh <- ports.Frame{Type: ports.MsgAck, Payload: ports.MarshalAck(ports.Ack{AckedStateNum: maxUnackedOutputStates})}
70+ go d.runConnLoop(ac)
53 71
72+ // The cumulative flush paint must be triggered by the MsgAck handler itself
73+ // (the renderer shadow only advanced on send, so the skip coalesces here).
54 awaitFrame(t, sends, ports.MsgOutput)74 awaitFrame(t, sends, ports.MsgOutput)
55 requireNoOutputFrame(t, sends)75 requireNoOutputFrame(t, sends)
56 ac.sendMu.Lock()76 ac.sendMu.Lock()

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