detached e120b49

refactor(app): delete dead broker render, local probe, kill, and spawn helpers

brokerClientRender, newLocalRouteProbe(WithConnector), connectivityOwnerFor,
joinLifecycleReleaseError, and the whole sendKillRequest/receiveKillResult/
newControlRequestID/ensureDaemon/retryDial/spawnDaemonWithEnvironment
cluster have zero production callers after the broker cutover.

Tests that verified real behavior through them are rewritten onto the
production path instead of deleted:
  - render tests now build *brokerClientPresentation directly, exactly as
    the terminal composition does.
  - newLocalRouteProbe/WithConnector are retained as test-only convenience
    wrappers (local_route_probe_test_helpers_test.go) around
    newDynamicLocalRouteProbe, the constructor brokerLocalProbe uses in
    production.
  - connectivityOwnerFor's three call sites now range over connectivityMatrix
    directly, the pattern every other pinning test in the file already uses.

spawn_test.go's ensureDaemon/retryDial-only test functions are deleted with
the functions they exercised; TestMain and the realSpawn/spawn-lock/retry-
attempts tests, which cover code that is still live, are untouched.
commit
e120b49a593685b0ec85ba63b7a8d7e504df219d
author
brice · · 2026-09-23 01:55 UTC
parent
3e67bb0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +67 -324
M internal/app/broker_client.go +0 -7
@@ -531,13 +531,6 @@ func (c *productionBrokerConnector) Connect(ctx context.Context) (ports.BrokerSe
531531 return c.connect(ctx)
532532 }
533533
534-// brokerClientRender adapts the shared presentation to the plain render callback
535-// the composition tests exercise directly.
536-func brokerClientRender(terminal ports.Terminal, picker *client.Picker, onState func(client.State)) func(client.State) {
537- presentation := &brokerClientPresentation{terminal: terminal, picker: picker, onState: onState}
538- return presentation.Render
539-}
540-
541534 // terminalBrokerSeams are the optional observation callbacks of one terminal
542535 // run. Production supplies none: runBrokerClient reports a failure on the
543536 // terminal itself through the same writer the picker paints with. The terminal
@@ -531,13 +531,6 @@ func (c *productionBrokerConnector) Connect(ctx context.Context) (ports.BrokerSe
531 return c.connect(ctx)531 return c.connect(ctx)
532 }532 }
533 533
534-// brokerClientRender adapts the shared presentation to the plain render callback
535-// the composition tests exercise directly.
536-func brokerClientRender(terminal ports.Terminal, picker *client.Picker, onState func(client.State)) func(client.State) {
537- presentation := &brokerClientPresentation{terminal: terminal, picker: picker, onState: onState}
538- return presentation.Render
539-}
540-
541 // terminalBrokerSeams are the optional observation callbacks of one terminal534 // terminalBrokerSeams are the optional observation callbacks of one terminal
542 // run. Production supplies none: runBrokerClient reports a failure on the535 // run. Production supplies none: runBrokerClient reports a failure on the
543 // terminal itself through the same writer the picker paints with. The terminal536 // terminal itself through the same writer the picker paints with. The terminal
M internal/app/broker_offline_client_render_test.go +13 -4
@@ -17,6 +17,15 @@ import (
1717 "github.com/bnema/vev/internal/usecase/client"
1818 )
1919
20+// testBrokerClientRender builds the same *brokerClientPresentation the
21+// production terminal composition renders through (see runBrokerClient's
22+// presentation construction), so tests exercise the real Render method
23+// instead of a test-only indirection.
24+func testBrokerClientRender(terminal ports.Terminal, picker *client.Picker, onState func(client.State)) func(client.State) {
25+ presentation := &brokerClientPresentation{terminal: terminal, picker: picker, onState: onState}
26+ return presentation.Render
27+}
28+
2029 // The resize presentation fence at the composition seam. The supervisor asks
2130 // its renderer to repaint whenever a resize invalidation reaches a serialized
2231 // picker wait, and the production offline composition paints only while the
@@ -152,7 +161,7 @@ func TestOfflineClientRenderRefusesNonPickerPresentations(t *testing.T) {
152161 picker := offlineRenderPicker(t)
153162
154163 observed := make(chan client.State, 8)
155- render := brokerClientRender(terminal, picker, func(state client.State) {
164+ render := testBrokerClientRender(terminal, picker, func(state client.State) {
156165 select {
157166 case observed <- state:
158167 default:
@@ -192,7 +201,7 @@ func TestOfflineClientRenderFenceIsPickerPresentation(t *testing.T) {
192201 for _, state := range states {
193202 t.Run(state.Presentation.String(), func(t *testing.T) {
194203 terminal := newOfflineRenderTerminal(small)
195- render := brokerClientRender(terminal, offlineRenderPicker(t), nil)
204+ render := testBrokerClientRender(terminal, offlineRenderPicker(t), nil)
196205
197206 render(state)
198207
@@ -218,7 +227,7 @@ func TestOfflineClientRenderPaintsPickerResize(t *testing.T) {
218227
219228 terminal := newOfflineRenderTerminal(small)
220229 picker := offlineRenderPicker(t)
221- render := brokerClientRender(terminal, picker, nil)
230+ render := testBrokerClientRender(terminal, picker, nil)
222231
223232 render(client.State{Presentation: client.PresentPicker, Connectivity: client.ConnectivityReady, Generation: 1})
224233 initial, flushes := terminal.written()
@@ -284,7 +293,7 @@ func (t *transactionRecordingTerminal) published() []ports.UIContext {
284293 // succeeds only after the frame was written and flushed.
285294 func TestOfflineClientPickerPaintCommitsPickerTransaction(t *testing.T) {
286295 terminal := newTransactionRecordingTerminal(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}})
287- render := brokerClientRender(terminal, offlineRenderPicker(t), nil)
296+ render := testBrokerClientRender(terminal, offlineRenderPicker(t), nil)
288297
289298 render(client.State{Presentation: client.PresentPicker, Connectivity: client.ConnectivityReady, Generation: 1})
290299 written, _ := terminal.written()
@@ -17,6 +17,15 @@ import (
17 "github.com/bnema/vev/internal/usecase/client"17 "github.com/bnema/vev/internal/usecase/client"
18 )18 )
19 19
20+// testBrokerClientRender builds the same *brokerClientPresentation the
21+// production terminal composition renders through (see runBrokerClient's
22+// presentation construction), so tests exercise the real Render method
23+// instead of a test-only indirection.
24+func testBrokerClientRender(terminal ports.Terminal, picker *client.Picker, onState func(client.State)) func(client.State) {
25+ presentation := &brokerClientPresentation{terminal: terminal, picker: picker, onState: onState}
26+ return presentation.Render
27+}
28+
20 // The resize presentation fence at the composition seam. The supervisor asks29 // The resize presentation fence at the composition seam. The supervisor asks
21 // its renderer to repaint whenever a resize invalidation reaches a serialized30 // its renderer to repaint whenever a resize invalidation reaches a serialized
22 // picker wait, and the production offline composition paints only while the31 // picker wait, and the production offline composition paints only while the
@@ -152,7 +161,7 @@ func TestOfflineClientRenderRefusesNonPickerPresentations(t *testing.T) {
152 picker := offlineRenderPicker(t)161 picker := offlineRenderPicker(t)
153 162
154 observed := make(chan client.State, 8)163 observed := make(chan client.State, 8)
155- render := brokerClientRender(terminal, picker, func(state client.State) {164+ render := testBrokerClientRender(terminal, picker, func(state client.State) {
156 select {165 select {
157 case observed <- state:166 case observed <- state:
158 default:167 default:
@@ -192,7 +201,7 @@ func TestOfflineClientRenderFenceIsPickerPresentation(t *testing.T) {
192 for _, state := range states {201 for _, state := range states {
193 t.Run(state.Presentation.String(), func(t *testing.T) {202 t.Run(state.Presentation.String(), func(t *testing.T) {
194 terminal := newOfflineRenderTerminal(small)203 terminal := newOfflineRenderTerminal(small)
195- render := brokerClientRender(terminal, offlineRenderPicker(t), nil)204+ render := testBrokerClientRender(terminal, offlineRenderPicker(t), nil)
196 205
197 render(state)206 render(state)
198 207
@@ -218,7 +227,7 @@ func TestOfflineClientRenderPaintsPickerResize(t *testing.T) {
218 227
219 terminal := newOfflineRenderTerminal(small)228 terminal := newOfflineRenderTerminal(small)
220 picker := offlineRenderPicker(t)229 picker := offlineRenderPicker(t)
221- render := brokerClientRender(terminal, picker, nil)230+ render := testBrokerClientRender(terminal, picker, nil)
222 231
223 render(client.State{Presentation: client.PresentPicker, Connectivity: client.ConnectivityReady, Generation: 1})232 render(client.State{Presentation: client.PresentPicker, Connectivity: client.ConnectivityReady, Generation: 1})
224 initial, flushes := terminal.written()233 initial, flushes := terminal.written()
@@ -284,7 +293,7 @@ func (t *transactionRecordingTerminal) published() []ports.UIContext {
284 // succeeds only after the frame was written and flushed.293 // succeeds only after the frame was written and flushed.
285 func TestOfflineClientPickerPaintCommitsPickerTransaction(t *testing.T) {294 func TestOfflineClientPickerPaintCommitsPickerTransaction(t *testing.T) {
286 terminal := newTransactionRecordingTerminal(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}})295 terminal := newTransactionRecordingTerminal(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}})
287- render := brokerClientRender(terminal, offlineRenderPicker(t), nil)296+ render := testBrokerClientRender(terminal, offlineRenderPicker(t), nil)
288 297
289 render(client.State{Presentation: client.PresentPicker, Connectivity: client.ConnectivityReady, Generation: 1})298 render(client.State{Presentation: client.PresentPicker, Connectivity: client.ConnectivityReady, Generation: 1})
290 written, _ := terminal.written()299 written, _ := terminal.written()
M internal/app/broker_offline_client_test.go +1 -1
@@ -299,7 +299,7 @@ func TestParseBrokerClientArgsAndProductionDispatchIsolation(t *testing.T) {
299299
300300 func mustConnectivityOwner(t *testing.T, kind string) connectivityOwner {
301301 t.Helper()
302- owner, ok := connectivityOwnerFor(kind)
302+ owner, ok := testConnectivityOwnerFor(kind)
303303 require.True(t, ok)
304304 return owner
305305 }
@@ -299,7 +299,7 @@ func TestParseBrokerClientArgsAndProductionDispatchIsolation(t *testing.T) {
299 299
300 func mustConnectivityOwner(t *testing.T, kind string) connectivityOwner {300 func mustConnectivityOwner(t *testing.T, kind string) connectivityOwner {
301 t.Helper()301 t.Helper()
302- owner, ok := connectivityOwnerFor(kind)302+ owner, ok := testConnectivityOwnerFor(kind)
303 require.True(t, ok)303 require.True(t, ok)
304 return owner304 return owner
305 }305 }
M internal/app/broker_offline_local.go +0 -30
@@ -64,36 +64,6 @@ type localRouteProbe struct {
6464
6565 var _ broker.LocalProbe = (*localRouteProbe)(nil)
6666
67-// newLocalRouteProbe fences one broker-owned local binding into a probe over a
68-// dial seam. The binding's identity, policy, and route address must validate,
69-// the ceilings must be a valid advertisement, the epoch must be the broker
70-// process incarnation every local stream is scoped to, and a nil dial is
71-// refused: a probe that cannot establish a carriage must not exist. It builds
72-// its own endpoint connector; a composition that already owns the pooled
73-// connector passes it to newLocalRouteProbeWithConnector instead, so one broker
74-// process never holds two connector owners for the same route.
75-func newLocalRouteProbe(binding brokerconfig.LocalBinding, epoch ports.BrokerEpoch, dial localCarrierDialer, ceilings daemonmux.MuxCeilings) (*localRouteProbe, error) {
76- if dial == nil {
77- return nil, errors.New("vev: broker local probe requires a dialer")
78- }
79- connector, err := daemonmux.NewEndpointConnector(daemonmux.RawCarrierDialer(dial), ceilings)
80- if err != nil {
81- return nil, fmt.Errorf("vev: broker local probe: %w", err)
82- }
83- return newLocalRouteProbeWithConnector(binding, epoch, connector)
84-}
85-
86-// newLocalRouteProbeWithConnector fences one broker-owned local binding into a
87-// probe over an existing connector. The connector is the same setup half a
88-// pooled physical connection uses, so the probe composes exactly the transport
89-// owner the broker already has and never becomes a second one. The binding's
90-// identity, policy, and route must validate and the epoch must be nonzero.
91-func newLocalRouteProbeWithConnector(binding brokerconfig.LocalBinding, epoch ports.BrokerEpoch, connector ports.BrokerEndpointConnector) (*localRouteProbe, error) {
92- return newDynamicLocalRouteProbe(binding.Route, binding.Policy, epoch, connector, func() (ports.BrokerDaemonIdentity, error) {
93- return binding.Identity, nil
94- })
95-}
96-
9767 func newDynamicLocalRouteProbe(route brokerconfig.Route, policy ports.BrokerPolicy, epoch ports.BrokerEpoch, connector ports.BrokerEndpointConnector, loadIdentity localIdentityLoader) (*localRouteProbe, error) {
9868 if epoch == 0 {
9969 return nil, errors.New("vev: broker local probe requires a broker epoch")
@@ -64,36 +64,6 @@ type localRouteProbe struct {
64 64
65 var _ broker.LocalProbe = (*localRouteProbe)(nil)65 var _ broker.LocalProbe = (*localRouteProbe)(nil)
66 66
67-// newLocalRouteProbe fences one broker-owned local binding into a probe over a
68-// dial seam. The binding's identity, policy, and route address must validate,
69-// the ceilings must be a valid advertisement, the epoch must be the broker
70-// process incarnation every local stream is scoped to, and a nil dial is
71-// refused: a probe that cannot establish a carriage must not exist. It builds
72-// its own endpoint connector; a composition that already owns the pooled
73-// connector passes it to newLocalRouteProbeWithConnector instead, so one broker
74-// process never holds two connector owners for the same route.
75-func newLocalRouteProbe(binding brokerconfig.LocalBinding, epoch ports.BrokerEpoch, dial localCarrierDialer, ceilings daemonmux.MuxCeilings) (*localRouteProbe, error) {
76- if dial == nil {
77- return nil, errors.New("vev: broker local probe requires a dialer")
78- }
79- connector, err := daemonmux.NewEndpointConnector(daemonmux.RawCarrierDialer(dial), ceilings)
80- if err != nil {
81- return nil, fmt.Errorf("vev: broker local probe: %w", err)
82- }
83- return newLocalRouteProbeWithConnector(binding, epoch, connector)
84-}
85-
86-// newLocalRouteProbeWithConnector fences one broker-owned local binding into a
87-// probe over an existing connector. The connector is the same setup half a
88-// pooled physical connection uses, so the probe composes exactly the transport
89-// owner the broker already has and never becomes a second one. The binding's
90-// identity, policy, and route must validate and the epoch must be nonzero.
91-func newLocalRouteProbeWithConnector(binding brokerconfig.LocalBinding, epoch ports.BrokerEpoch, connector ports.BrokerEndpointConnector) (*localRouteProbe, error) {
92- return newDynamicLocalRouteProbe(binding.Route, binding.Policy, epoch, connector, func() (ports.BrokerDaemonIdentity, error) {
93- return binding.Identity, nil
94- })
95-}
96-
97 func newDynamicLocalRouteProbe(route brokerconfig.Route, policy ports.BrokerPolicy, epoch ports.BrokerEpoch, connector ports.BrokerEndpointConnector, loadIdentity localIdentityLoader) (*localRouteProbe, error) {67 func newDynamicLocalRouteProbe(route brokerconfig.Route, policy ports.BrokerPolicy, epoch ports.BrokerEpoch, connector ports.BrokerEndpointConnector, loadIdentity localIdentityLoader) (*localRouteProbe, error) {
98 if epoch == 0 {68 if epoch == 0 {
99 return nil, errors.New("vev: broker local probe requires a broker epoch")69 return nil, errors.New("vev: broker local probe requires a broker epoch")
M internal/app/connectivity_matrix.go +0 -10
@@ -265,13 +265,3 @@ var connectivityMatrix = []connectivityEntry{
265265 Notes: "Mints one ephemeral authenticated QUIC server, admits exactly one carriage, and bridges it to the single provisioned private Unix daemonmux carriage. It never dials the production daemon socket, never fabricates a daemon incarnation, and starts the daemon behind that carriage only under the propagated --daemon-start if-needed authorization and its own provisioned launch policy.",
266266 },
267267 }
268-
269-// connectivityOwnerFor returns the matrix owner for a cmdKind identifier.
270-func connectivityOwnerFor(kind string) (connectivityOwner, bool) {
271- for _, entry := range connectivityMatrix {
272- if entry.Kind == kind {
273- return entry.Owner, true
274- }
275- }
276- return 0, false
277-}
@@ -265,13 +265,3 @@ var connectivityMatrix = []connectivityEntry{
265 Notes: "Mints one ephemeral authenticated QUIC server, admits exactly one carriage, and bridges it to the single provisioned private Unix daemonmux carriage. It never dials the production daemon socket, never fabricates a daemon incarnation, and starts the daemon behind that carriage only under the propagated --daemon-start if-needed authorization and its own provisioned launch policy.",265 Notes: "Mints one ephemeral authenticated QUIC server, admits exactly one carriage, and bridges it to the single provisioned private Unix daemonmux carriage. It never dials the production daemon socket, never fabricates a daemon incarnation, and starts the daemon behind that carriage only under the propagated --daemon-start if-needed authorization and its own provisioned launch policy.",
266 },266 },
267 }267 }
268-
269-// connectivityOwnerFor returns the matrix owner for a cmdKind identifier.
270-func connectivityOwnerFor(kind string) (connectivityOwner, bool) {
271- for _, entry := range connectivityMatrix {
272- if entry.Kind == kind {
273- return entry.Owner, true
274- }
275- }
276- return 0, false
277-}
M internal/app/connectivity_matrix_test.go +15 -3
@@ -14,6 +14,18 @@ import (
1414 "github.com/stretchr/testify/require"
1515 )
1616
17+// testConnectivityOwnerFor looks up the matrix owner for a cmdKind identifier
18+// directly against connectivityMatrix, the same declarative fixture every
19+// other pinning test in this file ranges over.
20+func testConnectivityOwnerFor(kind string) (connectivityOwner, bool) {
21+ for _, entry := range connectivityMatrix {
22+ if entry.Kind == kind {
23+ return entry.Owner, true
24+ }
25+ }
26+ return 0, false
27+}
28+
1729 // TestConnectivityMatrixCoversEveryCommandKind pins P1.2: every cmdKind
1830 // constant declared in run.go has exactly one matrix row. A new CLI entry
1931 // point without a row fails here before any transport work can ignore it.
@@ -53,10 +65,10 @@ func TestConnectivityMatrixCoversEveryCommandKind(t *testing.T) {
5365 require.Empty(t, missing, "matrix is missing cmdKind rows")
5466 require.Empty(t, extra, "matrix references unknown cmdKind rows")
5567
56- owner, ok := connectivityOwnerFor("kindAttach")
68+ owner, ok := testConnectivityOwnerFor("kindAttach")
5769 require.True(t, ok, "matrix lookup must resolve known kinds")
5870 require.Equal(t, connectivityBrokerOnly, owner)
59- _, ok = connectivityOwnerFor("kindDoesNotExist")
71+ _, ok = testConnectivityOwnerFor("kindDoesNotExist")
6072 require.False(t, ok, "matrix lookup must reject unknown kinds")
6173 }
6274
@@ -220,7 +232,7 @@ func TestConnectivityMatrixSingleHostStoreWriterSet(t *testing.T) {
220232 func TestConnectivityMatrixReadyProbeIsLocalDialOnly(t *testing.T) {
221233 t.Parallel()
222234
223- owner, ok := connectivityOwnerFor("kindBrokerReady")
235+ owner, ok := testConnectivityOwnerFor("kindBrokerReady")
224236 require.True(t, ok, "the readiness probe must have a matrix row")
225237 require.Equal(t, connectivityLocalOnly, owner, "the readiness probe is dial-only local infrastructure")
226238 for _, entry := range connectivityMatrix {
@@ -14,6 +14,18 @@ import (
14 "github.com/stretchr/testify/require"14 "github.com/stretchr/testify/require"
15 )15 )
16 16
17+// testConnectivityOwnerFor looks up the matrix owner for a cmdKind identifier
18+// directly against connectivityMatrix, the same declarative fixture every
19+// other pinning test in this file ranges over.
20+func testConnectivityOwnerFor(kind string) (connectivityOwner, bool) {
21+ for _, entry := range connectivityMatrix {
22+ if entry.Kind == kind {
23+ return entry.Owner, true
24+ }
25+ }
26+ return 0, false
27+}
28+
17 // TestConnectivityMatrixCoversEveryCommandKind pins P1.2: every cmdKind29 // TestConnectivityMatrixCoversEveryCommandKind pins P1.2: every cmdKind
18 // constant declared in run.go has exactly one matrix row. A new CLI entry30 // constant declared in run.go has exactly one matrix row. A new CLI entry
19 // point without a row fails here before any transport work can ignore it.31 // point without a row fails here before any transport work can ignore it.
@@ -53,10 +65,10 @@ func TestConnectivityMatrixCoversEveryCommandKind(t *testing.T) {
53 require.Empty(t, missing, "matrix is missing cmdKind rows")65 require.Empty(t, missing, "matrix is missing cmdKind rows")
54 require.Empty(t, extra, "matrix references unknown cmdKind rows")66 require.Empty(t, extra, "matrix references unknown cmdKind rows")
55 67
56- owner, ok := connectivityOwnerFor("kindAttach")68+ owner, ok := testConnectivityOwnerFor("kindAttach")
57 require.True(t, ok, "matrix lookup must resolve known kinds")69 require.True(t, ok, "matrix lookup must resolve known kinds")
58 require.Equal(t, connectivityBrokerOnly, owner)70 require.Equal(t, connectivityBrokerOnly, owner)
59- _, ok = connectivityOwnerFor("kindDoesNotExist")71+ _, ok = testConnectivityOwnerFor("kindDoesNotExist")
60 require.False(t, ok, "matrix lookup must reject unknown kinds")72 require.False(t, ok, "matrix lookup must reject unknown kinds")
61 }73 }
62 74
@@ -220,7 +232,7 @@ func TestConnectivityMatrixSingleHostStoreWriterSet(t *testing.T) {
220 func TestConnectivityMatrixReadyProbeIsLocalDialOnly(t *testing.T) {232 func TestConnectivityMatrixReadyProbeIsLocalDialOnly(t *testing.T) {
221 t.Parallel()233 t.Parallel()
222 234
223- owner, ok := connectivityOwnerFor("kindBrokerReady")235+ owner, ok := testConnectivityOwnerFor("kindBrokerReady")
224 require.True(t, ok, "the readiness probe must have a matrix row")236 require.True(t, ok, "the readiness probe must have a matrix row")
225 require.Equal(t, connectivityLocalOnly, owner, "the readiness probe is dial-only local infrastructure")237 require.Equal(t, connectivityLocalOnly, owner, "the readiness probe is dial-only local infrastructure")
226 for _, entry := range connectivityMatrix {238 for _, entry := range connectivityMatrix {
A internal/app/local_route_probe_test_helpers_test.go +38 -0
@@ -0,0 +1,38 @@
1+package app
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/vev/internal/adapters/brokerconfig"
8+ "github.com/bnema/vev/internal/adapters/daemonmux"
9+ "github.com/bnema/vev/internal/ports"
10+)
11+
12+// newLocalRouteProbe fences one broker-owned local binding into a probe over a
13+// dial seam. The binding's identity, policy, and route address must validate,
14+// the ceilings must be a valid advertisement, the epoch must be the broker
15+// process incarnation every local stream is scoped to, and a nil dial is
16+// refused: a probe that cannot establish a carriage must not exist. It builds
17+// its own endpoint connector; a caller that already owns the pooled connector
18+// uses newLocalRouteProbeWithConnector instead, exactly as brokerLocalProbe
19+// does in production.
20+func newLocalRouteProbe(binding brokerconfig.LocalBinding, epoch ports.BrokerEpoch, dial localCarrierDialer, ceilings daemonmux.MuxCeilings) (*localRouteProbe, error) {
21+ if dial == nil {
22+ return nil, errors.New("vev: broker local probe requires a dialer")
23+ }
24+ connector, err := daemonmux.NewEndpointConnector(daemonmux.RawCarrierDialer(dial), ceilings)
25+ if err != nil {
26+ return nil, fmt.Errorf("vev: broker local probe: %w", err)
27+ }
28+ return newLocalRouteProbeWithConnector(binding, epoch, connector)
29+}
30+
31+// newLocalRouteProbeWithConnector fences one broker-owned local binding into a
32+// probe over an existing connector, delegating to the same
33+// newDynamicLocalRouteProbe production constructor brokerLocalProbe uses.
34+func newLocalRouteProbeWithConnector(binding brokerconfig.LocalBinding, epoch ports.BrokerEpoch, connector ports.BrokerEndpointConnector) (*localRouteProbe, error) {
35+ return newDynamicLocalRouteProbe(binding.Route, binding.Policy, epoch, connector, func() (ports.BrokerDaemonIdentity, error) {
36+ return binding.Identity, nil
37+ })
38+}
@@ -0,0 +1,38 @@
1+package app
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/vev/internal/adapters/brokerconfig"
8+ "github.com/bnema/vev/internal/adapters/daemonmux"
9+ "github.com/bnema/vev/internal/ports"
10+)
11+
12+// newLocalRouteProbe fences one broker-owned local binding into a probe over a
13+// dial seam. The binding's identity, policy, and route address must validate,
14+// the ceilings must be a valid advertisement, the epoch must be the broker
15+// process incarnation every local stream is scoped to, and a nil dial is
16+// refused: a probe that cannot establish a carriage must not exist. It builds
17+// its own endpoint connector; a caller that already owns the pooled connector
18+// uses newLocalRouteProbeWithConnector instead, exactly as brokerLocalProbe
19+// does in production.
20+func newLocalRouteProbe(binding brokerconfig.LocalBinding, epoch ports.BrokerEpoch, dial localCarrierDialer, ceilings daemonmux.MuxCeilings) (*localRouteProbe, error) {
21+ if dial == nil {
22+ return nil, errors.New("vev: broker local probe requires a dialer")
23+ }
24+ connector, err := daemonmux.NewEndpointConnector(daemonmux.RawCarrierDialer(dial), ceilings)
25+ if err != nil {
26+ return nil, fmt.Errorf("vev: broker local probe: %w", err)
27+ }
28+ return newLocalRouteProbeWithConnector(binding, epoch, connector)
29+}
30+
31+// newLocalRouteProbeWithConnector fences one broker-owned local binding into a
32+// probe over an existing connector, delegating to the same
33+// newDynamicLocalRouteProbe production constructor brokerLocalProbe uses.
34+func newLocalRouteProbeWithConnector(binding brokerconfig.LocalBinding, epoch ports.BrokerEpoch, connector ports.BrokerEndpointConnector) (*localRouteProbe, error) {
35+ return newDynamicLocalRouteProbe(binding.Route, binding.Policy, epoch, connector, func() (ports.BrokerDaemonIdentity, error) {
36+ return binding.Identity, nil
37+ })
38+}
M internal/app/run.go +0 -75
@@ -648,10 +648,6 @@ type lifecycleOwnership interface {
648648 Release() error
649649 }
650650
651-func joinLifecycleReleaseError(retErr *error, owner lifecycleOwnership) {
652- *retErr = errors.Join(*retErr, owner.Release())
653-}
654-
655651 type lifecycleStartupDeps struct {
656652 ensurePrivate func(string) error
657653 acquire func(context.Context, string, time.Duration) (lifecycleOwnership, error)
@@ -982,79 +978,8 @@ const daemonStopTimeout = 2 * time.Second
982978 var (
983979 errDaemonNotRunning = errors.New("vev: no daemon running")
984980 errKillOutcomeUnknown = errors.New("vev: kill outcome unknown")
985- // errKillNotSent reports a Kill that was never placed on the wire because
986- // its caller was already canceled. The outcome is definite: the daemon
987- // cannot have acted, so it must not be reported as an unknown outcome.
988- errKillNotSent = errors.New("vev: kill request not sent")
989981 )
990982
991-func newControlRequestID() (uint64, error) {
992- var raw [8]byte
993- if _, err := rand.Read(raw[:]); err != nil {
994- return 0, err
995- }
996- id := uint64(0)
997- for _, b := range raw {
998- id = id<<8 | uint64(b)
999- }
1000- if id == 0 {
1001- id = 1
1002- }
1003- return id, nil
1004-}
1005-
1006-// sendKillRequest allocates a unique nonzero RequestID and sends one Kill
1007-// request, returning the ID its matching KillResult must carry. A caller
1008-// already canceled before the send returns errKillNotSent (a definite
1009-// not-sent outcome) instead of sending a request whose reply could no longer be
1010-// awaited; the request is never replayed.
1011-func sendKillRequest(ctx context.Context, connection ports.ClientConnection, request protocol.Kill) (uint64, error) {
1012- requestID, err := newControlRequestID()
1013- if err != nil {
1014- return 0, fmt.Errorf("preparing request: %w", err)
1015- }
1016- request.RequestID = requestID
1017- if err := ctx.Err(); err != nil {
1018- return 0, fmt.Errorf("%w: %w", errKillNotSent, err)
1019- }
1020- if err := connection.SendClient(request); err != nil {
1021- return 0, err
1022- }
1023- return requestID, nil
1024-}
1025-
1026-// receiveKillResult consumes exactly one correlated KillResult from a control
1027-// connection. A definite failed result becomes a plain error; a close,
1028-// cancellation, or an uncorrelated reply becomes errKillOutcomeUnknown, so a
1029-// caller never treats a lost result as success and never replays the kill.
1030-func receiveKillResult(ctx context.Context, connection ports.ClientConnection, requestID uint64) error {
1031- // Cancellation after the request was sent closes the exact connection so a
1032- // blocked receive unwinds; the lost reply stays outcome-unknown rather than
1033- // becoming a silent success or a blind retry.
1034- stop := context.AfterFunc(ctx, func() { _ = connection.Close() })
1035- defer stop()
1036-
1037- reply, err := connection.ReceiveServer()
1038- if err != nil {
1039- if ctxErr := ctx.Err(); ctxErr != nil {
1040- return fmt.Errorf("%w: awaiting explicit result: %v", errKillOutcomeUnknown, ctxErr)
1041- }
1042- return fmt.Errorf("%w: awaiting explicit result: %v", errKillOutcomeUnknown, err)
1043- }
1044- result, ok := reply.(protocol.KillResult)
1045- if !ok || result.RequestID != requestID {
1046- return fmt.Errorf("%w: unexpected reply %T", errKillOutcomeUnknown, reply)
1047- }
1048- switch result.Outcome {
1049- case protocol.KillSucceeded:
1050- return nil
1051- case protocol.KillFailed:
1052- return fmt.Errorf("vev: %s", result.Text)
1053- default:
1054- return fmt.Errorf("%w: %s", errKillOutcomeUnknown, result.Text)
1055- }
1056-}
1057-
1058983 func createDetachedLocalSession(ctx context.Context, name string) error {
1059984 service, err := connectBroker(ctx)
1060985 if err != nil {
@@ -648,10 +648,6 @@ type lifecycleOwnership interface {
648 Release() error648 Release() error
649 }649 }
650 650
651-func joinLifecycleReleaseError(retErr *error, owner lifecycleOwnership) {
652- *retErr = errors.Join(*retErr, owner.Release())
653-}
654-
655 type lifecycleStartupDeps struct {651 type lifecycleStartupDeps struct {
656 ensurePrivate func(string) error652 ensurePrivate func(string) error
657 acquire func(context.Context, string, time.Duration) (lifecycleOwnership, error)653 acquire func(context.Context, string, time.Duration) (lifecycleOwnership, error)
@@ -982,79 +978,8 @@ const daemonStopTimeout = 2 * time.Second
982 var (978 var (
983 errDaemonNotRunning = errors.New("vev: no daemon running")979 errDaemonNotRunning = errors.New("vev: no daemon running")
984 errKillOutcomeUnknown = errors.New("vev: kill outcome unknown")980 errKillOutcomeUnknown = errors.New("vev: kill outcome unknown")
985- // errKillNotSent reports a Kill that was never placed on the wire because
986- // its caller was already canceled. The outcome is definite: the daemon
987- // cannot have acted, so it must not be reported as an unknown outcome.
988- errKillNotSent = errors.New("vev: kill request not sent")
989 )981 )
990 982
991-func newControlRequestID() (uint64, error) {
992- var raw [8]byte
993- if _, err := rand.Read(raw[:]); err != nil {
994- return 0, err
995- }
996- id := uint64(0)
997- for _, b := range raw {
998- id = id<<8 | uint64(b)
999- }
1000- if id == 0 {
1001- id = 1
1002- }
1003- return id, nil
1004-}
1005-
1006-// sendKillRequest allocates a unique nonzero RequestID and sends one Kill
1007-// request, returning the ID its matching KillResult must carry. A caller
1008-// already canceled before the send returns errKillNotSent (a definite
1009-// not-sent outcome) instead of sending a request whose reply could no longer be
1010-// awaited; the request is never replayed.
1011-func sendKillRequest(ctx context.Context, connection ports.ClientConnection, request protocol.Kill) (uint64, error) {
1012- requestID, err := newControlRequestID()
1013- if err != nil {
1014- return 0, fmt.Errorf("preparing request: %w", err)
1015- }
1016- request.RequestID = requestID
1017- if err := ctx.Err(); err != nil {
1018- return 0, fmt.Errorf("%w: %w", errKillNotSent, err)
1019- }
1020- if err := connection.SendClient(request); err != nil {
1021- return 0, err
1022- }
1023- return requestID, nil
1024-}
1025-
1026-// receiveKillResult consumes exactly one correlated KillResult from a control
1027-// connection. A definite failed result becomes a plain error; a close,
1028-// cancellation, or an uncorrelated reply becomes errKillOutcomeUnknown, so a
1029-// caller never treats a lost result as success and never replays the kill.
1030-func receiveKillResult(ctx context.Context, connection ports.ClientConnection, requestID uint64) error {
1031- // Cancellation after the request was sent closes the exact connection so a
1032- // blocked receive unwinds; the lost reply stays outcome-unknown rather than
1033- // becoming a silent success or a blind retry.
1034- stop := context.AfterFunc(ctx, func() { _ = connection.Close() })
1035- defer stop()
1036-
1037- reply, err := connection.ReceiveServer()
1038- if err != nil {
1039- if ctxErr := ctx.Err(); ctxErr != nil {
1040- return fmt.Errorf("%w: awaiting explicit result: %v", errKillOutcomeUnknown, ctxErr)
1041- }
1042- return fmt.Errorf("%w: awaiting explicit result: %v", errKillOutcomeUnknown, err)
1043- }
1044- result, ok := reply.(protocol.KillResult)
1045- if !ok || result.RequestID != requestID {
1046- return fmt.Errorf("%w: unexpected reply %T", errKillOutcomeUnknown, reply)
1047- }
1048- switch result.Outcome {
1049- case protocol.KillSucceeded:
1050- return nil
1051- case protocol.KillFailed:
1052- return fmt.Errorf("vev: %s", result.Text)
1053- default:
1054- return fmt.Errorf("%w: %s", errKillOutcomeUnknown, result.Text)
1055- }
1056-}
1057-
1058 func createDetachedLocalSession(ctx context.Context, name string) error {983 func createDetachedLocalSession(ctx context.Context, name string) error {
1059 service, err := connectBroker(ctx)984 service, err := connectBroker(ctx)
1060 if err != nil {985 if err != nil {
M internal/app/spawn.go +0 -43
@@ -190,13 +190,6 @@ func ensureTargetWithLifecycle[T any](ctx context.Context, lockDir string, dial
190190 return ensureTarget(ctx, lockDir, dial, spawn, cfg)
191191 }
192192
193-// ensureDaemon returns a transport to a running daemon, spawning one if
194-// necessary after ensureDaemonWithLifecycle has established lifecycle
195-// availability. Tests also exercise this lower-level spawn election directly.
196-func ensureDaemon(ctx context.Context, dir string, dial dialFunc, spawn spawnFunc, cfg backoffConfig) (wire.Transport, error) {
197- return ensureTarget(ctx, dir, func(ctx context.Context) (wire.Transport, error) { return dial(ctx, dir) }, spawn, cfg)
198-}
199-
200193 // ensureTarget is the shared spawn election: it dials once, then takes the
201194 // spawn lock in lockDir, and only the elected winner spawns. Every dialer —
202195 // the session transport and the daemonmux carriage — therefore observes the
@@ -265,12 +258,6 @@ func acquireSpawnLock(dir string) (release func(), acquired bool, err error) {
265258 return func() {}, false, nil
266259 }
267260
268-// retryDial dials repeatedly with exponential backoff until the daemon
269-// answers, the context is cancelled, or the total budget is exhausted.
270-func retryDial(ctx context.Context, dir string, dial dialFunc, cfg backoffConfig) (wire.Transport, error) {
271- return retryTarget(ctx, dir, func(ctx context.Context) (wire.Transport, error) { return dial(ctx, dir) }, cfg)
272-}
273-
274261 // retryTarget is the shared bounded redial: it repeats the same dial with
275262 // exponential backoff until it answers, the caller's context ends, or the total
276263 // budget is exhausted. It is generic so the daemonmux carriage retries under
@@ -343,36 +330,6 @@ func withoutPerformanceTraceEnv(env []string) []string {
343330 return filtered
344331 }
345332
346-// spawnDaemonWithEnvironment launches an explicitly selected daemon binary
347-// with the complete environment owned by a launch configuration. It retains
348-// the same double-fork boundary as realSpawn without invoking a shell or
349-// inheriting ambient variables.
350-func spawnDaemonWithEnvironment(executable string, environment []string) error {
351- if executable == "" {
352- return errors.New("vev: missing launch executable")
353- }
354- devNull, err := os.OpenFile(os.DevNull, os.O_RDWR, 0)
355- if err != nil {
356- return fmt.Errorf("opening %s: %w", os.DevNull, err)
357- }
358- defer func() { _ = devNull.Close() }()
359-
360- var stderr bytes.Buffer
361- cmd := exec.Command(executable, "--daemon-launcher")
362- cmd.Env = append([]string(nil), environment...)
363- cmd.Dir = platform.DirOrHome("")
364- cmd.Stdin = devNull
365- cmd.Stdout = devNull
366- cmd.Stderr = &stderr
367- if err := cmd.Run(); err != nil {
368- if detail := bytes.TrimSpace(stderr.Bytes()); len(detail) > 0 {
369- return fmt.Errorf("%w: %s", err, detail)
370- }
371- return err
372- }
373- return nil
374-}
375-
376333 // runDaemonLauncher is the intermediate half of the double-fork. It starts
377334 // the long-lived daemon in a new session and exits immediately; realSpawn
378335 // waits for this exit so the daemon is no longer descended from the client.
@@ -190,13 +190,6 @@ func ensureTargetWithLifecycle[T any](ctx context.Context, lockDir string, dial
190 return ensureTarget(ctx, lockDir, dial, spawn, cfg)190 return ensureTarget(ctx, lockDir, dial, spawn, cfg)
191 }191 }
192 192
193-// ensureDaemon returns a transport to a running daemon, spawning one if
194-// necessary after ensureDaemonWithLifecycle has established lifecycle
195-// availability. Tests also exercise this lower-level spawn election directly.
196-func ensureDaemon(ctx context.Context, dir string, dial dialFunc, spawn spawnFunc, cfg backoffConfig) (wire.Transport, error) {
197- return ensureTarget(ctx, dir, func(ctx context.Context) (wire.Transport, error) { return dial(ctx, dir) }, spawn, cfg)
198-}
199-
200 // ensureTarget is the shared spawn election: it dials once, then takes the193 // ensureTarget is the shared spawn election: it dials once, then takes the
201 // spawn lock in lockDir, and only the elected winner spawns. Every dialer —194 // spawn lock in lockDir, and only the elected winner spawns. Every dialer —
202 // the session transport and the daemonmux carriage — therefore observes the195 // the session transport and the daemonmux carriage — therefore observes the
@@ -265,12 +258,6 @@ func acquireSpawnLock(dir string) (release func(), acquired bool, err error) {
265 return func() {}, false, nil258 return func() {}, false, nil
266 }259 }
267 260
268-// retryDial dials repeatedly with exponential backoff until the daemon
269-// answers, the context is cancelled, or the total budget is exhausted.
270-func retryDial(ctx context.Context, dir string, dial dialFunc, cfg backoffConfig) (wire.Transport, error) {
271- return retryTarget(ctx, dir, func(ctx context.Context) (wire.Transport, error) { return dial(ctx, dir) }, cfg)
272-}
273-
274 // retryTarget is the shared bounded redial: it repeats the same dial with261 // retryTarget is the shared bounded redial: it repeats the same dial with
275 // exponential backoff until it answers, the caller's context ends, or the total262 // exponential backoff until it answers, the caller's context ends, or the total
276 // budget is exhausted. It is generic so the daemonmux carriage retries under263 // budget is exhausted. It is generic so the daemonmux carriage retries under
@@ -343,36 +330,6 @@ func withoutPerformanceTraceEnv(env []string) []string {
343 return filtered330 return filtered
344 }331 }
345 332
346-// spawnDaemonWithEnvironment launches an explicitly selected daemon binary
347-// with the complete environment owned by a launch configuration. It retains
348-// the same double-fork boundary as realSpawn without invoking a shell or
349-// inheriting ambient variables.
350-func spawnDaemonWithEnvironment(executable string, environment []string) error {
351- if executable == "" {
352- return errors.New("vev: missing launch executable")
353- }
354- devNull, err := os.OpenFile(os.DevNull, os.O_RDWR, 0)
355- if err != nil {
356- return fmt.Errorf("opening %s: %w", os.DevNull, err)
357- }
358- defer func() { _ = devNull.Close() }()
359-
360- var stderr bytes.Buffer
361- cmd := exec.Command(executable, "--daemon-launcher")
362- cmd.Env = append([]string(nil), environment...)
363- cmd.Dir = platform.DirOrHome("")
364- cmd.Stdin = devNull
365- cmd.Stdout = devNull
366- cmd.Stderr = &stderr
367- if err := cmd.Run(); err != nil {
368- if detail := bytes.TrimSpace(stderr.Bytes()); len(detail) > 0 {
369- return fmt.Errorf("%w: %s", err, detail)
370- }
371- return err
372- }
373- return nil
374-}
375-
376 // runDaemonLauncher is the intermediate half of the double-fork. It starts333 // runDaemonLauncher is the intermediate half of the double-fork. It starts
377 // the long-lived daemon in a new session and exits immediately; realSpawn334 // the long-lived daemon in a new session and exits immediately; realSpawn
378 // waits for this exit so the daemon is no longer descended from the client.335 // waits for this exit so the daemon is no longer descended from the client.
M internal/app/spawn_test.go +0 -151
@@ -12,9 +12,6 @@ import (
1212 "syscall"
1313 "testing"
1414 "time"
15-
16- "github.com/bnema/vev/internal/protocol/wire"
17- wiremocks "github.com/bnema/vev/internal/protocol/wire/mocks"
1815 )
1916
2017 var errDialFailed = errors.New("dial failed")
@@ -347,151 +344,3 @@ func TestRetryAttemptsClampsNonPositiveInitialDelay(t *testing.T) {
347344 t.Fatalf("retryAttempts retried after %s, want a positive initial delay", elapsed)
348345 }
349346 }
350-
351-func TestRetryDialSucceedsAfterTransientFailures(t *testing.T) {
352- want := wiremocks.NewMockTransport(t)
353- var calls atomic.Int32
354- dial := func(context.Context, string) (wire.Transport, error) {
355- if calls.Add(1) <= 3 {
356- return nil, errDialFailed
357- }
358- return want, nil
359- }
360-
361- got, err := retryDial(context.Background(), t.TempDir(), dial, fastBackoff)
362- if err != nil {
363- t.Fatalf("retryDial: %v", err)
364- }
365- if got != want {
366- t.Fatalf("retryDial returned unexpected transport")
367- }
368- if calls.Load() != 4 {
369- t.Fatalf("dial called %d times, want 4", calls.Load())
370- }
371-}
372-
373-func TestRetryDialPermanentFailure(t *testing.T) {
374- dial := func(context.Context, string) (wire.Transport, error) { return nil, errDialFailed }
375- _, err := retryDial(context.Background(), t.TempDir(), dial, fastBackoff)
376- if !errors.Is(err, ErrDaemonUnreachable) {
377- t.Fatalf("retryDial error = %v, want ErrDaemonUnreachable", err)
378- }
379-}
380-
381-func TestRetryDialContextCancelled(t *testing.T) {
382- ctx, cancel := context.WithCancel(context.Background())
383- cancel()
384- var calls atomic.Int32
385- dial := func(context.Context, string) (wire.Transport, error) {
386- calls.Add(1)
387- return nil, errDialFailed
388- }
389- _, err := retryDial(ctx, t.TempDir(), dial, fastBackoff)
390- if !errors.Is(err, context.Canceled) {
391- t.Fatalf("retryDial error = %v, want context.Canceled", err)
392- }
393- if calls.Load() != 0 {
394- t.Fatalf("dial called %d times for canceled context, want 0", calls.Load())
395- }
396-}
397-
398-func TestEnsureDaemonContextCancelledDoesNotDialOrSpawn(t *testing.T) {
399- ctx, cancel := context.WithCancel(context.Background())
400- cancel()
401- dial := func(context.Context, string) (wire.Transport, error) {
402- t.Fatal("ensureDaemon dialed with a canceled context")
403- return nil, nil
404- }
405- spawn := func() error {
406- t.Fatal("ensureDaemon spawned with a canceled context")
407- return nil
408- }
409-
410- _, err := ensureDaemon(ctx, t.TempDir(), dial, spawn, fastBackoff)
411- if !errors.Is(err, context.Canceled) {
412- t.Fatalf("ensureDaemon error = %v, want context.Canceled", err)
413- }
414-}
415-
416-func TestEnsureDaemonDoesNotSpawnWhenContextCancelsAfterFailedDial(t *testing.T) {
417- ctx, cancel := context.WithCancel(context.Background())
418- defer cancel()
419- var spawnCalls atomic.Int32
420- dial := func(context.Context, string) (wire.Transport, error) {
421- cancel()
422- return nil, errDialFailed
423- }
424- spawn := func() error {
425- spawnCalls.Add(1)
426- return nil
427- }
428-
429- _, err := ensureDaemon(ctx, filepath.Join(t.TempDir(), "vev"), dial, spawn, fastBackoff)
430- if !errors.Is(err, context.Canceled) {
431- t.Fatalf("ensureDaemon error = %v, want context.Canceled", err)
432- }
433- if got := spawnCalls.Load(); got != 0 {
434- t.Fatalf("spawn called %d times after canceled failed dial, want 0", got)
435- }
436-}
437-
438-func TestEnsureDaemonSpawnsThenDials(t *testing.T) {
439- dir := filepath.Join(t.TempDir(), "vev")
440- want := wiremocks.NewMockTransport(t)
441- var dialCalls, spawnCalls atomic.Int32
442- dial := func(context.Context, string) (wire.Transport, error) {
443- // First (pre-spawn) dial fails; the post-spawn retry succeeds.
444- if dialCalls.Add(1) == 1 {
445- return nil, errDialFailed
446- }
447- return want, nil
448- }
449- spawn := func() error {
450- spawnCalls.Add(1)
451- return nil
452- }
453-
454- got, err := ensureDaemon(context.Background(), dir, dial, spawn, fastBackoff)
455- if err != nil {
456- t.Fatalf("ensureDaemon: %v", err)
457- }
458- if got != want {
459- t.Fatalf("ensureDaemon returned unexpected transport")
460- }
461- if spawnCalls.Load() != 1 {
462- t.Fatalf("spawn called %d times, want 1", spawnCalls.Load())
463- }
464- // The spawn lock must be released once ensureDaemon returns.
465- if _, err := os.Stat(filepath.Join(dir, spawnLockName)); !os.IsNotExist(err) {
466- t.Fatalf("spawn lock not released: stat err=%v", err)
467- }
468-}
469-
470-func TestEnsureDaemonReturnsExistingDaemon(t *testing.T) {
471- want := wiremocks.NewMockTransport(t)
472- var spawnCalls atomic.Int32
473- dial := func(context.Context, string) (wire.Transport, error) { return want, nil }
474- spawn := func() error { spawnCalls.Add(1); return nil }
475-
476- got, err := ensureDaemon(context.Background(), t.TempDir(), dial, spawn, fastBackoff)
477- if err != nil {
478- t.Fatalf("ensureDaemon: %v", err)
479- }
480- if got != want {
481- t.Fatalf("ensureDaemon returned unexpected transport")
482- }
483- if spawnCalls.Load() != 0 {
484- t.Fatalf("spawn should not be called when daemon already up, got %d", spawnCalls.Load())
485- }
486-}
487-
488-func TestEnsureDaemonSpawnFailure(t *testing.T) {
489- spawnErr := errors.New("exec boom")
490- dial := func(context.Context, string) (wire.Transport, error) { return nil, errDialFailed }
491- spawn := func() error { return spawnErr }
492-
493- _, err := ensureDaemon(context.Background(), filepath.Join(t.TempDir(), "vev"), dial, spawn, fastBackoff)
494- if !errors.Is(err, spawnErr) {
495- t.Fatalf("ensureDaemon error = %v, want wrapped %v", err, spawnErr)
496- }
497-}
@@ -12,9 +12,6 @@ import (
12 "syscall"12 "syscall"
13 "testing"13 "testing"
14 "time"14 "time"
15-
16- "github.com/bnema/vev/internal/protocol/wire"
17- wiremocks "github.com/bnema/vev/internal/protocol/wire/mocks"
18 )15 )
19 16
20 var errDialFailed = errors.New("dial failed")17 var errDialFailed = errors.New("dial failed")
@@ -347,151 +344,3 @@ func TestRetryAttemptsClampsNonPositiveInitialDelay(t *testing.T) {
347 t.Fatalf("retryAttempts retried after %s, want a positive initial delay", elapsed)344 t.Fatalf("retryAttempts retried after %s, want a positive initial delay", elapsed)
348 }345 }
349 }346 }
350-
351-func TestRetryDialSucceedsAfterTransientFailures(t *testing.T) {
352- want := wiremocks.NewMockTransport(t)
353- var calls atomic.Int32
354- dial := func(context.Context, string) (wire.Transport, error) {
355- if calls.Add(1) <= 3 {
356- return nil, errDialFailed
357- }
358- return want, nil
359- }
360-
361- got, err := retryDial(context.Background(), t.TempDir(), dial, fastBackoff)
362- if err != nil {
363- t.Fatalf("retryDial: %v", err)
364- }
365- if got != want {
366- t.Fatalf("retryDial returned unexpected transport")
367- }
368- if calls.Load() != 4 {
369- t.Fatalf("dial called %d times, want 4", calls.Load())
370- }
371-}
372-
373-func TestRetryDialPermanentFailure(t *testing.T) {
374- dial := func(context.Context, string) (wire.Transport, error) { return nil, errDialFailed }
375- _, err := retryDial(context.Background(), t.TempDir(), dial, fastBackoff)
376- if !errors.Is(err, ErrDaemonUnreachable) {
377- t.Fatalf("retryDial error = %v, want ErrDaemonUnreachable", err)
378- }
379-}
380-
381-func TestRetryDialContextCancelled(t *testing.T) {
382- ctx, cancel := context.WithCancel(context.Background())
383- cancel()
384- var calls atomic.Int32
385- dial := func(context.Context, string) (wire.Transport, error) {
386- calls.Add(1)
387- return nil, errDialFailed
388- }
389- _, err := retryDial(ctx, t.TempDir(), dial, fastBackoff)
390- if !errors.Is(err, context.Canceled) {
391- t.Fatalf("retryDial error = %v, want context.Canceled", err)
392- }
393- if calls.Load() != 0 {
394- t.Fatalf("dial called %d times for canceled context, want 0", calls.Load())
395- }
396-}
397-
398-func TestEnsureDaemonContextCancelledDoesNotDialOrSpawn(t *testing.T) {
399- ctx, cancel := context.WithCancel(context.Background())
400- cancel()
401- dial := func(context.Context, string) (wire.Transport, error) {
402- t.Fatal("ensureDaemon dialed with a canceled context")
403- return nil, nil
404- }
405- spawn := func() error {
406- t.Fatal("ensureDaemon spawned with a canceled context")
407- return nil
408- }
409-
410- _, err := ensureDaemon(ctx, t.TempDir(), dial, spawn, fastBackoff)
411- if !errors.Is(err, context.Canceled) {
412- t.Fatalf("ensureDaemon error = %v, want context.Canceled", err)
413- }
414-}
415-
416-func TestEnsureDaemonDoesNotSpawnWhenContextCancelsAfterFailedDial(t *testing.T) {
417- ctx, cancel := context.WithCancel(context.Background())
418- defer cancel()
419- var spawnCalls atomic.Int32
420- dial := func(context.Context, string) (wire.Transport, error) {
421- cancel()
422- return nil, errDialFailed
423- }
424- spawn := func() error {
425- spawnCalls.Add(1)
426- return nil
427- }
428-
429- _, err := ensureDaemon(ctx, filepath.Join(t.TempDir(), "vev"), dial, spawn, fastBackoff)
430- if !errors.Is(err, context.Canceled) {
431- t.Fatalf("ensureDaemon error = %v, want context.Canceled", err)
432- }
433- if got := spawnCalls.Load(); got != 0 {
434- t.Fatalf("spawn called %d times after canceled failed dial, want 0", got)
435- }
436-}
437-
438-func TestEnsureDaemonSpawnsThenDials(t *testing.T) {
439- dir := filepath.Join(t.TempDir(), "vev")
440- want := wiremocks.NewMockTransport(t)
441- var dialCalls, spawnCalls atomic.Int32
442- dial := func(context.Context, string) (wire.Transport, error) {
443- // First (pre-spawn) dial fails; the post-spawn retry succeeds.
444- if dialCalls.Add(1) == 1 {
445- return nil, errDialFailed
446- }
447- return want, nil
448- }
449- spawn := func() error {
450- spawnCalls.Add(1)
451- return nil
452- }
453-
454- got, err := ensureDaemon(context.Background(), dir, dial, spawn, fastBackoff)
455- if err != nil {
456- t.Fatalf("ensureDaemon: %v", err)
457- }
458- if got != want {
459- t.Fatalf("ensureDaemon returned unexpected transport")
460- }
461- if spawnCalls.Load() != 1 {
462- t.Fatalf("spawn called %d times, want 1", spawnCalls.Load())
463- }
464- // The spawn lock must be released once ensureDaemon returns.
465- if _, err := os.Stat(filepath.Join(dir, spawnLockName)); !os.IsNotExist(err) {
466- t.Fatalf("spawn lock not released: stat err=%v", err)
467- }
468-}
469-
470-func TestEnsureDaemonReturnsExistingDaemon(t *testing.T) {
471- want := wiremocks.NewMockTransport(t)
472- var spawnCalls atomic.Int32
473- dial := func(context.Context, string) (wire.Transport, error) { return want, nil }
474- spawn := func() error { spawnCalls.Add(1); return nil }
475-
476- got, err := ensureDaemon(context.Background(), t.TempDir(), dial, spawn, fastBackoff)
477- if err != nil {
478- t.Fatalf("ensureDaemon: %v", err)
479- }
480- if got != want {
481- t.Fatalf("ensureDaemon returned unexpected transport")
482- }
483- if spawnCalls.Load() != 0 {
484- t.Fatalf("spawn should not be called when daemon already up, got %d", spawnCalls.Load())
485- }
486-}
487-
488-func TestEnsureDaemonSpawnFailure(t *testing.T) {
489- spawnErr := errors.New("exec boom")
490- dial := func(context.Context, string) (wire.Transport, error) { return nil, errDialFailed }
491- spawn := func() error { return spawnErr }
492-
493- _, err := ensureDaemon(context.Background(), filepath.Join(t.TempDir(), "vev"), dial, spawn, fastBackoff)
494- if !errors.Is(err, spawnErr) {
495- t.Fatalf("ensureDaemon error = %v, want wrapped %v", err, spawnErr)
496- }
497-}

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