1package app
2
3import (
4 "context"
5 "errors"
6 "io"
7 "os"
8 "path/filepath"
9 "sync"
10 "sync/atomic"
11 "testing"
12 "time"
13
14 "github.com/stretchr/testify/require"
15
16 "github.com/bnema/vev/internal/adapters/brokerconfig"
17 "github.com/bnema/vev/internal/adapters/daemonmux"
18 "github.com/bnema/vev/internal/ports"
19 "github.com/bnema/vev/internal/protocol/wire"
20)
21
22// Broker-owned daemon start coverage (Plan 001 P7, UI-driver slice 1).
23//
24// These tests drive the real start decision — the closed start authorization,
25// the launch policy, the shared election, and the carriage dial — through the
26// scripted starter seams. Nothing here re-executes a daemon, binds a socket, or
27// reads a session: every assertion is about whether a spawn was elected and
28// which carriage was dialed, which is exactly the contract the broker owns.
29
30// stubRawCarriage is one inert raw framed carriage: enough to satisfy the dial
31// seam without a socket, a peer, or a goroutine.
32type stubRawCarriage struct{ closed atomic.Bool }
33
34func (c *stubRawCarriage) Send(wire.Envelope) error { return nil }
35
36func (c *stubRawCarriage) RecvBounded(uint64) (wire.Envelope, error) { return wire.Envelope{}, io.EOF }
37
38func (c *stubRawCarriage) Close() error {
39 c.closed.Store(true)
40 return nil
41}
42
43// scriptedDaemonStarter is a goroutine-safe recording starter: it counts dials
44// and spawns, keeps the exact target each dial received, and fails every dial
45// until the scripted spawn (or the test) makes the carriage available.
46type scriptedDaemonStarter struct {
47 mu sync.Mutex
48 carriage string
49 // published is the carriage the scripted daemon advertises as the endpoint
50 // this process can start. Empty means the same carriage it answers at.
51 published string
52 dialed []ports.BrokerDialTarget
53 dials int
54 spawns int
55 // running reports that the daemon behind the carriage is published, as the
56 // elected spawner's daemon is once it binds its endpoint.
57 running bool
58 // available reports whether the carriage answers. A nil func means "answer
59 // only once a spawn happened".
60 available func() bool
61 // dialErr overrides the failure a dial reports while unavailable.
62 dialErr error
63 // spawnErr fails the elected spawn.
64 spawnErr error
65 // daemonOwner is the lifecycle ownership the spawned daemon holds, exactly
66 // as the real daemon does for its whole run. It is what makes a concurrent
67 // second caller wait instead of electing itself.
68 daemonOwner lifecycleOwnership
69}
70
71func (s *scriptedDaemonStarter) starter(backoff backoffConfig) brokerDaemonStarter {
72 return brokerDaemonStarter{
73 dial: s.dial,
74 spawn: s.spawn,
75 backoff: backoff,
76 // The scripted daemon publishes exactly the carriage the test declared,
77 // so the start path sees one reachable daemon endpoint like production.
78 published: func() string { return s.publishedCarriage() },
79 }
80}
81
82func (s *scriptedDaemonStarter) dial(_ context.Context, carriage string) (daemonmux.RawFramedTransport, error) {
83 s.mu.Lock()
84 defer s.mu.Unlock()
85 s.dials++
86 s.dialed = append(s.dialed, ports.BrokerDialTarget{Address: carriage})
87 if s.carriage != carriage {
88 return nil, os.ErrNotExist
89 }
90 if s.available == nil {
91 if s.spawns == 0 && !s.running {
92 return nil, os.ErrNotExist
93 }
94 } else if !s.available() {
95 if s.dialErr != nil {
96 return nil, s.dialErr
97 }
98 return nil, os.ErrNotExist
99 }
100 return &stubRawCarriage{}, nil
101}
102
103func (s *scriptedDaemonStarter) spawn() error {
104 s.mu.Lock()
105 defer s.mu.Unlock()
106 s.spawns++
107 if s.spawnErr != nil {
108 return s.spawnErr
109 }
110 // A real spawned daemon immediately takes lifecycle ownership of the
111 // runtime directory and holds it for its whole run; a concurrent caller
112 // therefore observes ErrBusy and waits for the carriage rather than
113 // electing a second spawner.
114 lockDir := filepath.Dir(s.publishedCarriageLocked())
115 if owner, err := daemonLifecycleProbe.TryAcquire(lockDir); err == nil {
116 s.daemonOwner = owner
117 }
118 return nil
119}
120
121// markRunning publishes the daemon the elected spawner started, so the callers
122// waiting on the held election converge on its carriage.
123func (s *scriptedDaemonStarter) markRunning() {
124 s.mu.Lock()
125 defer s.mu.Unlock()
126 s.running = true
127}
128
129// isRunning reports whether the daemon is published.
130func (s *scriptedDaemonStarter) isRunning() bool {
131 s.mu.Lock()
132 defer s.mu.Unlock()
133 return s.running
134}
135
136// publishedCarriageLocked reports the published carriage while the mutex is
137// already held.
138func (s *scriptedDaemonStarter) publishedCarriageLocked() string {
139 if s.published != "" {
140 return s.published
141 }
142 return s.carriage
143}
144
145// publishedCarriage reports the carriage the scripted daemon publishes as the
146// endpoint this process can start.
147func (s *scriptedDaemonStarter) publishedCarriage() string {
148 s.mu.Lock()
149 defer s.mu.Unlock()
150 return s.publishedCarriageLocked()
151}
152
153func (s *scriptedDaemonStarter) counters() (dials, spawns int) {
154 s.mu.Lock()
155 defer s.mu.Unlock()
156 return s.dials, s.spawns
157}
158
159// fastDaemonStartBackoff bounds the election the way an unresponsive daemon
160// would, without the production five-second budget.
161var fastDaemonStartBackoff = backoffConfig{initial: time.Millisecond, max: 2 * time.Millisecond, total: 150 * time.Millisecond}
162
163// withDaemonStarter swaps the package start seam for one test and restores it.
164// The start seam is process state, so no caller of this helper runs in parallel.
165func withDaemonStarter(t *testing.T, starter brokerDaemonStarter) {
166 t.Helper()
167 restore := localDaemonStarter
168 localDaemonStarter = starter
169 t.Cleanup(func() { localDaemonStarter = restore })
170}
171
172// testDaemonCarriage returns a fresh owner-only daemon runtime directory and the
173// exact private carriage a daemon bound to it publishes. It is the carriage the
174// scripted starter declares, so the start path sees one reachable endpoint.
175func testDaemonCarriage(t *testing.T) (dir, carriage string) {
176 t.Helper()
177 dir = t.TempDir()
178 // The lifecycle lock refuses a directory with group or other access, and
179 // t.TempDir() is 0755; the daemon runtime directory is owner-only.
180 require.NoError(t, os.Chmod(dir, 0o700))
181 return dir, daemonmux.SocketPath(dir)
182}
183
184// startableLocalTarget builds the resolved local target the broker publishes for
185// its own machine daemon under the supplied start authorization. The policy is
186// the production local daemon authority, which explicitly names the launch
187// token; a test that needs a non-launching policy overrides it. The address is
188// the bounded token the resolver would publish for the provisioned carriage.
189func startableLocalTarget(t *testing.T, carriage string, mode ports.BrokerDaemonStartMode) ports.BrokerDialTarget {
190 t.Helper()
191 _, binding := testLocalSandboxConfigForCarriage(t, carriage)
192 target := ports.BrokerDialTarget{
193 Fence: ports.BrokerEndpointFence{Local: true},
194 Address: binding.Route.Address(),
195 Policy: localDaemonPolicy(),
196 StartMode: mode,
197 ExpectedIdentity: ports.BrokerExpectedIdentity{Identity: brokerLocalTestIdentity, Bound: true},
198 }
199 require.NoError(t, target.Validate())
200 return target
201}
202
203// TestDaemonStartExistingOnlyNeverSpawns proves an existing-only acquisition is
204// exactly one carriage dial: no lifecycle probe, no spawn lock, no process, and
205// no fallback of any kind, even though the carriage is absent.
206func TestDaemonStartExistingOnlyNeverSpawns(t *testing.T) {
207 isolateSandboxEnv(t)
208 dir, carriage := testDaemonCarriage(t)
209 scripted := &scriptedDaemonStarter{carriage: carriage}
210 withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
211
212 target := startableLocalTarget(t, carriage, ports.BrokerDaemonExistingOnly)
213 raw, err := dialBrokerLocalDaemon(context.Background(), carriage, target)
214 require.ErrorIs(t, err, os.ErrNotExist)
215 var brokerErr ports.BrokerError
216 require.ErrorAs(t, err, &brokerErr)
217 require.Equal(t, ports.BrokerErrorNoDaemon, brokerErr.Code)
218 require.Nil(t, raw)
219
220 dials, spawns := scripted.counters()
221 require.Equal(t, 1, dials, "existing-only is exactly one carriage dial")
222 require.Zero(t, spawns, "existing-only must never spawn")
223 _, statErr := os.Stat(filepath.Join(dir, spawnLockName))
224 require.ErrorIs(t, statErr, os.ErrNotExist, "existing-only must not take the spawn lock")
225 _, probeErr := os.Stat(filepath.Join(dir, "lifecycle.lock"))
226 require.ErrorIs(t, probeErr, os.ErrNotExist, "existing-only must not probe lifecycle ownership")
227}
228
229// TestDaemonStartExistingOnlyReturnsRunningDaemonWithoutProbing proves a
230// reachable carriage is the whole result: the running daemon is never restarted
231// and no election is entered.
232func TestDaemonStartExistingOnlyReturnsRunningDaemonWithoutProbing(t *testing.T) {
233 isolateSandboxEnv(t)
234 dir, carriage := testDaemonCarriage(t)
235 scripted := &scriptedDaemonStarter{carriage: carriage, available: func() bool { return true }}
236 withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
237
238 target := startableLocalTarget(t, carriage, ports.BrokerDaemonExistingOnly)
239 raw, err := dialBrokerLocalDaemon(context.Background(), carriage, target)
240 require.NoError(t, err)
241 require.NotNil(t, raw)
242
243 // A start-if-needed acquisition against the same running daemon also
244 // returns it unchanged: the mode authorizes a start, it never demands one.
245 startIfNeeded := startableLocalTarget(t, carriage, ports.BrokerDaemonStartIfNeeded)
246 rawStartIfNeeded, err := dialBrokerLocalDaemon(context.Background(), carriage, startIfNeeded)
247 require.NoError(t, err)
248 require.NotNil(t, rawStartIfNeeded)
249
250 dials, spawns := scripted.counters()
251 require.Equal(t, 2, dials)
252 require.Zero(t, spawns, "a running daemon is never restarted")
253 _, probeErr := os.Stat(filepath.Join(dir, "lifecycle.lock"))
254 require.ErrorIs(t, probeErr, os.ErrNotExist, "a reachable carriage never enters the election")
255}
256
257// TestDaemonStartIfNeededSpawnsExactlyOnce proves the authorized start path
258// elects one spawner, then returns the carriage the spawned daemon published.
259func TestDaemonStartIfNeededSpawnsExactlyOnce(t *testing.T) {
260 isolateSandboxEnv(t)
261 dir, carriage := testDaemonCarriage(t)
262 scripted := &scriptedDaemonStarter{carriage: carriage}
263 withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
264
265 target := startableLocalTarget(t, carriage, ports.BrokerDaemonStartIfNeeded)
266 raw, err := dialBrokerLocalDaemon(context.Background(), carriage, target)
267 require.NoError(t, err)
268 require.NotNil(t, raw)
269
270 dials, spawns := scripted.counters()
271 require.GreaterOrEqual(t, dials, 2, "the start path dials before and after the spawn")
272 require.Equal(t, 1, spawns)
273 // The election released the spawn lock.
274 require.NoFileExists(t, filepath.Join(dir, spawnLockName))
275}
276
277// TestDaemonStartRefusesPolicyWithoutLaunchAuthority proves the closed start
278// authorization can only narrow the resolved policy: a policy that does not name
279// the broker launch authority refuses the start outright, and nothing spawns.
280func TestDaemonStartRefusesPolicyWithoutLaunchAuthority(t *testing.T) {
281 isolateSandboxEnv(t)
282 dir, carriage := testDaemonCarriage(t)
283 scripted := &scriptedDaemonStarter{carriage: carriage}
284 withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
285
286 target := startableLocalTarget(t, carriage, ports.BrokerDaemonStartIfNeeded)
287 target.Policy.Launch = "sandbox-private"
288 require.NoError(t, target.Validate())
289
290 raw, err := dialBrokerLocalDaemon(context.Background(), carriage, target)
291 require.Error(t, err)
292 require.Nil(t, raw)
293 var typed ports.BrokerError
294 require.ErrorAs(t, err, &typed)
295 require.Equal(t, ports.BrokerErrorConflictingPolicy, typed.Code)
296
297 dials, spawns := scripted.counters()
298 require.Equal(t, 1, dials, "the carriage is dialed once before the refusal")
299 require.Zero(t, spawns, "a non-launching policy must never spawn")
300 require.NoFileExists(t, filepath.Join(dir, spawnLockName))
301}
302
303// TestDaemonStartRefusesRegisteredEndpoint proves the local start mechanics
304// refuse a registered endpoint even on a local carriage: the broker owns the
305// start authority of its own machine daemon only.
306func TestDaemonStartRefusesRegisteredEndpoint(t *testing.T) {
307 isolateSandboxEnv(t)
308 _, carriage := testDaemonCarriage(t)
309 scripted := &scriptedDaemonStarter{carriage: carriage}
310 withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
311
312 target := startableLocalTarget(t, carriage, ports.BrokerDaemonStartIfNeeded)
313 target.Fence = ports.BrokerEndpointFence{Registration: brokerTestRegistration()}
314 require.NoError(t, target.Validate())
315
316 _, err := dialBrokerLocalDaemon(context.Background(), carriage, target)
317 require.Error(t, err)
318 dials, spawns := scripted.counters()
319 require.Zero(t, dials, "a registered endpoint is refused before any dial")
320 require.Zero(t, spawns)
321}
322
323// TestDaemonStartRefusesCarriageOutsideTheProvisionedRoute proves a carriage that
324// is not an absolute cleaned private mux path is refused before any dial, so a
325// hand-built target can never make the launcher start a daemon for a foreign
326// endpoint.
327func TestDaemonStartRefusesCarriageOutsideTheProvisionedRoute(t *testing.T) {
328 isolateSandboxEnv(t)
329 _, carriage := testDaemonCarriage(t)
330 scripted := &scriptedDaemonStarter{carriage: carriage}
331 withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
332
333 target := startableLocalTarget(t, carriage, ports.BrokerDaemonStartIfNeeded)
334 for _, invalid := range []string{"", "relative/daemonmux.sock", carriage + "/.."} {
335 _, err := dialBrokerLocalDaemon(context.Background(), invalid, target)
336 require.Error(t, err)
337 var typed ports.BrokerError
338 require.ErrorAs(t, err, &typed)
339 require.Equal(t, ports.BrokerErrorIncompatible, typed.Code)
340 }
341 dials, spawns := scripted.counters()
342 require.Zero(t, dials)
343 require.Zero(t, spawns)
344}
345
346// TestDaemonStartRefusesForeignCarriageStart proves a start is refused for a
347// carriage this process cannot publish: the launcher re-executes this binary,
348// which binds its own private endpoint, so starting a daemon for a foreign
349// carriage would spawn a process the caller could never reach.
350func TestDaemonStartRefusesForeignCarriageStart(t *testing.T) {
351 isolateSandboxEnv(t)
352 _, own := testDaemonCarriage(t)
353 foreignDir := t.TempDir()
354 require.NoError(t, os.Chmod(foreignDir, 0o700))
355 foreign := filepath.Join(foreignDir, "daemonmux.sock")
356
357 // The scripted daemon publishes and answers at the process' own carriage;
358 // the dial target names a different one.
359 scripted := &scriptedDaemonStarter{carriage: own, published: own}
360 withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
361
362 target := startableLocalTarget(t, foreign, ports.BrokerDaemonStartIfNeeded)
363 _, err := dialBrokerDaemonCarriage(context.Background(), foreign, target)
364 require.Error(t, err)
365 var typed ports.BrokerError
366 require.ErrorAs(t, err, &typed)
367 require.Equal(t, ports.BrokerErrorIncompatible, typed.Code)
368
369 dials, spawns := scripted.counters()
370 require.Equal(t, 1, dials, "the carriage is dialed once before the refusal")
371 require.Zero(t, spawns, "a foreign carriage must never be started")
372 require.NoFileExists(t, filepath.Join(filepath.Dir(own), spawnLockName))
373}
374
375// TestDaemonStartForeignCarriageWinsOverPolicyRefusal pins the refusal order:
376// an absent daemon on a foreign carriage is incompatible even when the policy
377// also forbids launching.
378func TestDaemonStartForeignCarriageWinsOverPolicyRefusal(t *testing.T) {
379 isolateSandboxEnv(t)
380 _, own := testDaemonCarriage(t)
381 foreignDir := t.TempDir()
382 require.NoError(t, os.Chmod(foreignDir, 0o700))
383 foreign := filepath.Join(foreignDir, "daemonmux.sock")
384 scripted := &scriptedDaemonStarter{carriage: own, published: own}
385 withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
386
387 target := startableLocalTarget(t, foreign, ports.BrokerDaemonStartIfNeeded)
388 target.Policy.Launch = "sandbox-private"
389 _, err := dialBrokerDaemonCarriage(context.Background(), foreign, target)
390 var typed ports.BrokerError
391 require.ErrorAs(t, err, &typed)
392 require.Equal(t, ports.BrokerErrorIncompatible, typed.Code)
393 _, spawns := scripted.counters()
394 require.Zero(t, spawns)
395}
396
397// TestDaemonStartDoesNotSpawnOnNonAbsenceFailure proves only proven absence can
398// authorize a start: a permission, path, peer, or cancellation failure is
399// reported unchanged and never repaired by spawning a second daemon.
400func TestDaemonStartDoesNotSpawnOnNonAbsenceFailure(t *testing.T) {
401 tests := []struct {
402 name string
403 err error
404 }{
405 {name: "permission", err: os.ErrPermission},
406 {name: "context canceled", err: context.Canceled},
407 {name: "invalid path", err: os.ErrInvalid},
408 {name: "unclassified", err: errors.New("carriage refused")},
409 }
410 for _, tt := range tests {
411 t.Run(tt.name, func(t *testing.T) {
412 isolateSandboxEnv(t)
413 dir, carriage := testDaemonCarriage(t)
414 scripted := &scriptedDaemonStarter{
415 carriage: carriage,
416 available: func() bool { return false },
417 dialErr: tt.err,
418 }
419 withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
420
421 target := startableLocalTarget(t, carriage, ports.BrokerDaemonStartIfNeeded)
422 _, err := dialBrokerLocalDaemon(context.Background(), carriage, target)
423 require.ErrorIs(t, err, tt.err)
424
425 dials, spawns := scripted.counters()
426 require.Equal(t, 1, dials)
427 require.Zero(t, spawns)
428 require.NoFileExists(t, filepath.Join(dir, spawnLockName))
429 })
430 }
431}
432
433// TestDaemonStartConcurrentRequestsShareOneElection proves N concurrent
434// start-if-needed requests never elect a second spawner while one election is
435// already in progress: the spawn lock is held by the elected spawner, so every
436// other caller waits for the carriage it publishes instead of spawning again.
437//
438// The spawn lock is taken here exactly as another client's elected spawner
439// would take it, and the carriage becomes dialable once that "spawner" finishes.
440// The starter therefore records zero spawns of its own, every caller converges
441// on the published carriage, and no caller ever holds the lock itself.
442func TestDaemonStartConcurrentRequestsShareOneElection(t *testing.T) {
443 isolateSandboxEnv(t)
444 dir, carriage := testDaemonCarriage(t)
445 scripted := &scriptedDaemonStarter{carriage: carriage}
446 withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
447
448 // Another client's elected spawner holds the lock; the daemon it starts
449 // publishes the carriage shortly afterwards.
450 release, acquired, err := acquireSpawnLock(dir)
451 require.NoError(t, err)
452 require.True(t, acquired, "the test must hold the spawn lock")
453 t.Cleanup(release)
454 // The daemon being started takes lifecycle ownership at once and keeps it
455 // for its whole run, as the real daemon does. Without it, a caller that
456 // dialed just before the daemon was published could win the spawn lock
457 // once it is released and elect itself.
458 daemonOwner, err := daemonLifecycleProbe.TryAcquire(filepath.Dir(carriage))
459 require.NoError(t, err)
460 t.Cleanup(func() { _ = daemonOwner.Release() })
461
462 // The target is resolved once: require.* is not goroutine-safe, and the
463 // resolved target is immutable and shared by every caller.
464 target := startableLocalTarget(t, carriage, ports.BrokerDaemonStartIfNeeded)
465
466 const callers = 8
467 var wg sync.WaitGroup
468 results := make([]daemonmux.RawFramedTransport, callers)
469 errs := make([]error, callers)
470 start := make(chan struct{})
471 for i := range callers {
472 wg.Add(1)
473 go func() {
474 defer wg.Done()
475 <-start
476 results[i], errs[i] = dialBrokerLocalDaemon(context.Background(), carriage, target)
477 }()
478 }
479 // Publish the daemon the held election is starting, as its spawner does.
480 go func() {
481 <-start
482 scripted.markRunning()
483 }()
484 close(start)
485 // The held election ends once the carriage answers.
486 require.Eventually(t, func() bool { return scripted.isRunning() }, brokerTestWait, time.Millisecond)
487 release()
488 wg.Wait()
489
490 for i := range callers {
491 require.NoError(t, errs[i], "caller %d", i)
492 require.NotNil(t, results[i], "caller %d", i)
493 }
494 _, spawns := scripted.counters()
495 require.Zero(t, spawns, "a caller must never spawn while another election holds the lock")
496}
497
498// TestDaemonStartConcurrentExistingOnlyNeverSpawns proves concurrent
499// existing-only acquisitions never share a start: each one dials, fails on the
500// same absent carriage, and spawns nothing.
501func TestDaemonStartConcurrentExistingOnlyNeverSpawns(t *testing.T) {
502 isolateSandboxEnv(t)
503 _, carriage := testDaemonCarriage(t)
504 scripted := &scriptedDaemonStarter{carriage: carriage}
505 withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
506
507 target := startableLocalTarget(t, carriage, ports.BrokerDaemonExistingOnly)
508 const callers = 8
509 var wg sync.WaitGroup
510 start := make(chan struct{})
511 for range callers {
512 wg.Add(1)
513 go func() {
514 defer wg.Done()
515 <-start
516 _, err := dialBrokerLocalDaemon(context.Background(), carriage, target)
517 require.ErrorIs(t, err, os.ErrNotExist)
518 }()
519 }
520 close(start)
521 wg.Wait()
522
523 dials, spawns := scripted.counters()
524 require.Equal(t, callers, dials)
525 require.Zero(t, spawns)
526}
527
528// TestDaemonStartPropagatesTheWholeTarget proves the resolved target reaches the
529// carriage owner whole: address, fence, policy, start authorization, and expected
530// identity all travel together, so a dial can never act on an authorization the
531// connector did not validate.
532func TestDaemonStartPropagatesTheWholeTarget(t *testing.T) {
533 isolateSandboxEnv(t)
534 _, carriage := testDaemonCarriage(t)
535
536 var seen []ports.BrokerDialTarget
537 var mu sync.Mutex
538 connectorDial := func(ctx context.Context, target ports.BrokerDialTarget) (daemonmux.RawFramedTransport, error) {
539 mu.Lock()
540 seen = append(seen, target)
541 mu.Unlock()
542 return nil, os.ErrNotExist
543 }
544 connector, err := daemonmux.NewEndpointConnector(daemonmux.RawCarrierDialer(connectorDial), daemonmux.DefaultMuxCeilings())
545 require.NoError(t, err)
546
547 // The pool-side connector already owns the whole target; the local start
548 // helper is what turns it into the carriage decision.
549 scripted := &scriptedDaemonStarter{carriage: carriage}
550 withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
551
552 target := startableLocalTarget(t, carriage, ports.BrokerDaemonExistingOnly)
553 raw, err := dialBrokerLocalDaemon(context.Background(), carriage, target)
554 require.Error(t, err)
555 require.Nil(t, raw)
556
557 // The exact same target value is what a connector hands to its dial
558 // function, so the local helper observes the identical authorization.
559 _, err = connector.Connect(context.Background(), target)
560 require.Error(t, err)
561 mu.Lock()
562 require.Equal(t, []ports.BrokerDialTarget{target}, seen, "the connector dials the whole resolved target")
563 mu.Unlock()
564 require.Equal(t, ports.BrokerDaemonExistingOnly, seen[0].StartMode)
565 require.True(t, seen[0].Fence.Local)
566 require.Equal(t, target.Policy, seen[0].Policy)
567 require.Equal(t, target.ExpectedIdentity, seen[0].ExpectedIdentity)
568}
569
570// TestDaemonStartNeverFallsBackToASessionDial proves structurally that the
571// start path owns no session carriage: it never dials the daemon's public
572// session socket and never wraps a session transport.
573func TestDaemonStartNeverFallsBackToASessionDial(t *testing.T) {
574 sources := loadAppSources(t, false)
575 body, ok := sources["broker_daemon_start.go"]
576 require.True(t, ok, "the daemon start owner must exist")
577
578 for _, forbidden := range []string{
579 "ipc.DialContext(",
580 "ipc.SocketPath(",
581 "sessionwire.",
582 "localDaemonDialer",
583 "ensureDaemonWithLifecycle",
584 "ensureDaemon(",
585 } {
586 require.NotContains(t, body, forbidden, "the daemon start owner must not reach a session carriage")
587 }
588 // The only transport it hands out is the daemonmux carriage.
589 require.Contains(t, body, "daemonmux.RawFramedTransport")
590}
591
592// TestDaemonStartRefusesIncompleteStarter proves a start seam that cannot dial
593// or spawn fails closed instead of dialing a session or panicking.
594func TestDaemonStartRefusesIncompleteStarter(t *testing.T) {
595 isolateSandboxEnv(t)
596 _, carriage := testDaemonCarriage(t)
597 target := startableLocalTarget(t, carriage, ports.BrokerDaemonStartIfNeeded)
598
599 for _, starter := range []brokerDaemonStarter{{}, {dial: dialLocalDaemonCarriage}, {spawn: realSpawn}} {
600 withDaemonStarter(t, starter)
601 _, err := dialBrokerLocalDaemon(context.Background(), carriage, target)
602 require.Error(t, err)
603 var typed ports.BrokerError
604 require.ErrorAs(t, err, &typed)
605 require.Equal(t, ports.BrokerErrorIncompatible, typed.Code)
606 }
607}
608
609// TestDaemonStartRefusesInvalidTarget proves an unvalidated resolved target is
610// refused before any dial or spawn.
611func TestDaemonStartRefusesInvalidTarget(t *testing.T) {
612 isolateSandboxEnv(t)
613 _, carriage := testDaemonCarriage(t)
614 scripted := &scriptedDaemonStarter{carriage: carriage}
615 withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
616
617 invalid := startableLocalTarget(t, carriage, ports.BrokerDaemonStartIfNeeded)
618 invalid.StartMode = 0
619 _, err := dialBrokerLocalDaemon(context.Background(), carriage, invalid)
620 require.Error(t, err)
621 var typed ports.BrokerError
622 require.ErrorAs(t, err, &typed)
623 require.Equal(t, ports.BrokerErrorIncompatible, typed.Code)
624 dials, spawns := scripted.counters()
625 require.Zero(t, dials)
626 require.Zero(t, spawns)
627}
628
629// TestDaemonStartArgvFlagRoundTrips proves the helper argument spelling is one
630// closed value both sides agree on, and that an unknown value is refused in
631// either direction.
632func TestDaemonStartArgvFlagRoundTrips(t *testing.T) {
633 tests := []struct {
634 mode ports.BrokerDaemonStartMode
635 flag string
636 }{
637 {mode: ports.BrokerDaemonExistingOnly, flag: "existing-only"},
638 {mode: ports.BrokerDaemonStartIfNeeded, flag: "if-needed"},
639 }
640 for _, tt := range tests {
641 flag, err := brokerDaemonStartArgvFlag(tt.mode)
642 require.NoError(t, err)
643 require.Equal(t, tt.flag, flag)
644 parsed, err := parseBrokerDaemonStartArgvFlag(flag)
645 require.NoError(t, err)
646 require.Equal(t, tt.mode, parsed)
647 }
648 _, err := brokerDaemonStartArgvFlag(ports.BrokerDaemonStartMode(0))
649 require.Error(t, err)
650 for _, invalid := range []string{"", "existing_only", "if_needed", "always", "Existing-Only"} {
651 _, err := parseBrokerDaemonStartArgvFlag(invalid)
652 require.Error(t, err, "value %q must be refused", invalid)
653 }
654}
655
656// testLocalSandboxConfigForCarriage derives local authority from production state.
657func testLocalSandboxConfigForCarriage(t *testing.T, carriage string) (*brokerconfig.Config, brokerconfig.LocalBinding) {
658 t.Helper()
659 layout := emptyProductionBrokerLayout(t, "")
660 config, err := brokerconfig.LoadProduction(layout, productionBrokerConfigPath(), brokerLocalTestIdentity, brokerTestPolicy(), carriage)
661 require.NoError(t, err)
662 binding, ok := config.LocalBinding()
663 require.True(t, ok)
664 return config, binding
665}