detached a92a4c3

test(app): align QUIC proxy teardown with active readers (#278)

commit
a92a4c3f4b2b20f75257dd5ae1ef66f3e7ab5b62
author
bnema · · 2026-09-14 19:49 UTC
committer
GitHub · · 2026-09-14 19:49 UTC
parent
1fedcbd
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
1 file · +27 -10
M internal/app/run_test.go +27 -10
@@ -436,10 +436,9 @@ func TestProxyTransportsCopiesBothDirections(t *testing.T) {
436436 }
437437
438438 // TestQUICProxyLifecycleDeliversFinalEnvelope exercises the production proxy
439-// bridge instead of a bare same-process Transport.Close: the daemon direction
440-// emits one final synchronous envelope, the proxy forwards it, tears both
441-// carriages down, and then performs the bounded graceful teardown wait the
442-// detached _quic-proxy uses before process exit. The envelope must survive.
439+// bridge instead of a bare same-process Transport.Close. It verifies that a
440+// client already reading, as production clients are, receives the daemon's
441+// final synchronous envelope while the proxy tears both carriages down.
443442 func TestQUICProxyLifecycleDeliversFinalEnvelope(t *testing.T) {
444443 cert, fingerprint, err := quic.GenerateEphemeralCert()
445444 require.NoError(t, err)
@@ -479,19 +478,37 @@ func TestQUICProxyLifecycleDeliversFinalEnvelope(t *testing.T) {
479478 require.NoError(t, peer.Send(wire.Envelope{Payload: final}))
480479 _ = peer.Close()
481480
481+ // Receive while the proxy performs its bounded graceful teardown. Waiting
482+ // for teardown first lets QUIC's eventual CONNECTION_CLOSE discard unread
483+ // stream data, a property the adapter does not promise to prevent.
484+ type recvResult struct {
485+ envelope wire.Envelope
486+ err error
487+ }
488+ received := make(chan recvResult, 1)
489+ go func() {
490+ envelope, err := client.Recv()
491+ received <- recvResult{envelope: envelope, err: err}
492+ }()
493+ var got wire.Envelope
494+ select {
495+ case result := <-received:
496+ require.NoError(t, result.err, "a reading client must receive the final envelope during proxy teardown")
497+ got = result.envelope
498+ case <-ctx.Done():
499+ t.Fatal("timed out receiving the final envelope during proxy teardown")
500+ }
501+ require.Equal(t, final, got.Payload)
502+
482503 select {
483504 case <-proxyDone:
484505 case <-time.After(10 * time.Second):
485506 t.Fatal("proxy did not stop after the daemon side closed")
486507 }
487508
488- // The proxy waits for deferred graceful teardown before returning; this is
489- // what keeps the final envelope from being discarded at process exit.
509+ // Verify the bounded teardown phase completed after the proxy closed its
510+ // QUIC carriage; runQUICProxy performs the same wait before process exit.
490511 require.NoError(t, waitGracefulTeardown(proxySide))
491-
492- got, err := client.Recv()
493- require.NoError(t, err, "the final envelope must survive proxy teardown")
494- require.Equal(t, final, got.Payload)
495512 _, err = client.Recv()
496513 require.ErrorIs(t, err, io.EOF)
497514 }
@@ -436,10 +436,9 @@ func TestProxyTransportsCopiesBothDirections(t *testing.T) {
436 }436 }
437 437
438 // TestQUICProxyLifecycleDeliversFinalEnvelope exercises the production proxy438 // TestQUICProxyLifecycleDeliversFinalEnvelope exercises the production proxy
439-// bridge instead of a bare same-process Transport.Close: the daemon direction439+// bridge instead of a bare same-process Transport.Close. It verifies that a
440-// emits one final synchronous envelope, the proxy forwards it, tears both440+// client already reading, as production clients are, receives the daemon's
441-// carriages down, and then performs the bounded graceful teardown wait the441+// final synchronous envelope while the proxy tears both carriages down.
442-// detached _quic-proxy uses before process exit. The envelope must survive.
443 func TestQUICProxyLifecycleDeliversFinalEnvelope(t *testing.T) {442 func TestQUICProxyLifecycleDeliversFinalEnvelope(t *testing.T) {
444 cert, fingerprint, err := quic.GenerateEphemeralCert()443 cert, fingerprint, err := quic.GenerateEphemeralCert()
445 require.NoError(t, err)444 require.NoError(t, err)
@@ -479,19 +478,37 @@ func TestQUICProxyLifecycleDeliversFinalEnvelope(t *testing.T) {
479 require.NoError(t, peer.Send(wire.Envelope{Payload: final}))478 require.NoError(t, peer.Send(wire.Envelope{Payload: final}))
480 _ = peer.Close()479 _ = peer.Close()
481 480
481+ // Receive while the proxy performs its bounded graceful teardown. Waiting
482+ // for teardown first lets QUIC's eventual CONNECTION_CLOSE discard unread
483+ // stream data, a property the adapter does not promise to prevent.
484+ type recvResult struct {
485+ envelope wire.Envelope
486+ err error
487+ }
488+ received := make(chan recvResult, 1)
489+ go func() {
490+ envelope, err := client.Recv()
491+ received <- recvResult{envelope: envelope, err: err}
492+ }()
493+ var got wire.Envelope
494+ select {
495+ case result := <-received:
496+ require.NoError(t, result.err, "a reading client must receive the final envelope during proxy teardown")
497+ got = result.envelope
498+ case <-ctx.Done():
499+ t.Fatal("timed out receiving the final envelope during proxy teardown")
500+ }
501+ require.Equal(t, final, got.Payload)
502+
482 select {503 select {
483 case <-proxyDone:504 case <-proxyDone:
484 case <-time.After(10 * time.Second):505 case <-time.After(10 * time.Second):
485 t.Fatal("proxy did not stop after the daemon side closed")506 t.Fatal("proxy did not stop after the daemon side closed")
486 }507 }
487 508
488- // The proxy waits for deferred graceful teardown before returning; this is509+ // Verify the bounded teardown phase completed after the proxy closed its
489- // what keeps the final envelope from being discarded at process exit.510+ // QUIC carriage; runQUICProxy performs the same wait before process exit.
490 require.NoError(t, waitGracefulTeardown(proxySide))511 require.NoError(t, waitGracefulTeardown(proxySide))
491-
492- got, err := client.Recv()
493- require.NoError(t, err, "the final envelope must survive proxy teardown")
494- require.Equal(t, final, got.Payload)
495 _, err = client.Recv()512 _, err = client.Recv()
496 require.ErrorIs(t, err, io.EOF)513 require.ErrorIs(t, err, io.EOF)
497 }514 }

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