v0.7.0 4c16dd8

quic_test.go RawHistoryFolder

1package quic
2
3import (
4	"context"
5	"io"
6	"net"
7	"strconv"
8	"sync"
9	"testing"
10	"time"
11
12	"github.com/bnema/vev/internal/ports"
13	"github.com/bnema/vev/internal/protocol/wire"
14	quicgo "github.com/quic-go/quic-go"
15	"github.com/stretchr/testify/require"
16)
17
18func TestQUICEnvelopeRoundTrip(t *testing.T) {
19	cert, fingerprint, err := GenerateEphemeralCert()
20	require.NoError(t, err)
21	require.Len(t, fingerprint, 32)
22
23	listener, err := ListenConfig("127.0.0.1:0", cert, Config{}, 4)
24	require.NoError(t, err)
25	defer func() { _ = listener.Close() }()
26
27	accepted := make(chan wire.Transport, 1)
28	go func() {
29		transport, err := listener.Accept()
30		if err != nil {
31			return
32		}
33		accepted <- transport
34	}()
35
36	dialer := DialConfig(listener.Addr(), "vev-bootstrap", fingerprint, Config{}, 10*time.Second)
37	client, err := dialer.Dial(context.Background())
38	require.NoError(t, err)
39	defer func() { _ = client.Close() }()
40
41	select {
42	case server := <-accepted:
43		defer func() { _ = server.Close() }()
44		payload := []byte("quic envelope stays exact")
45		require.NoError(t, client.Send(wire.Envelope{Payload: payload}))
46		got, err := server.Recv()
47		require.NoError(t, err)
48		require.Equal(t, payload, got.Payload)
49		require.NoError(t, server.Send(wire.Envelope{Payload: payload}))
50		echo, err := client.Recv()
51		require.NoError(t, err)
52		require.Equal(t, payload, echo.Payload)
53	case <-time.After(10 * time.Second):
54		t.Fatal("listener did not accept")
55	}
56}
57
58func TestQUICWrongFingerprintFails(t *testing.T) {
59	cert, _, err := GenerateEphemeralCert()
60	require.NoError(t, err)
61	listener, err := ListenConfig("127.0.0.1:0", cert, Config{}, 4)
62	require.NoError(t, err)
63	defer func() { _ = listener.Close() }()
64	accepted := make(chan struct{})
65	go func() {
66		defer close(accepted)
67		_, _ = listener.Accept()
68	}()
69
70	wrong := make([]byte, 32)
71	dialer := DialConfig(listener.Addr(), "vev-bootstrap", wrong, Config{}, 5*time.Second)
72	_, err = dialer.Dial(context.Background())
73	require.Error(t, err)
74}
75
76func TestQUICConfigUsesTunnelSafeInitialPacketSize(t *testing.T) {
77	for _, config := range []*quicgo.Config{clientQUICConfig(Config{}), serverQUICConfig(Config{})} {
78		require.Equal(t, uint16(1200), config.InitialPacketSize)
79	}
80}
81
82func TestQUICConfigValidation(t *testing.T) {
83	cert, _, err := GenerateEphemeralCert()
84	require.NoError(t, err)
85	_, err = ListenConfig("", cert, Config{KeepAlivePeriod: -time.Second}, 4)
86	require.Error(t, err)
87	_, err = ListenConfig("", cert, Config{MaxIncomingStreams: -1}, 4)
88	require.Error(t, err)
89	_, err = ListenConfig("", cert, Config{}, -1)
90	require.Error(t, err)
91	// Empty addr selects loopback, never the wildcard.
92	listener, err := ListenConfig("", cert, Config{}, 4)
93	require.NoError(t, err)
94	defer func() { _ = listener.Close() }()
95	require.Contains(t, listener.Addr(), "127.0.0.1")
96}
97
98// TestQUICOrderlyCloseDeliversFinalEnvelope is the regression guard for an
99// orderly close discarding the envelope written immediately before it: the
100// send side must be FINed, not RESET_STREAMed, so the peer still receives the
101// final envelope before observing EOF.
102func TestQUICOrderlyCloseDeliversFinalEnvelope(t *testing.T) {
103	server, client := dialTestPairWithConfig(t, Config{})
104	payload := []byte("final envelope survives an orderly close")
105	require.NoError(t, server.Send(wire.Envelope{Payload: payload}))
106	require.NoError(t, server.Close())
107
108	got, err := client.Recv()
109	require.NoError(t, err, "an orderly close must not discard the final envelope")
110	require.Equal(t, payload, got.Payload)
111	_, err = client.Recv()
112	require.ErrorIs(t, err, io.EOF)
113}
114
115func TestQUICResolveAddrsExpandsLiteralsAndHonorsContext(t *testing.T) {
116	addrs, err := resolveQUICAddrs(context.Background(), "127.0.0.1:4433")
117	require.NoError(t, err)
118	require.Len(t, addrs, 1)
119	require.Equal(t, 4433, addrs[0].Port)
120	require.True(t, addrs[0].IP.Equal(net.ParseIP("127.0.0.1")))
121
122	addrs, err = resolveQUICAddrs(context.Background(), "[::1]:4433")
123	require.NoError(t, err)
124	require.Len(t, addrs, 1)
125	require.True(t, addrs[0].IP.Equal(net.ParseIP("::1")))
126
127	_, err = resolveQUICAddrs(context.Background(), "127.0.0.1:notaport")
128	require.Error(t, err)
129
130	canceled, cancel := context.WithCancel(context.Background())
131	cancel()
132	_, err = resolveQUICAddrs(canceled, "definitely-not-a-real-host.invalid:4433")
133	require.Error(t, err, "resolution must be bounded by the dial context")
134}
135
136// TestQUICDialFallsBackAcrossResolvedAddresses proves every resolved address
137// is attempted under the one shared dial deadline: a black-holed first
138// candidate must not prevent the reachable second from being dialed.
139func TestQUICDialFallsBackAcrossResolvedAddresses(t *testing.T) {
140	cert, fingerprint, err := GenerateEphemeralCert()
141	require.NoError(t, err)
142	listener, err := ListenConfig("127.0.0.1:0", cert, Config{}, 4)
143	require.NoError(t, err)
144	defer func() { _ = listener.Close() }()
145	accepted := make(chan wire.Transport, 1)
146	go func() {
147		if transport, err := listener.Accept(); err == nil {
148			accepted <- transport
149		}
150	}()
151	_, portText, err := net.SplitHostPort(listener.Addr())
152	require.NoError(t, err)
153	port, err := strconv.Atoi(portText)
154	require.NoError(t, err)
155
156	dialer := DialConfig("placeholder:1", "vev-bootstrap", fingerprint, Config{}, 4*time.Second)
157	dialer.resolve = func(context.Context, string) ([]*net.UDPAddr, error) {
158		return []*net.UDPAddr{
159			{IP: net.ParseIP("127.0.0.1"), Port: 1}, // nothing listens here
160			{IP: net.ParseIP("127.0.0.1"), Port: port},
161		}, nil
162	}
163	client, err := dialer.Dial(context.Background())
164	require.NoError(t, err, "the dialer must fail over to a later resolved address")
165	defer func() { _ = client.Close() }()
166
167	select {
168	case server := <-accepted:
169		defer func() { _ = server.Close() }()
170		payload := []byte("second address")
171		require.NoError(t, client.Send(wire.Envelope{Payload: payload}))
172		got, err := server.Recv()
173		require.NoError(t, err)
174		require.Equal(t, payload, got.Payload)
175	case <-time.After(10 * time.Second):
176		t.Fatal("fallback address was not dialed")
177	}
178}
179
180type markRecorder struct {
181	mu    sync.Mutex
182	marks []ports.RuntimeMark
183}
184
185func (r *markRecorder) ObserveRuntime(mark ports.RuntimeMark) {
186	r.mu.Lock()
187	r.marks = append(r.marks, mark)
188	r.mu.Unlock()
189}
190
191func (r *markRecorder) Flush() {}
192func (r *markRecorder) Close() {}
193
194// TestQUICDialObserverEmitsAdapterMarks proves the observer threaded through
195// DialConfig reaches the dialed transport and observes its raw carriage.
196func TestQUICDialObserverEmitsAdapterMarks(t *testing.T) {
197	cert, fingerprint, err := GenerateEphemeralCert()
198	require.NoError(t, err)
199	listener, err := ListenConfig("127.0.0.1:0", cert, Config{}, 4)
200	require.NoError(t, err)
201	defer func() { _ = listener.Close() }()
202	accepted := make(chan wire.Transport, 1)
203	go func() {
204		if transport, err := listener.Accept(); err == nil {
205			accepted <- transport
206		}
207	}()
208
209	recorder := &markRecorder{}
210	client, err := DialConfig(listener.Addr(), "vev-bootstrap", fingerprint, Config{}, 10*time.Second, WithRuntimeObserver(recorder)).Dial(context.Background())
211	require.NoError(t, err)
212	defer func() { _ = client.Close() }()
213
214	select {
215	case server := <-accepted:
216		defer func() { _ = server.Close() }()
217		require.NoError(t, client.Send(wire.Envelope{Payload: []byte("observed")}))
218		_, err = server.Recv()
219		require.NoError(t, err)
220	case <-time.After(10 * time.Second):
221		t.Fatal("listener did not accept")
222	}
223
224	recorder.mu.Lock()
225	defer recorder.mu.Unlock()
226	require.NotEmpty(t, recorder.marks, "observer must receive marks from the QUIC transport")
227	for _, mark := range recorder.marks {
228		require.Equal(t, "quic", mark.Component)
229	}
230}
231
232func TestQUICTransportCloseReportsEOF(t *testing.T) {
233	cert, fingerprint, err := GenerateEphemeralCert()
234	require.NoError(t, err)
235	listener, err := ListenConfig("127.0.0.1:0", cert, Config{}, 4)
236	require.NoError(t, err)
237	defer func() { _ = listener.Close() }()
238
239	accepted := make(chan wire.Transport, 1)
240	go func() {
241		transport, err := listener.Accept()
242		if err != nil {
243			return
244		}
245		accepted <- transport
246	}()
247	dialer := DialConfig(listener.Addr(), "vev-bootstrap", fingerprint, Config{}, 10*time.Second)
248	client, err := dialer.Dial(context.Background())
249	require.NoError(t, err)
250
251	select {
252	case server := <-accepted:
253		// A graceful peer close reads as EOF, like IPC/SSH.
254		require.NoError(t, server.Close())
255		_, err = client.Recv()
256		require.ErrorIs(t, err, io.EOF)
257		_ = client.Close()
258	case <-time.After(10 * time.Second):
259		t.Fatal("listener did not accept")
260		_ = client.Close()
261	}
262}
263
264func TestQUICProbeSkippedOnce(t *testing.T) {
265	cert, fingerprint, err := GenerateEphemeralCert()
266	require.NoError(t, err)
267	listener, err := ListenConfig("127.0.0.1:0", cert, Config{}, 4)
268	require.NoError(t, err)
269	defer func() { _ = listener.Close() }()
270
271	accepted := make(chan wire.Transport, 1)
272	go func() {
273		transport, err := listener.Accept()
274		if err != nil {
275			return
276		}
277		accepted <- transport
278	}()
279	dialer := DialConfig(listener.Addr(), "vev-bootstrap", fingerprint, Config{}, 10*time.Second)
280	client, err := dialer.Dial(context.Background())
281	require.NoError(t, err)
282
283	select {
284	case server := <-accepted:
285		defer func() { _ = server.Close() }()
286		defer func() { _ = client.Close() }()
287		// The dial probe is skipped silently; the next Recv blocks
288		// for real traffic, so close unblocks it with an error.
289		received := make(chan error, 1)
290		go func() {
291			_, err := server.Recv()
292			received <- err
293		}()
294		require.NoError(t, client.Close())
295		select {
296		case err := <-received:
297			require.Error(t, err)
298		case <-time.After(10 * time.Second):
299			t.Fatal("server Recv was not unblocked")
300		}
301	case <-time.After(10 * time.Second):
302		t.Fatal("listener did not accept")
303		_ = client.Close()
304	}
305}
306
307func TestQUICCloseUnblocksRecv(t *testing.T) {
308	cert, fingerprint, err := GenerateEphemeralCert()
309	require.NoError(t, err)
310	listener, err := ListenConfig("127.0.0.1:0", cert, Config{}, 4)
311	require.NoError(t, err)
312	defer func() { _ = listener.Close() }()
313
314	accepted := make(chan wire.Transport, 1)
315	go func() {
316		transport, err := listener.Accept()
317		if err != nil {
318			return
319		}
320		accepted <- transport
321	}()
322	dialer := DialConfig(listener.Addr(), "vev-bootstrap", fingerprint, Config{}, 10*time.Second)
323	client, err := dialer.Dial(context.Background())
324	require.NoError(t, err)
325
326	select {
327	case server := <-accepted:
328		received := make(chan error, 1)
329		go func() {
330			_, err := server.Recv()
331			received <- err
332		}()
333		require.NoError(t, client.Close())
334		select {
335		case err := <-received:
336			require.Error(t, err)
337		case <-time.After(10 * time.Second):
338			t.Fatal("server Recv was not unblocked by client close")
339		}
340		_ = server.Close()
341	case <-time.After(10 * time.Second):
342		t.Fatal("listener did not accept")
343		_ = client.Close()
344	}
345}
346
347// dialTestPairWithConfig returns the server and client ends of one live
348// transport whose listener uses the supplied Config, both enforcing the
349// one-stream contract.
350func dialTestPairWithConfig(t *testing.T, config Config) (*Transport, *Transport) {
351	t.Helper()
352	cert, fingerprint, err := GenerateEphemeralCert()
353	require.NoError(t, err)
354	listener, err := ListenConfig("127.0.0.1:0", cert, config, 4)
355	require.NoError(t, err)
356	t.Cleanup(func() { _ = listener.Close() })
357	accepted := make(chan *Transport, 1)
358	go func() {
359		transport, err := listener.Accept()
360		if err == nil {
361			accepted <- transport.(*Transport)
362		}
363	}()
364	client, err := DialConfig(listener.Addr(), "vev-bootstrap", fingerprint, Config{}, 10*time.Second).Dial(context.Background())
365	require.NoError(t, err)
366	t.Cleanup(func() { _ = client.Close() })
367	select {
368	case server := <-accepted:
369		t.Cleanup(func() { _ = server.Close() })
370		return server, client.(*Transport)
371	case <-time.After(10 * time.Second):
372		t.Fatal("listener did not accept")
373		return nil, nil
374	}
375}
376
377// TestQUICUniStreamsTrulyDisabled proves the zero Config keeps quic-go's
378// 100-stream library default from leaking in: without a negative limit the
379// peer could open unidirectional streams.
380func TestQUICUniStreamsTrulyDisabled(t *testing.T) {
381	_, client := dialTestPairWithConfig(t, Config{})
382	ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
383	defer cancel()
384	_, err := client.conn.OpenUniStreamSync(ctx)
385	require.Error(t, err, "unidirectional streams must be truly disabled, not quic-go's 100-stream default")
386}
387
388// TestQUICServerCannotOpenStreams proves role-forbidden streams are truly
389// disabled on the dialer side: the server never opens streams toward the
390// client.
391func TestQUICServerCannotOpenStreams(t *testing.T) {
392	server, _ := dialTestPairWithConfig(t, Config{})
393	openers := map[string]func(context.Context) error{
394		"bidi": func(ctx context.Context) error {
395			_, err := server.conn.OpenStreamSync(ctx)
396			return err
397		},
398		"uni": func(ctx context.Context) error {
399			_, err := server.conn.OpenUniStreamSync(ctx)
400			return err
401		},
402	}
403	for name, open := range openers {
404		t.Run(name, func(t *testing.T) {
405			ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
406			defer cancel()
407			require.Error(t, open(ctx), "the server role must not open streams toward the dialer")
408		})
409	}
410}
411
412// TestQUICDelayedExtraStreamEnforced proves the one-stream contract holds
413// for the whole connection lifetime, not just a handshake window: a second
414// stream opened well after setup fails the live transport instead of being
415// silently tolerated. The listener's explicit limit of two makes the second
416// stream admissible at the QUIC layer so the test exercises the lifetime
417// guard, not flow control.
418func TestQUICDelayedExtraStreamEnforced(t *testing.T) {
419	server, client := dialTestPairWithConfig(t, Config{MaxIncomingStreams: 2})
420	// Land the violation long after the handshake has settled.
421	time.Sleep(200 * time.Millisecond)
422	openCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
423	defer cancel()
424	extra, err := client.conn.OpenStreamSync(openCtx)
425	require.NoError(t, err, "second stream is admissible under the explicit limit")
426	defer func() { _ = extra.Close() }()
427	// QUIC has no stream-open frame: the peer only learns about the stream
428	// when a frame referencing it arrives.
429	_, err = extra.Write([]byte("violation"))
430	require.NoError(t, err)
431
432	recvErr := make(chan error, 1)
433	go func() {
434		_, err := server.Recv()
435		recvErr <- err
436	}()
437	select {
438	case err := <-recvErr:
439		require.Error(t, err, "delayed extra stream must fail the live transport")
440	case <-time.After(5 * time.Second):
441		t.Fatal("delayed extra stream was not enforced beyond the startup window")
442	}
443}
444
445// TestQUICDialHasNoStartupGraceLatency proves connection setup does not pay a
446// fixed per-connection stream-contract grace window. Each such window cost a
447// flat 100ms; ten sequential loopback dials must now finish in a fraction of
448// that accumulated cost, even on a loaded race-enabled machine. The bound is
449// deliberately loose (well under the ~1s the removed window alone would add)
450// so it measures the regression without being a tight timing assertion.
451func TestQUICDialHasNoStartupGraceLatency(t *testing.T) {
452	cert, fingerprint, err := GenerateEphemeralCert()
453	require.NoError(t, err)
454	listener, err := ListenConfig("127.0.0.1:0", cert, Config{}, 16)
455	require.NoError(t, err)
456	accepted := make(chan wire.Transport, 16)
457	acceptorDone := make(chan struct{})
458	go func() {
459		defer close(acceptorDone)
460		for {
461			transport, err := listener.Accept()
462			if err != nil {
463				return
464			}
465			accepted <- transport
466		}
467	}()
468
469	const dials = 10
470	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
471	defer cancel()
472	start := time.Now()
473	for range dials {
474		client, err := DialConfig(listener.Addr(), "vev-bootstrap", fingerprint, Config{}, 5*time.Second).Dial(ctx)
475		require.NoError(t, err)
476		require.NoError(t, client.Close())
477	}
478	elapsed := time.Since(start)
479	require.Less(t, elapsed, 700*time.Millisecond,
480		"sequential dials must not pay a per-connection startup grace window")
481
482	require.NoError(t, listener.Close())
483	<-acceptorDone
484	close(accepted)
485	for transport := range accepted {
486		_ = transport.Close()
487	}
488}
489
490// readLinkEvent reads one link event or fails the test.
491func readLinkEvent(t *testing.T, events <-chan ports.LinkEvent) (ports.LinkEvent, bool) {
492	t.Helper()
493	select {
494	case event, ok := <-events:
495		return event, ok
496	case <-time.After(5 * time.Second):
497		t.Fatal("timed out waiting for a link event")
498		return ports.LinkEvent{}, false
499	}
500}
501
502// TestQUICLinkEventsReportTerminalOffline proves the LinkEvents contract is
503// honest: the adapter publishes the terminal Offline transition and closes
504// the event stream, locally on Close and remotely when the peer ends the
505// connection, without leaking a QUIC library type in LinkEvent.Err.
506func TestQUICLinkEventsReportTerminalOffline(t *testing.T) {
507	server, client := dialTestPairWithConfig(t, Config{})
508	require.Equal(t, ports.LinkStateConnected, server.LinkState())
509	require.Equal(t, ports.LinkStateConnected, client.LinkState())
510
511	require.NoError(t, server.Close())
512	require.Equal(t, ports.LinkStateOffline, server.LinkState())
513	event, ok := readLinkEvent(t, server.LinkEvents())
514	require.True(t, ok)
515	require.Equal(t, ports.LinkStateOffline, event.State)
516	require.NoError(t, event.Err, "a local Close is not a failure")
517	_, ok = readLinkEvent(t, server.LinkEvents())
518	require.False(t, ok, "local event stream must close after the terminal event")
519
520	require.Eventually(t, func() bool {
521		return client.LinkState() == ports.LinkStateOffline
522	}, 5*time.Second, 10*time.Millisecond, "peer must observe the terminal Offline state")
523	event, ok = readLinkEvent(t, client.LinkEvents())
524	require.True(t, ok)
525	require.Equal(t, ports.LinkStateOffline, event.State)
526	require.ErrorIs(t, event.Err, io.EOF, "peer failure must map to a boundary-safe error")
527	_, ok = readLinkEvent(t, client.LinkEvents())
528	require.False(t, ok, "peer event stream must close after the terminal event")
529}
530
531func (r *markRecorder) snapshot() []ports.RuntimeMark {
532	r.mu.Lock()
533	defer r.mu.Unlock()
534	return append([]ports.RuntimeMark(nil), r.marks...)
535}
536
537// assertBalancedQUICMarks proves every started adapter operation also closed:
538// the transport must never leak a start mark when Close (or a failure) ends an
539// operation.
540func assertBalancedQUICMarks(t *testing.T, marks []ports.RuntimeMark) {
541	t.Helper()
542	starts := make(map[uint64]ports.RuntimeMarkKind)
543	for _, mark := range marks {
544		switch mark.Kind {
545		case ports.RuntimeAdapterSendStart, ports.RuntimeAdapterReceiveStart:
546			_, duplicate := starts[mark.Sequence]
547			require.False(t, duplicate, "duplicate start mark for one operation")
548			starts[mark.Sequence] = mark.Kind
549		case ports.RuntimeAdapterSendEnd, ports.RuntimeAdapterReceiveEnd:
550			start, ok := starts[mark.Sequence]
551			require.True(t, ok, "end mark without a start mark")
552			want := ports.RuntimeAdapterSendEnd
553			if start == ports.RuntimeAdapterReceiveStart {
554				want = ports.RuntimeAdapterReceiveEnd
555			}
556			require.Equal(t, want, mark.Kind)
557			delete(starts, mark.Sequence)
558		}
559	}
560	require.Empty(t, starts, "start marks without an end mark")
561}
562
563// dialObservedTestPair dials a listener configured with observer, so the
564// accepted (accept-side) transport carries runtime marks.
565func dialObservedTestPair(t *testing.T, observer ports.SerializedRuntimeObserver) (wire.Transport, wire.Transport) {
566	t.Helper()
567	cert, fingerprint, err := GenerateEphemeralCert()
568	require.NoError(t, err)
569	listener, err := ListenConfig("127.0.0.1:0", cert, Config{}, 4, WithListenerRuntimeObserver(observer))
570	require.NoError(t, err)
571	t.Cleanup(func() { _ = listener.Close() })
572
573	accepted := make(chan wire.Transport, 1)
574	go func() {
575		if transport, err := listener.Accept(); err == nil {
576			accepted <- transport
577		}
578	}()
579
580	client, err := DialConfig(listener.Addr(), "vev-bootstrap", fingerprint, Config{}, 10*time.Second).Dial(context.Background())
581	require.NoError(t, err)
582	t.Cleanup(func() { _ = client.Close() })
583
584	select {
585	case server := <-accepted:
586		t.Cleanup(func() { _ = server.Close() })
587		return client, server
588	case <-time.After(10 * time.Second):
589		t.Fatal("listener did not accept")
590		return nil, nil
591	}
592}
593
594// TestQUICAcceptObserverEmitsAdapterMarks proves the observer wired through
595// the listener (not just DialConfig) reaches the accepted transport.
596func TestQUICAcceptObserverEmitsAdapterMarks(t *testing.T) {
597	recorder := &markRecorder{}
598	client, server := dialObservedTestPair(t, recorder)
599
600	require.NoError(t, client.Send(wire.Envelope{Payload: []byte("observed")}))
601	got, err := server.Recv()
602	require.NoError(t, err)
603	require.Equal(t, []byte("observed"), got.Payload)
604	require.NoError(t, server.Send(wire.Envelope{Payload: []byte("ack")}))
605	_, err = client.Recv()
606	require.NoError(t, err)
607
608	marks := recorder.snapshot()
609	require.NotEmpty(t, marks, "accept side must observe carriage marks")
610	for _, mark := range marks {
611		require.Equal(t, "quic", mark.Component)
612	}
613	assertBalancedQUICMarks(t, marks)
614}
615
616// TestQUICObserverMarksBalanceAcrossClose proves Close gates new observed
617// operations and waits for an in-flight blocked Recv to close its mark pair.
618func TestQUICObserverMarksBalanceAcrossClose(t *testing.T) {
619	recorder := &markRecorder{}
620	client, server := dialObservedTestPair(t, recorder)
621
622	require.NoError(t, client.Send(wire.Envelope{Payload: []byte("before close")}))
623	_, err := server.Recv()
624	require.NoError(t, err)
625
626	blocked := make(chan struct{})
627	go func() {
628		_, _ = server.Recv()
629		close(blocked)
630	}()
631	require.NoError(t, server.Close())
632	<-blocked
633
634	before := len(recorder.snapshot())
635	// A Send after Close fails closed without opening a new observed operation.
636	_ = server.Send(wire.Envelope{Payload: []byte("late")})
637	marks := recorder.snapshot()
638	require.Equal(t, before, len(marks), "operations after Close must not be observed")
639	assertBalancedQUICMarks(t, marks)
640}

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