handshake_deadline_test.go RawHistoryFolder

1package daemon
2
3import (
4	"context"
5	"testing"
6	"time"
7
8	"github.com/stretchr/testify/require"
9
10	"github.com/bnema/vev/internal/domain"
11	"github.com/bnema/vev/internal/protocol"
12)
13
14// acceptedDeadlineConn is a typed daemon connection that yields one scripted
15// first message, then blocks on its embedded transport, while exposing the
16// optional accepted absolute handshake deadline seam. Ordinary test transports
17// keep the fresh-budget behavior because they do not implement the seam.
18type acceptedDeadlineConn struct {
19	*handshakeBlockingTransport
20	deadline time.Time
21	first    protocol.ClientMessage
22}
23
24func (c *acceptedDeadlineConn) ReceiveClient() (protocol.ClientMessage, error) {
25	if c.first != nil {
26		message := c.first
27		c.first = nil
28		return message, nil
29	}
30	return c.handshakeBlockingTransport.ReceiveClient()
31}
32
33func (c *acceptedDeadlineConn) HandshakeDeadline() time.Time { return c.deadline }
34
35func TestAcceptedHandshakeDeadlineSeamResolution(t *testing.T) {
36	plain := newHandshakeBlockingTransport(false)
37	require.True(t, acceptedHandshakeDeadline(plain).IsZero(),
38		"a connection without the optional seam must keep the fresh 15s budget")
39
40	fixed := time.Unix(1_700_000_000, 0)
41	seamed := &acceptedDeadlineConn{
42		handshakeBlockingTransport: newHandshakeBlockingTransport(false),
43		deadline:                   fixed,
44	}
45	require.Equal(t, fixed, acceptedHandshakeDeadline(seamed))
46}
47
48func TestNewHandshakeContextConsumesAcceptedQueueTime(t *testing.T) {
49	clock := &signalClock{timers: make(chan *signalTimer, 2)}
50	d := newTestDaemon(t, nil, clock)
51
52	// signalClock.Now is the zero time, so a deadline 4s after it leaves only
53	// that much of the accepted budget instead of restarting 15s.
54	ctx, timedOut, finish := d.newHandshakeContext(context.Background(), time.Time{}.Add(4*time.Second))
55	defer finish()
56
57	timer := <-clock.timers
58	require.Equal(t, 4*time.Second, timer.duration, "queue time must be consumed from the accepted budget")
59	require.NoError(t, ctx.Err())
60	select {
61	case <-timedOut:
62		t.Fatal("handshake timed out before the accepted budget elapsed")
63	default:
64	}
65
66	timer.ch <- time.Time{}
67	awaitTestCompletion(t, ctx.Done(), "accepted budget did not cancel the handshake context")
68	awaitTestCompletion(t, timedOut, "accepted budget did not publish the timeout signal")
69}
70
71func TestNewHandshakeContextExpiredAcceptedDeadlineFailsPromptly(t *testing.T) {
72	clock := &signalClock{timers: make(chan *signalTimer, 1)}
73	d := newTestDaemon(t, nil, clock)
74
75	ctx, timedOut, finish := d.newHandshakeContext(context.Background(), time.Time{}.Add(-time.Second))
76	require.ErrorIs(t, ctx.Err(), context.Canceled)
77	select {
78	case <-timedOut:
79	default:
80		t.Fatal("an already-expired accepted deadline did not publish the timeout signal")
81	}
82	select {
83	case timer := <-clock.timers:
84		t.Fatalf("an already-expired accepted deadline started a new %s budget", timer.duration)
85	default:
86	}
87	// finish stays safe to call repeatedly on the immediate-expiry path.
88	finish()
89	finish()
90}
91
92// TestAcceptedHandshakeDeadlineSpansFirstFrameThroughWelcome proves handleConn
93// adopts the accepted absolute deadline as the single budget covering the first
94// ReceiveClient and the blocked Welcome send, so no second 15s budget is
95// started and the accepted budget alone ends the handshake.
96func TestAcceptedHandshakeDeadlineSpansFirstFrameThroughWelcome(t *testing.T) {
97	pty, release := newBlockingPTY(t)
98	defer release()
99	clock := &signalClock{timers: make(chan *signalTimer, 4)}
100	d := newTestDaemon(t, newFactory(t, pty), clock)
101
102	conn := &acceptedDeadlineConn{
103		handshakeBlockingTransport: newHandshakeBlockingTransport(true),
104		deadline:                   time.Time{}.Add(9 * time.Second),
105		first: protocol.Hello{
106			Version: protocol.Version,
107			Intent:  protocol.IntentNew,
108			Name:    "accepted-deadline",
109			Size:    domain.Size{Cols: 80, Rows: 24},
110			TermEnv: "xterm-256color",
111		},
112	}
113	done := make(chan struct{})
114	go func() {
115		d.handleConn(conn)
116		close(done)
117	}()
118
119	timer := <-clock.timers
120	require.Equal(t, 9*time.Second, timer.duration,
121		"handleConn must adopt the accepted deadline rather than start 15s")
122	<-conn.welcome
123	timer.ch <- time.Time{}
124	awaitTestCompletion(t, done, "accepted handshake did not stop after its budget elapsed")
125	requireClosedHandshakeTransport(t, conn.handshakeBlockingTransport)
126	d.mu.Lock()
127	require.Empty(t, d.sessions, "a budget-expired newly-created handshake must not leave an empty session")
128	require.Empty(t, d.resume.parked)
129	d.mu.Unlock()
130}
131
132// TestExpiredAcceptedHandshakeDeadlineClosesConn proves handleConn fails an
133// already-expired accepted connection promptly: it closes the transport,
134// returns, and never starts a fresh 15s receive budget.
135func TestExpiredAcceptedHandshakeDeadlineClosesConn(t *testing.T) {
136	clock := &signalClock{timers: make(chan *signalTimer, 1)}
137	d := newTestDaemon(t, nil, clock)
138
139	conn := &acceptedDeadlineConn{
140		handshakeBlockingTransport: newHandshakeBlockingTransport(false),
141		deadline:                   time.Time{}.Add(-time.Second),
142	}
143	done := make(chan struct{})
144	go func() {
145		d.handleConn(conn)
146		close(done)
147	}()
148
149	awaitTestCompletion(t, done, "an expired accepted deadline did not fail promptly")
150	requireClosedHandshakeTransport(t, conn.handshakeBlockingTransport)
151	select {
152	case timer := <-clock.timers:
153		t.Fatalf("an expired accepted deadline started a fresh %s budget", timer.duration)
154	default:
155	}
156	d.mu.Lock()
157	require.Empty(t, d.sessions)
158	require.Empty(t, d.resume.parked)
159	d.mu.Unlock()
160}
161
162// TestHandleHelloAdoptsExpiredAcceptedDeadline proves the direct handleHello
163// entry point honors the same optional seam as handleConn.
164func TestHandleHelloAdoptsExpiredAcceptedDeadline(t *testing.T) {
165	clock := &signalClock{timers: make(chan *signalTimer, 1)}
166	d := newTestDaemon(t, nil, clock)
167
168	conn := &acceptedDeadlineConn{
169		handshakeBlockingTransport: newHandshakeBlockingTransport(false),
170		deadline:                   time.Time{}.Add(-time.Second),
171	}
172	done := make(chan struct{})
173	go func() {
174		d.handleHello(conn, protocol.Hello{
175			Version: protocol.Version,
176			Intent:  protocol.IntentNew,
177			Name:    "direct-expired",
178			Size:    domain.Size{Cols: 80, Rows: 24},
179			TermEnv: "xterm-256color",
180		})
181		close(done)
182	}()
183
184	awaitTestCompletion(t, done, "handleHello did not fail on an expired accepted deadline")
185	requireClosedHandshakeTransport(t, conn.handshakeBlockingTransport)
186	select {
187	case timer := <-clock.timers:
188		t.Fatalf("handleHello started a fresh %s budget for an expired accepted deadline", timer.duration)
189	default:
190	}
191	d.mu.Lock()
192	require.Empty(t, d.sessions)
193	require.Empty(t, d.resume.parked)
194	d.mu.Unlock()
195}

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