detached 644ac29

test(app): run broker ensure tests against the production layout

commit
644ac295fb9e4af26b1b2eab25802f88d78071c4
author
brice · · 2026-09-23 14:53 UTC
parent
eebe943
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +84 -332
M internal/app/broker_offline_status_test.go +84 -332
@@ -1,7 +1,6 @@
11 package app
22
33 import (
4- "bytes"
54 "context"
65 "encoding/json"
76 "errors"
@@ -17,6 +16,7 @@ import (
1716
1817 "github.com/bnema/vev/internal/adapters/brokerconfig"
1918 "github.com/bnema/vev/internal/adapters/brokeripc"
19+ "github.com/bnema/vev/internal/adapters/daemonmux"
2020 "github.com/bnema/vev/internal/adapters/ipc"
2121 "github.com/bnema/vev/internal/adapters/lifecycle"
2222 )
@@ -31,16 +31,30 @@ func sandboxEmptyDocument(idleGrace string) map[string]any {
3131 return document
3232 }
3333
34-// emptySandboxRoot writes one marker-valid sandbox root and returns its layout.
35-func emptySandboxRoot(t *testing.T, idleGrace string) brokerconfig.Layout {
34+// emptyProductionBrokerLayout provisions the real broker config in private XDG
35+// directories; each test receives an independent runtime, state, and config.
36+func emptyProductionBrokerLayout(t *testing.T, idleGrace string) brokerconfig.Layout {
3637 t.Helper()
37- isolateSandboxEnv(t)
38- root := filepath.Join(shortTempDir(t, "vevu"), "s")
39- require.NoError(t, os.MkdirAll(root, 0o700))
40- writeSandboxConfig(t, root, sandboxEmptyDocument(idleGrace))
41- layout, err := offlineLayout(root)
42- require.NoError(t, err)
43- return layout
38+ t.Setenv("VEV_ENV", "")
39+ t.Setenv("VEV_ENV_ROOT", "")
40+ t.Setenv("XDG_RUNTIME_DIR", shortTempDir(t, "vb"))
41+ t.Setenv("XDG_STATE_HOME", shortTempDir(t, "vb"))
42+ t.Setenv("XDG_CONFIG_HOME", shortTempDir(t, "vb"))
43+ require.NoError(t, ensureProductionBrokerConfig())
44+ if idleGrace != "" {
45+ raw, err := json.Marshal(sandboxEmptyDocument(idleGrace))
46+ require.NoError(t, err)
47+ require.NoError(t, os.WriteFile(productionBrokerConfigPath(), raw, 0o600))
48+ }
49+ return productionBrokerLayout()
50+}
51+
52+// productionStatusRequest provides the inputs used by production connect-or-spawn.
53+func productionStatusRequest(layout brokerconfig.Layout, timeout time.Duration) brokerStatusRequest {
54+ return brokerStatusRequest{
55+ layout: layout, socketPath: brokeripc.SocketPath(layout.Runtime),
56+ deadline: time.Now().Add(timeout), timeout: timeout,
57+ }
4458 }
4559
4660 // fakeStatusLock records releases for one held election.
@@ -63,8 +77,7 @@ func readyReport(socketPath string) brokerStatusReport {
6377 func testStatusDeps(t *testing.T) (brokerStatusDeps, *statusRecorder) {
6478 t.Helper()
6579 rec := &statusRecorder{
66- now: time.Now(),
67- stdout: &bytes.Buffer{},
80+ now: time.Now(),
6881 }
6982 deps := brokerStatusDeps{
7083 now: func() time.Time { return rec.now },
@@ -83,7 +96,6 @@ func testStatusDeps(t *testing.T) (brokerStatusDeps, *statusRecorder) {
8396 rec.spawns.Add(1)
8497 return nil
8598 },
86- stdout: rec.stdout,
8799 }
88100 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {
89101 return brokerStatusReport{}, absentError(socketPath)
@@ -98,7 +110,6 @@ type statusRecorder struct {
98110 spawns atomic.Int32
99111 elections atomic.Int32
100112 locks []*fakeStatusLock
101- stdout *bytes.Buffer
102113 }
103114
104115 // setProbe installs one scripted probe that sees its 1-based attempt number.
@@ -108,119 +119,23 @@ func (r *statusRecorder) setProbe(deps *brokerStatusDeps, probe func(attempt int
108119 }
109120 }
110121
111-func (r *statusRecorder) report(t *testing.T) brokerStatusReport {
112- t.Helper()
113- var report brokerStatusReport
114- require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(r.stdout.String())), &report))
115- return report
116-}
117-
118-// decodeOneReport asserts stdout holds exactly one JSON document and decodes it.
122+// decodeOneReport remains shared with the subprocess broker tests until those
123+// are converted to the production launcher.
119124 func decodeOneReport(t *testing.T, raw string) brokerStatusReport {
120125 t.Helper()
121126 trimmed := strings.TrimSpace(raw)
122- require.Equal(t, 1, strings.Count(trimmed, "\n")+1, "stdout must hold exactly one report line")
127+ require.Equal(t, 1, strings.Count(trimmed, "\n")+1)
123128 var report brokerStatusReport
124129 require.NoError(t, json.Unmarshal([]byte(trimmed), &report))
125130 return report
126131 }
127132
128-func TestParseBrokerLauncherArgs(t *testing.T) {
129- tests := []struct {
130- name string
131- args []string
132- want brokerLauncherOptions
133- wantErr string
134- }{
135- {name: "root only", args: []string{"--offline-root", "/srv/sandbox"}, want: brokerLauncherOptions{offlineRoot: "/srv/sandbox"}},
136- {
137- name: "root and grace",
138- args: []string{"--offline-root", "/srv/sandbox", "--idle-grace", "90s"},
139- want: brokerLauncherOptions{offlineRoot: "/srv/sandbox", idleGrace: 90 * time.Second},
140- },
141- {name: "missing root", args: nil, wantErr: "requires --offline-root"},
142- {name: "missing root value", args: []string{"--offline-root"}, wantErr: "requires a path"},
143- {name: "duplicate root", args: []string{"--offline-root", "/a", "--offline-root", "/b"}, wantErr: "duplicate --offline-root"},
144- {name: "duplicate grace", args: []string{"--offline-root", "/a", "--idle-grace", "1s", "--idle-grace", "2s"}, wantErr: "duplicate --idle-grace"},
145- {name: "zero grace", args: []string{"--offline-root", "/a", "--idle-grace", "0s"}, wantErr: "must be positive"},
146- {name: "invalid grace", args: []string{"--offline-root", "/a", "--idle-grace", "soon"}, wantErr: "not a duration"},
147- {name: "ensure is not a launcher flag", args: []string{"--offline-root", "/a", "--ensure"}, wantErr: "unknown flag"},
148- {name: "positional", args: []string{"--offline-root", "/a", "extra"}, wantErr: "positional"},
149- }
150- for _, tt := range tests {
151- t.Run(tt.name, func(t *testing.T) {
152- command, err := parseBrokerLauncherArgs(tt.args)
153- if tt.wantErr != "" {
154- require.Error(t, err)
155- require.Contains(t, err.Error(), tt.wantErr)
156- return
157- }
158- require.NoError(t, err)
159- require.Equal(t, kindBrokerLauncher, command.kind)
160- require.Equal(t, tt.want, command.brokerLauncher)
161- })
162- }
163-}
164-
165-func TestParseBrokerStatusArgs(t *testing.T) {
166- tests := []struct {
167- name string
168- args []string
169- want brokerStatusOptions
170- wantErr string
171- }{
172- {
173- name: "root only defaults the timeout",
174- args: []string{"--offline-root", "/srv/sandbox"},
175- want: brokerStatusOptions{offlineRoot: "/srv/sandbox", timeout: brokerStatusDefaultTimeout},
176- },
177- {
178- name: "ensure and timeout",
179- args: []string{"--offline-root", "/a", "--ensure", "--timeout", "250ms"},
180- want: brokerStatusOptions{offlineRoot: "/a", ensure: true, timeout: 250 * time.Millisecond},
181- },
182- {name: "missing root", args: []string{"--ensure"}, wantErr: "requires --offline-root"},
183- {name: "duplicate root", args: []string{"--offline-root", "/a", "--offline-root", "/b"}, wantErr: "duplicate --offline-root"},
184- {name: "duplicate ensure", args: []string{"--offline-root", "/a", "--ensure", "--ensure"}, wantErr: "duplicate --ensure"},
185- {name: "duplicate timeout", args: []string{"--offline-root", "/a", "--timeout", "1s", "--timeout", "2s"}, wantErr: "duplicate --timeout"},
186- {name: "missing timeout value", args: []string{"--offline-root", "/a", "--timeout"}, wantErr: "requires a duration"},
187- {name: "zero timeout", args: []string{"--offline-root", "/a", "--timeout", "0s"}, wantErr: "must be positive"},
188- {name: "invalid timeout", args: []string{"--offline-root", "/a", "--timeout", "soon"}, wantErr: "not a duration"},
189- {name: "idle grace is not a status flag", args: []string{"--offline-root", "/a", "--idle-grace", "1s"}, wantErr: "unknown flag"},
190- {name: "positional", args: []string{"--offline-root", "/a", "extra"}, wantErr: "positional"},
191- }
192- for _, tt := range tests {
193- t.Run(tt.name, func(t *testing.T) {
194- command, err := parseBrokerStatusArgs(tt.args)
195- if tt.wantErr != "" {
196- require.Error(t, err)
197- require.Contains(t, err.Error(), tt.wantErr)
198- return
199- }
200- require.NoError(t, err)
201- require.Equal(t, kindBrokerStatus, command.kind)
202- require.Equal(t, tt.want, command.brokerStatus)
203- })
204- }
205-}
206-
207-func TestBrokerLauncherAndStatusAreHiddenFromPublicHelp(t *testing.T) {
208- require.NotContains(t, usageText, brokerLauncherCommand)
209- require.NotContains(t, usageText, brokerStatusCommand)
210- _, err := parseArgs([]string{brokerLauncherCommand, "--offline-root", "/srv/sandbox"})
211- require.NoError(t, err)
212- _, err = parseArgs([]string{brokerStatusCommand, "--offline-root", "/srv/sandbox", "--ensure"})
213- require.NoError(t, err)
214- _, err = parseArgs([]string{"--help"})
215- require.NoError(t, err)
216-}
217-
218133 func TestEffectiveIdleGrace(t *testing.T) {
219- layout := emptySandboxRoot(t, "90s")
220- config, err := brokerconfig.Load(layout)
134+ layout := emptyProductionBrokerLayout(t, "90s")
135+ config, err := brokerconfig.LoadProduction(layout, productionBrokerConfigPath(), "", localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
221136 require.NoError(t, err)
222- unprovisioned := emptySandboxRoot(t, "")
223- plain, err := brokerconfig.Load(unprovisioned)
137+ unprovisioned := emptyProductionBrokerLayout(t, "")
138+ plain, err := brokerconfig.LoadProduction(unprovisioned, productionBrokerConfigPath(), "", localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
224139 require.NoError(t, err)
225140
226141 tests := []struct {
@@ -251,71 +166,27 @@ func TestEffectiveIdleGrace(t *testing.T) {
251166 }
252167
253168 func TestBrokerServeRefusesConflictingIdleGrace(t *testing.T) {
254- layout := emptySandboxRoot(t, "90s")
169+ layout := emptyProductionBrokerLayout(t, "90s")
255170 ctx, cancel := context.WithCancel(context.Background())
256171 defer cancel()
257172 deps, _ := testBrokerServeDeps(newSandboxClock())
258- err := runBrokerServe(ctx, brokerServeOptions{offlineRoot: layout.Root, idleGrace: time.Minute}, deps)
173+ err := runBrokerServe(ctx, brokerServeOptions{production: true, idleGrace: time.Minute}, deps)
259174 require.Error(t, err)
260175 require.Contains(t, err.Error(), "conflicts with the provisioned idle grace")
261176 require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"), "a refused serve must not take the lifetime lock")
262177 }
263178
264179 func TestBrokerLauncherRefusesConflictingIdleGraceBeforeSpawning(t *testing.T) {
265- layout := emptySandboxRoot(t, "90s")
266- err := runBrokerLauncher(brokerLauncherOptions{offlineRoot: layout.Root, idleGrace: time.Minute})
180+ layout := emptyProductionBrokerLayout(t, "90s")
181+ config, err := brokerconfig.LoadProduction(layout, productionBrokerConfigPath(), "", localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
182+ require.NoError(t, err)
183+ _, err = effectiveIdleGrace(config, time.Minute)
267184 require.Error(t, err)
268185 require.Contains(t, err.Error(), "conflicts with the provisioned idle grace")
269186 }
270187
271-func TestBrokerStatusDialOnlyReportsOfflineWithoutCreatingAnything(t *testing.T) {
272- layout := emptySandboxRoot(t, "")
273- deps := defaultBrokerStatusDeps()
274- var out bytes.Buffer
275- deps.stdout = &out
276-
277- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, timeout: time.Second}, deps))
278- report := decodeOneReport(t, out.String())
279- require.Equal(t, "offline", report.Status)
280- require.Equal(t, brokeripc.SocketPath(layout.Runtime), report.Endpoint)
281- require.Zero(t, report.Epoch)
282- require.Zero(t, report.Revision)
283- require.Zero(t, report.HostCount)
284-
285- require.NoDirExists(t, layout.Runtime, "dial-only status must not create the runtime directory")
286- require.NoDirExists(t, layout.Spawn, "dial-only status must not create the spawn directory")
287-}
288-
289-func TestBrokerStatusDialOnlyReadyAgainstLiveSandbox(t *testing.T) {
290- root, prodRuntime, prodState := isolateSandboxEnv(t)
291- fixture := newBrokerMuxFixture(t, brokerTestPolicy())
292- require.NoError(t, os.MkdirAll(root, 0o700))
293- writeSandboxConfig(t, root, sandboxRegistrationDocument(fixture.route))
294-
295- clk := newSandboxClock()
296- serveDeps, ready := testBrokerServeDeps(clk)
297- ctx, cancel := context.WithCancel(context.Background())
298- defer cancel()
299- done := runSandbox(ctx, brokerServeOptions{offlineRoot: root}, serveDeps)
300- awaitSandboxReady(t, ready, done)
301-
302- deps := defaultBrokerStatusDeps()
303- var out bytes.Buffer
304- deps.stdout = &out
305- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: root, timeout: time.Second}, deps))
306- report := decodeOneReport(t, out.String())
307- require.Equal(t, "ready", report.Status)
308- require.Positive(t, report.Epoch)
309- require.Positive(t, report.Revision)
310- // Provisioned endpoints seed durable broker membership before serving.
311- require.Equal(t, 1, report.HostCount)
312-
313- awaitSandboxCancel(t, cancel, done)
314- requireProductionUntouched(t, prodRuntime, prodState)
315-}
316-
317188 func TestBrokerStatusEnsureSpawnsOnceThenReportsReady(t *testing.T) {
318- layout := emptySandboxRoot(t, "")
189+ layout := emptyProductionBrokerLayout(t, "")
319190 deps, rec := testStatusDeps(t)
320191 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {
321192 if rec.spawns.Load() > 0 {
@@ -323,17 +194,18 @@ func TestBrokerStatusEnsureSpawnsOnceThenReportsReady(t *testing.T) {
323194 }
324195 return brokerStatusReport{}, absentError(socketPath)
325196 })
326- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps))
197+ report, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
198+ require.NoError(t, err)
327199
328200 require.Equal(t, int32(1), rec.spawns.Load(), "exactly one spawn is expected")
329201 require.Equal(t, int32(1), rec.elections.Load(), "exactly one election is expected")
330202 require.Len(t, rec.locks, 1)
331203 require.Equal(t, int32(1), rec.locks[0].releases.Load(), "the election must be released after readiness")
332- require.Equal(t, "ready", rec.report(t).Status)
204+ require.Equal(t, "ready", report.Status)
333205 }
334206
335207 func TestBrokerStatusEnsureWaitsForAnotherSpawner(t *testing.T) {
336- layout := emptySandboxRoot(t, "")
208+ layout := emptyProductionBrokerLayout(t, "")
337209 var readyAfter int32 = 4
338210 deps, rec := testStatusDeps(t)
339211 rec.setProbe(&deps, func(attempt int, socketPath string) (brokerStatusReport, error) {
@@ -346,15 +218,16 @@ func TestBrokerStatusEnsureWaitsForAnotherSpawner(t *testing.T) {
346218 rec.elections.Add(1)
347219 return nil, lifecycle.ErrBusy
348220 }
349- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps))
221+ report, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
222+ require.NoError(t, err)
350223
351224 require.Zero(t, rec.spawns.Load(), "a waiter must never spawn while another caller is elected")
352225 require.Positive(t, rec.elections.Load())
353- require.Equal(t, "ready", rec.report(t).Status)
226+ require.Equal(t, "ready", report.Status)
354227 }
355228
356229 func TestBrokerStatusEnsureDefersToLifetimeOwnerBeforeSpawning(t *testing.T) {
357- layout := emptySandboxRoot(t, "")
230+ layout := emptyProductionBrokerLayout(t, "")
358231 var ownerCalls int32
359232 deps, rec := testStatusDeps(t)
360233 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {
@@ -369,13 +242,14 @@ func TestBrokerStatusEnsureDefersToLifetimeOwnerBeforeSpawning(t *testing.T) {
369242 }
370243 return false, nil
371244 }
372- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps))
245+ _, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
246+ require.NoError(t, err)
373247 require.Equal(t, int32(1), rec.spawns.Load(), "a live lifetime owner must suppress the spawn until it is gone")
374- require.Equal(t, int32(1), rec.elections.Load(), "the election is taken only after the live owner releases the sandbox")
248+ require.Equal(t, int32(1), rec.elections.Load(), "the election is taken only after the live owner releases the broker")
375249 }
376250
377251 func TestBrokerStatusEnsureRecoversAfterSpawnerDeath(t *testing.T) {
378- layout := emptySandboxRoot(t, "")
252+ layout := emptyProductionBrokerLayout(t, "")
379253 var electionAttempts int32
380254 deps, rec := testStatusDeps(t)
381255 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {
@@ -394,210 +268,96 @@ func TestBrokerStatusEnsureRecoversAfterSpawnerDeath(t *testing.T) {
394268 rec.locks = append(rec.locks, lock)
395269 return lock, nil
396270 }
397- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps))
271+ report, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
272+ require.NoError(t, err)
398273 require.Equal(t, int32(1), rec.spawns.Load(), "a waiter must take over once the elected spawner dies")
399- require.Equal(t, "ready", rec.report(t).Status)
274+ require.Equal(t, "ready", report.Status)
400275 }
401276
402277 func TestBrokerStatusEnsureIncompatibleEndpointFailsWithoutSpawning(t *testing.T) {
403- layout := emptySandboxRoot(t, "")
278+ layout := emptyProductionBrokerLayout(t, "")
404279 deps, rec := testStatusDeps(t)
405280 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {
406281 return brokerStatusReport{}, incompatibleError(socketPath, errors.New("foreign endpoint"))
407282 })
408- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps)
283+ _, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
409284 require.ErrorIs(t, err, errBrokerIncompatible)
410285 require.Zero(t, rec.spawns.Load())
411286 require.Zero(t, rec.elections.Load())
412- report := rec.report(t)
413- require.Equal(t, "offline", report.Status)
414- require.Equal(t, 3, exitCode(err))
415287 }
416288
417289 func TestBrokerStatusEnsureBoundedTimeoutReportsOffline(t *testing.T) {
418- layout := emptySandboxRoot(t, "")
290+ layout := emptyProductionBrokerLayout(t, "")
419291 deps, rec := testStatusDeps(t)
420- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: 50 * time.Millisecond}, deps)
292+ _, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, 50*time.Millisecond), deps)
421293 require.ErrorIs(t, err, errBrokerNotReady)
422- require.Equal(t, 3, exitCode(err))
423294 require.Equal(t, int32(1), rec.spawns.Load())
424- require.Equal(t, "offline", rec.report(t).Status)
425295 }
426296
427297 func TestBrokerStatusEnsureSpawnFailureIsReported(t *testing.T) {
428- layout := emptySandboxRoot(t, "")
298+ layout := emptyProductionBrokerLayout(t, "")
429299 spawnErr := errors.New("launcher exec failed")
430- deps, rec := testStatusDeps(t)
300+ deps, _ := testStatusDeps(t)
431301 deps.spawn = func(context.Context, string, time.Duration, bool) error {
432- rec.spawns.Add(1)
433302 return spawnErr
434303 }
435- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps)
304+ _, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
436305 require.ErrorIs(t, err, spawnErr)
437- require.Equal(t, 3, exitCode(err))
438- require.Equal(t, "offline", rec.report(t).Status)
439306 }
440307
441-func TestBrokerStatusEnsureRejectsUnsafeRoot(t *testing.T) {
442- isolateSandboxEnv(t)
443- tests := []struct {
444- name string
445- root string
446- }{
447- {name: "relative root", root: "relative"},
448- {name: "production overlap", root: ipc.SocketDir()},
449- }
450- for _, tt := range tests {
451- t.Run(tt.name, func(t *testing.T) {
452- deps, rec := testStatusDeps(t)
453- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: tt.root, ensure: true, timeout: time.Second}, deps)
454- require.Error(t, err)
455- require.Equal(t, 3, exitCode(err), "a layout failure must still exit 3")
456- require.Equal(t, "offline", rec.report(t).Status, "a layout failure must still emit one offline report")
457- require.Zero(t, rec.spawns.Load())
458- })
459- }
460-}
461-
462-// TestBrokerStatusEnsureConfigFailureReportsOffline proves a malformed or
463-// missing configuration still leaves the caller with one offline JSON document
464-// and a code-3 failure instead of a bare error.
465-func TestBrokerStatusEnsureConfigFailureReportsOffline(t *testing.T) {
466- isolateSandboxEnv(t)
467- root := filepath.Join(shortTempDir(t, "vevm"), "s")
468- require.NoError(t, os.MkdirAll(root, 0o700))
469- deps, rec := testStatusDeps(t)
470- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: root, ensure: true, timeout: time.Second}, deps)
308+// A corrupt production broker.json is rejected before broker ownership.
309+func TestBrokerEnsureProductionConfigFailure(t *testing.T) {
310+ layout := emptyProductionBrokerLayout(t, "")
311+ require.NoError(t, os.WriteFile(productionBrokerConfigPath(), []byte("not json"), 0o600))
312+ deps, _ := testBrokerServeDeps(newSandboxClock())
313+ err := runBrokerServe(context.Background(), brokerServeOptions{production: true}, deps)
471314 require.Error(t, err)
472- require.Contains(t, err.Error(), brokerconfig.ConfigFileName)
473- require.Equal(t, 3, exitCode(err))
474- require.Equal(t, "offline", rec.report(t).Status)
475- require.Zero(t, rec.spawns.Load())
315+ require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"))
476316 }
477317
478-// TestBrokerStatusEnsureSecuresRootBeforeSpawning proves the ensure path
479-// validates and secures the sandbox root exactly as the launcher does, before
480-// the spawn directory is created or any election lock is taken.
318+// TestBrokerStatusEnsureSecuresRootBeforeSpawning rejects an insecure
319+// production spawn directory before electing a launcher.
481320 func TestBrokerStatusEnsureSecuresRootBeforeSpawning(t *testing.T) {
482- layout := emptySandboxRoot(t, "")
483- require.NoError(t, os.Chmod(layout.Root, 0o755))
321+ layout := emptyProductionBrokerLayout(t, "")
322+ require.NoError(t, os.MkdirAll(layout.Runtime, 0o700))
323+ require.NoError(t, os.MkdirAll(layout.Spawn, 0o700))
324+ require.NoError(t, os.Chmod(layout.Spawn, 0o755))
484325 deps, rec := testStatusDeps(t)
485- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps)
326+ _, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
486327 require.Error(t, err)
487328 require.Contains(t, err.Error(), "0755")
488- require.Equal(t, 3, exitCode(err))
489- require.Equal(t, "offline", rec.report(t).Status)
490329 require.Zero(t, rec.spawns.Load())
491330 require.Zero(t, rec.elections.Load())
492- require.NoFileExists(t, filepath.Join(layout.Runtime, brokerconfig.SpawnDirName), "an insecure root must be refused before the spawn directory is created")
331+ require.NoFileExists(t, filepath.Join(layout.Spawn, "lifecycle.lock"), "an insecure spawn directory must be refused before election")
493332 }
494333
495334 // TestBrokerStatusEnsureDeadlineBoundsSpawnWait proves the overall ensure
496335 // deadline bounds a launcher spawn that never returns, so a blocking launcher
497-// cannot wedge the status command.
336+// cannot wedge connect-or-spawn.
498337 func TestBrokerStatusEnsureDeadlineBoundsSpawnWait(t *testing.T) {
499- layout := emptySandboxRoot(t, "")
500- deps, rec := testStatusDeps(t)
338+ layout := emptyProductionBrokerLayout(t, "")
339+ deps, _ := testStatusDeps(t)
501340 deps.spawn = func(ctx context.Context, _ string, _ time.Duration, _ bool) error {
502341 <-ctx.Done()
503342 return ctx.Err()
504343 }
505344 start := time.Now()
506- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: 200 * time.Millisecond}, deps)
345+ _, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, 200*time.Millisecond), deps)
507346 require.ErrorIs(t, err, context.DeadlineExceeded)
508- require.Equal(t, 3, exitCode(err))
509347 require.Less(t, time.Since(start), 5*time.Second, "the overall deadline must bound the launcher spawn wait")
510- require.Equal(t, "offline", rec.report(t).Status)
511-}
512-
513-// TestBrokerStatusDialOnlyAppliesTimeout proves the dial-only probe is bounded
514-// by --timeout as one absolute bound and is never silently replaced by the
515-// ensure probe budget.
516-func TestBrokerStatusDialOnlyAppliesTimeout(t *testing.T) {
517- tests := []struct {
518- name string
519- timeout time.Duration
520- }{
521- {name: "short timeout is the probe bound", timeout: 250 * time.Millisecond},
522- {name: "default timeout is the probe bound", timeout: brokerStatusDefaultTimeout},
523- }
524- for _, tt := range tests {
525- t.Run(tt.name, func(t *testing.T) {
526- layout := emptySandboxRoot(t, "")
527- deps, _ := testStatusDeps(t)
528- var observed time.Duration
529- sawDeadline := false
530- deps.probe = func(ctx context.Context, socketPath string) (brokerStatusReport, error) {
531- deadline, ok := ctx.Deadline()
532- sawDeadline = ok
533- if ok {
534- observed = time.Until(deadline)
535- }
536- return brokerStatusReport{}, absentError(socketPath)
537- }
538- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, timeout: tt.timeout}, deps))
539- require.True(t, sawDeadline, "dial-only must bound the probe with the timeout context")
540- require.InDelta(t, tt.timeout.Seconds(), observed.Seconds(), 0.5)
541- })
542- }
543-}
544-
545-// TestBrokerStatusDialOnlyBoundedByTimeout proves a live-but-silent endpoint is
546-// bounded by --timeout rather than the ensure probe budget.
547-func TestBrokerStatusDialOnlyBoundedByTimeout(t *testing.T) {
548- layout := emptySandboxRoot(t, "")
549- require.NoError(t, os.MkdirAll(layout.Runtime, 0o700))
550- _ = bindUnixSocket(t, brokeripc.SocketPath(layout.Runtime))
551- deps := defaultBrokerStatusDeps()
552- var out bytes.Buffer
553- deps.stdout = &out
554- start := time.Now()
555- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, timeout: 300 * time.Millisecond}, deps))
556- require.Less(t, time.Since(start), time.Second, "the dial-only probe must be bounded by --timeout, not the ensure budget")
557- require.Equal(t, "offline", decodeOneReport(t, out.String()).Status)
558348 }
559349
560350 func TestBrokerStatusEnsureHonorsCancellation(t *testing.T) {
561- layout := emptySandboxRoot(t, "")
351+ layout := emptyProductionBrokerLayout(t, "")
562352 deps, _ := testStatusDeps(t)
563353 ctx, cancel := context.WithCancel(context.Background())
564354 cancel()
565- err := runBrokerStatus(ctx, brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps)
355+ _, err := ensureBrokerReady(ctx, productionStatusRequest(layout, time.Second), deps)
566356 require.ErrorIs(t, err, context.Canceled)
567357 }
568358
569-func TestBrokerStatusReportIsBoundedJSON(t *testing.T) {
570- var out bytes.Buffer
571- require.NoError(t, writeBrokerStatusReport(&out, brokerStatusReport{
572- Status: "ready", Endpoint: brokeripc.SocketPath("/tmp/sandbox/runtime"), Epoch: 3, Revision: 9, HostCount: 4,
573- }))
574- var fields map[string]json.RawMessage
575- require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(out.String())), &fields))
576- require.Equal(t, []string{"endpoint", "epoch", "host_count", "revision", "status"}, sortedKeys(fields))
577- require.LessOrEqual(t, out.Len(), brokerStatusMaxReportBytes)
578-}
579-
580-// sortedKeys returns the sorted keys of one JSON object.
581-func sortedKeys(fields map[string]json.RawMessage) []string {
582- keys := make([]string, 0, len(fields))
583- for key := range fields {
584- keys = append(keys, key)
585- }
586- slicesSort(keys)
587- return keys
588-}
589-
590-// slicesSort is a tiny insertion sort so the test file needs no extra import.
591-func slicesSort(values []string) {
592- for i := 1; i < len(values); i++ {
593- for j := i; j > 0 && values[j] < values[j-1]; j-- {
594- values[j], values[j-1] = values[j-1], values[j]
595- }
596- }
597-}
598-
599359 func TestProbeBrokerStatusClassifiesAbsenceAndIncompatibility(t *testing.T) {
600- layout := emptySandboxRoot(t, "")
360+ layout := emptyProductionBrokerLayout(t, "")
601361 socketPath := brokeripc.SocketPath(layout.Runtime)
602362
603363 t.Run("missing endpoint is absence", func(t *testing.T) {
@@ -637,7 +397,7 @@ func TestProbeBrokerStatusClassifiesAbsenceAndIncompatibility(t *testing.T) {
637397 }
638398
639399 func TestProbeBrokerStatusSilentRegistrationIsBounded(t *testing.T) {
640- layout := emptySandboxRoot(t, "")
400+ layout := emptyProductionBrokerLayout(t, "")
641401 socketPath := brokeripc.SocketPath(layout.Runtime)
642402 require.NoError(t, os.MkdirAll(layout.Runtime, 0o700))
643403 // A listener that never reads or answers: the dial succeeds and the broker
@@ -674,11 +434,3 @@ func bindStaleUnixSocket(t *testing.T, path string) {
674434 require.NoError(t, listener.Close())
675435 require.FileExists(t, path)
676436 }
677-
678-// Ensure the helper fixture contract used elsewhere stays coherent.
679-func TestStatusFixtureDocument(t *testing.T) {
680- document := sandboxEmptyDocument("1s")
681- require.Equal(t, brokerconfig.Marker, document["marker"])
682- require.Equal(t, "1s", document["idleGrace"])
683- require.Empty(t, sandboxEmptyDocument("")["idleGrace"])
684-}
@@ -1,7 +1,6 @@
1 package app1 package app
2 2
3 import (3 import (
4- "bytes"
5 "context"4 "context"
6 "encoding/json"5 "encoding/json"
7 "errors"6 "errors"
@@ -17,6 +16,7 @@ import (
17 16
18 "github.com/bnema/vev/internal/adapters/brokerconfig"17 "github.com/bnema/vev/internal/adapters/brokerconfig"
19 "github.com/bnema/vev/internal/adapters/brokeripc"18 "github.com/bnema/vev/internal/adapters/brokeripc"
19+ "github.com/bnema/vev/internal/adapters/daemonmux"
20 "github.com/bnema/vev/internal/adapters/ipc"20 "github.com/bnema/vev/internal/adapters/ipc"
21 "github.com/bnema/vev/internal/adapters/lifecycle"21 "github.com/bnema/vev/internal/adapters/lifecycle"
22 )22 )
@@ -31,16 +31,30 @@ func sandboxEmptyDocument(idleGrace string) map[string]any {
31 return document31 return document
32 }32 }
33 33
34-// emptySandboxRoot writes one marker-valid sandbox root and returns its layout.34+// emptyProductionBrokerLayout provisions the real broker config in private XDG
35-func emptySandboxRoot(t *testing.T, idleGrace string) brokerconfig.Layout {35+// directories; each test receives an independent runtime, state, and config.
36+func emptyProductionBrokerLayout(t *testing.T, idleGrace string) brokerconfig.Layout {
36 t.Helper()37 t.Helper()
37- isolateSandboxEnv(t)38+ t.Setenv("VEV_ENV", "")
38- root := filepath.Join(shortTempDir(t, "vevu"), "s")39+ t.Setenv("VEV_ENV_ROOT", "")
39- require.NoError(t, os.MkdirAll(root, 0o700))40+ t.Setenv("XDG_RUNTIME_DIR", shortTempDir(t, "vb"))
40- writeSandboxConfig(t, root, sandboxEmptyDocument(idleGrace))41+ t.Setenv("XDG_STATE_HOME", shortTempDir(t, "vb"))
41- layout, err := offlineLayout(root)42+ t.Setenv("XDG_CONFIG_HOME", shortTempDir(t, "vb"))
42- require.NoError(t, err)43+ require.NoError(t, ensureProductionBrokerConfig())
43- return layout44+ if idleGrace != "" {
45+ raw, err := json.Marshal(sandboxEmptyDocument(idleGrace))
46+ require.NoError(t, err)
47+ require.NoError(t, os.WriteFile(productionBrokerConfigPath(), raw, 0o600))
48+ }
49+ return productionBrokerLayout()
50+}
51+
52+// productionStatusRequest provides the inputs used by production connect-or-spawn.
53+func productionStatusRequest(layout brokerconfig.Layout, timeout time.Duration) brokerStatusRequest {
54+ return brokerStatusRequest{
55+ layout: layout, socketPath: brokeripc.SocketPath(layout.Runtime),
56+ deadline: time.Now().Add(timeout), timeout: timeout,
57+ }
44 }58 }
45 59
46 // fakeStatusLock records releases for one held election.60 // fakeStatusLock records releases for one held election.
@@ -63,8 +77,7 @@ func readyReport(socketPath string) brokerStatusReport {
63 func testStatusDeps(t *testing.T) (brokerStatusDeps, *statusRecorder) {77 func testStatusDeps(t *testing.T) (brokerStatusDeps, *statusRecorder) {
64 t.Helper()78 t.Helper()
65 rec := &statusRecorder{79 rec := &statusRecorder{
66- now: time.Now(),80+ now: time.Now(),
67- stdout: &bytes.Buffer{},
68 }81 }
69 deps := brokerStatusDeps{82 deps := brokerStatusDeps{
70 now: func() time.Time { return rec.now },83 now: func() time.Time { return rec.now },
@@ -83,7 +96,6 @@ func testStatusDeps(t *testing.T) (brokerStatusDeps, *statusRecorder) {
83 rec.spawns.Add(1)96 rec.spawns.Add(1)
84 return nil97 return nil
85 },98 },
86- stdout: rec.stdout,
87 }99 }
88 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {100 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {
89 return brokerStatusReport{}, absentError(socketPath)101 return brokerStatusReport{}, absentError(socketPath)
@@ -98,7 +110,6 @@ type statusRecorder struct {
98 spawns atomic.Int32110 spawns atomic.Int32
99 elections atomic.Int32111 elections atomic.Int32
100 locks []*fakeStatusLock112 locks []*fakeStatusLock
101- stdout *bytes.Buffer
102 }113 }
103 114
104 // setProbe installs one scripted probe that sees its 1-based attempt number.115 // setProbe installs one scripted probe that sees its 1-based attempt number.
@@ -108,119 +119,23 @@ func (r *statusRecorder) setProbe(deps *brokerStatusDeps, probe func(attempt int
108 }119 }
109 }120 }
110 121
111-func (r *statusRecorder) report(t *testing.T) brokerStatusReport {122+// decodeOneReport remains shared with the subprocess broker tests until those
112- t.Helper()123+// are converted to the production launcher.
113- var report brokerStatusReport
114- require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(r.stdout.String())), &report))
115- return report
116-}
117-
118-// decodeOneReport asserts stdout holds exactly one JSON document and decodes it.
119 func decodeOneReport(t *testing.T, raw string) brokerStatusReport {124 func decodeOneReport(t *testing.T, raw string) brokerStatusReport {
120 t.Helper()125 t.Helper()
121 trimmed := strings.TrimSpace(raw)126 trimmed := strings.TrimSpace(raw)
122- require.Equal(t, 1, strings.Count(trimmed, "\n")+1, "stdout must hold exactly one report line")127+ require.Equal(t, 1, strings.Count(trimmed, "\n")+1)
123 var report brokerStatusReport128 var report brokerStatusReport
124 require.NoError(t, json.Unmarshal([]byte(trimmed), &report))129 require.NoError(t, json.Unmarshal([]byte(trimmed), &report))
125 return report130 return report
126 }131 }
127 132
128-func TestParseBrokerLauncherArgs(t *testing.T) {
129- tests := []struct {
130- name string
131- args []string
132- want brokerLauncherOptions
133- wantErr string
134- }{
135- {name: "root only", args: []string{"--offline-root", "/srv/sandbox"}, want: brokerLauncherOptions{offlineRoot: "/srv/sandbox"}},
136- {
137- name: "root and grace",
138- args: []string{"--offline-root", "/srv/sandbox", "--idle-grace", "90s"},
139- want: brokerLauncherOptions{offlineRoot: "/srv/sandbox", idleGrace: 90 * time.Second},
140- },
141- {name: "missing root", args: nil, wantErr: "requires --offline-root"},
142- {name: "missing root value", args: []string{"--offline-root"}, wantErr: "requires a path"},
143- {name: "duplicate root", args: []string{"--offline-root", "/a", "--offline-root", "/b"}, wantErr: "duplicate --offline-root"},
144- {name: "duplicate grace", args: []string{"--offline-root", "/a", "--idle-grace", "1s", "--idle-grace", "2s"}, wantErr: "duplicate --idle-grace"},
145- {name: "zero grace", args: []string{"--offline-root", "/a", "--idle-grace", "0s"}, wantErr: "must be positive"},
146- {name: "invalid grace", args: []string{"--offline-root", "/a", "--idle-grace", "soon"}, wantErr: "not a duration"},
147- {name: "ensure is not a launcher flag", args: []string{"--offline-root", "/a", "--ensure"}, wantErr: "unknown flag"},
148- {name: "positional", args: []string{"--offline-root", "/a", "extra"}, wantErr: "positional"},
149- }
150- for _, tt := range tests {
151- t.Run(tt.name, func(t *testing.T) {
152- command, err := parseBrokerLauncherArgs(tt.args)
153- if tt.wantErr != "" {
154- require.Error(t, err)
155- require.Contains(t, err.Error(), tt.wantErr)
156- return
157- }
158- require.NoError(t, err)
159- require.Equal(t, kindBrokerLauncher, command.kind)
160- require.Equal(t, tt.want, command.brokerLauncher)
161- })
162- }
163-}
164-
165-func TestParseBrokerStatusArgs(t *testing.T) {
166- tests := []struct {
167- name string
168- args []string
169- want brokerStatusOptions
170- wantErr string
171- }{
172- {
173- name: "root only defaults the timeout",
174- args: []string{"--offline-root", "/srv/sandbox"},
175- want: brokerStatusOptions{offlineRoot: "/srv/sandbox", timeout: brokerStatusDefaultTimeout},
176- },
177- {
178- name: "ensure and timeout",
179- args: []string{"--offline-root", "/a", "--ensure", "--timeout", "250ms"},
180- want: brokerStatusOptions{offlineRoot: "/a", ensure: true, timeout: 250 * time.Millisecond},
181- },
182- {name: "missing root", args: []string{"--ensure"}, wantErr: "requires --offline-root"},
183- {name: "duplicate root", args: []string{"--offline-root", "/a", "--offline-root", "/b"}, wantErr: "duplicate --offline-root"},
184- {name: "duplicate ensure", args: []string{"--offline-root", "/a", "--ensure", "--ensure"}, wantErr: "duplicate --ensure"},
185- {name: "duplicate timeout", args: []string{"--offline-root", "/a", "--timeout", "1s", "--timeout", "2s"}, wantErr: "duplicate --timeout"},
186- {name: "missing timeout value", args: []string{"--offline-root", "/a", "--timeout"}, wantErr: "requires a duration"},
187- {name: "zero timeout", args: []string{"--offline-root", "/a", "--timeout", "0s"}, wantErr: "must be positive"},
188- {name: "invalid timeout", args: []string{"--offline-root", "/a", "--timeout", "soon"}, wantErr: "not a duration"},
189- {name: "idle grace is not a status flag", args: []string{"--offline-root", "/a", "--idle-grace", "1s"}, wantErr: "unknown flag"},
190- {name: "positional", args: []string{"--offline-root", "/a", "extra"}, wantErr: "positional"},
191- }
192- for _, tt := range tests {
193- t.Run(tt.name, func(t *testing.T) {
194- command, err := parseBrokerStatusArgs(tt.args)
195- if tt.wantErr != "" {
196- require.Error(t, err)
197- require.Contains(t, err.Error(), tt.wantErr)
198- return
199- }
200- require.NoError(t, err)
201- require.Equal(t, kindBrokerStatus, command.kind)
202- require.Equal(t, tt.want, command.brokerStatus)
203- })
204- }
205-}
206-
207-func TestBrokerLauncherAndStatusAreHiddenFromPublicHelp(t *testing.T) {
208- require.NotContains(t, usageText, brokerLauncherCommand)
209- require.NotContains(t, usageText, brokerStatusCommand)
210- _, err := parseArgs([]string{brokerLauncherCommand, "--offline-root", "/srv/sandbox"})
211- require.NoError(t, err)
212- _, err = parseArgs([]string{brokerStatusCommand, "--offline-root", "/srv/sandbox", "--ensure"})
213- require.NoError(t, err)
214- _, err = parseArgs([]string{"--help"})
215- require.NoError(t, err)
216-}
217-
218 func TestEffectiveIdleGrace(t *testing.T) {133 func TestEffectiveIdleGrace(t *testing.T) {
219- layout := emptySandboxRoot(t, "90s")134+ layout := emptyProductionBrokerLayout(t, "90s")
220- config, err := brokerconfig.Load(layout)135+ config, err := brokerconfig.LoadProduction(layout, productionBrokerConfigPath(), "", localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
221 require.NoError(t, err)136 require.NoError(t, err)
222- unprovisioned := emptySandboxRoot(t, "")137+ unprovisioned := emptyProductionBrokerLayout(t, "")
223- plain, err := brokerconfig.Load(unprovisioned)138+ plain, err := brokerconfig.LoadProduction(unprovisioned, productionBrokerConfigPath(), "", localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
224 require.NoError(t, err)139 require.NoError(t, err)
225 140
226 tests := []struct {141 tests := []struct {
@@ -251,71 +166,27 @@ func TestEffectiveIdleGrace(t *testing.T) {
251 }166 }
252 167
253 func TestBrokerServeRefusesConflictingIdleGrace(t *testing.T) {168 func TestBrokerServeRefusesConflictingIdleGrace(t *testing.T) {
254- layout := emptySandboxRoot(t, "90s")169+ layout := emptyProductionBrokerLayout(t, "90s")
255 ctx, cancel := context.WithCancel(context.Background())170 ctx, cancel := context.WithCancel(context.Background())
256 defer cancel()171 defer cancel()
257 deps, _ := testBrokerServeDeps(newSandboxClock())172 deps, _ := testBrokerServeDeps(newSandboxClock())
258- err := runBrokerServe(ctx, brokerServeOptions{offlineRoot: layout.Root, idleGrace: time.Minute}, deps)173+ err := runBrokerServe(ctx, brokerServeOptions{production: true, idleGrace: time.Minute}, deps)
259 require.Error(t, err)174 require.Error(t, err)
260 require.Contains(t, err.Error(), "conflicts with the provisioned idle grace")175 require.Contains(t, err.Error(), "conflicts with the provisioned idle grace")
261 require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"), "a refused serve must not take the lifetime lock")176 require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"), "a refused serve must not take the lifetime lock")
262 }177 }
263 178
264 func TestBrokerLauncherRefusesConflictingIdleGraceBeforeSpawning(t *testing.T) {179 func TestBrokerLauncherRefusesConflictingIdleGraceBeforeSpawning(t *testing.T) {
265- layout := emptySandboxRoot(t, "90s")180+ layout := emptyProductionBrokerLayout(t, "90s")
266- err := runBrokerLauncher(brokerLauncherOptions{offlineRoot: layout.Root, idleGrace: time.Minute})181+ config, err := brokerconfig.LoadProduction(layout, productionBrokerConfigPath(), "", localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
182+ require.NoError(t, err)
183+ _, err = effectiveIdleGrace(config, time.Minute)
267 require.Error(t, err)184 require.Error(t, err)
268 require.Contains(t, err.Error(), "conflicts with the provisioned idle grace")185 require.Contains(t, err.Error(), "conflicts with the provisioned idle grace")
269 }186 }
270 187
271-func TestBrokerStatusDialOnlyReportsOfflineWithoutCreatingAnything(t *testing.T) {
272- layout := emptySandboxRoot(t, "")
273- deps := defaultBrokerStatusDeps()
274- var out bytes.Buffer
275- deps.stdout = &out
276-
277- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, timeout: time.Second}, deps))
278- report := decodeOneReport(t, out.String())
279- require.Equal(t, "offline", report.Status)
280- require.Equal(t, brokeripc.SocketPath(layout.Runtime), report.Endpoint)
281- require.Zero(t, report.Epoch)
282- require.Zero(t, report.Revision)
283- require.Zero(t, report.HostCount)
284-
285- require.NoDirExists(t, layout.Runtime, "dial-only status must not create the runtime directory")
286- require.NoDirExists(t, layout.Spawn, "dial-only status must not create the spawn directory")
287-}
288-
289-func TestBrokerStatusDialOnlyReadyAgainstLiveSandbox(t *testing.T) {
290- root, prodRuntime, prodState := isolateSandboxEnv(t)
291- fixture := newBrokerMuxFixture(t, brokerTestPolicy())
292- require.NoError(t, os.MkdirAll(root, 0o700))
293- writeSandboxConfig(t, root, sandboxRegistrationDocument(fixture.route))
294-
295- clk := newSandboxClock()
296- serveDeps, ready := testBrokerServeDeps(clk)
297- ctx, cancel := context.WithCancel(context.Background())
298- defer cancel()
299- done := runSandbox(ctx, brokerServeOptions{offlineRoot: root}, serveDeps)
300- awaitSandboxReady(t, ready, done)
301-
302- deps := defaultBrokerStatusDeps()
303- var out bytes.Buffer
304- deps.stdout = &out
305- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: root, timeout: time.Second}, deps))
306- report := decodeOneReport(t, out.String())
307- require.Equal(t, "ready", report.Status)
308- require.Positive(t, report.Epoch)
309- require.Positive(t, report.Revision)
310- // Provisioned endpoints seed durable broker membership before serving.
311- require.Equal(t, 1, report.HostCount)
312-
313- awaitSandboxCancel(t, cancel, done)
314- requireProductionUntouched(t, prodRuntime, prodState)
315-}
316-
317 func TestBrokerStatusEnsureSpawnsOnceThenReportsReady(t *testing.T) {188 func TestBrokerStatusEnsureSpawnsOnceThenReportsReady(t *testing.T) {
318- layout := emptySandboxRoot(t, "")189+ layout := emptyProductionBrokerLayout(t, "")
319 deps, rec := testStatusDeps(t)190 deps, rec := testStatusDeps(t)
320 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {191 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {
321 if rec.spawns.Load() > 0 {192 if rec.spawns.Load() > 0 {
@@ -323,17 +194,18 @@ func TestBrokerStatusEnsureSpawnsOnceThenReportsReady(t *testing.T) {
323 }194 }
324 return brokerStatusReport{}, absentError(socketPath)195 return brokerStatusReport{}, absentError(socketPath)
325 })196 })
326- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps))197+ report, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
198+ require.NoError(t, err)
327 199
328 require.Equal(t, int32(1), rec.spawns.Load(), "exactly one spawn is expected")200 require.Equal(t, int32(1), rec.spawns.Load(), "exactly one spawn is expected")
329 require.Equal(t, int32(1), rec.elections.Load(), "exactly one election is expected")201 require.Equal(t, int32(1), rec.elections.Load(), "exactly one election is expected")
330 require.Len(t, rec.locks, 1)202 require.Len(t, rec.locks, 1)
331 require.Equal(t, int32(1), rec.locks[0].releases.Load(), "the election must be released after readiness")203 require.Equal(t, int32(1), rec.locks[0].releases.Load(), "the election must be released after readiness")
332- require.Equal(t, "ready", rec.report(t).Status)204+ require.Equal(t, "ready", report.Status)
333 }205 }
334 206
335 func TestBrokerStatusEnsureWaitsForAnotherSpawner(t *testing.T) {207 func TestBrokerStatusEnsureWaitsForAnotherSpawner(t *testing.T) {
336- layout := emptySandboxRoot(t, "")208+ layout := emptyProductionBrokerLayout(t, "")
337 var readyAfter int32 = 4209 var readyAfter int32 = 4
338 deps, rec := testStatusDeps(t)210 deps, rec := testStatusDeps(t)
339 rec.setProbe(&deps, func(attempt int, socketPath string) (brokerStatusReport, error) {211 rec.setProbe(&deps, func(attempt int, socketPath string) (brokerStatusReport, error) {
@@ -346,15 +218,16 @@ func TestBrokerStatusEnsureWaitsForAnotherSpawner(t *testing.T) {
346 rec.elections.Add(1)218 rec.elections.Add(1)
347 return nil, lifecycle.ErrBusy219 return nil, lifecycle.ErrBusy
348 }220 }
349- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps))221+ report, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
222+ require.NoError(t, err)
350 223
351 require.Zero(t, rec.spawns.Load(), "a waiter must never spawn while another caller is elected")224 require.Zero(t, rec.spawns.Load(), "a waiter must never spawn while another caller is elected")
352 require.Positive(t, rec.elections.Load())225 require.Positive(t, rec.elections.Load())
353- require.Equal(t, "ready", rec.report(t).Status)226+ require.Equal(t, "ready", report.Status)
354 }227 }
355 228
356 func TestBrokerStatusEnsureDefersToLifetimeOwnerBeforeSpawning(t *testing.T) {229 func TestBrokerStatusEnsureDefersToLifetimeOwnerBeforeSpawning(t *testing.T) {
357- layout := emptySandboxRoot(t, "")230+ layout := emptyProductionBrokerLayout(t, "")
358 var ownerCalls int32231 var ownerCalls int32
359 deps, rec := testStatusDeps(t)232 deps, rec := testStatusDeps(t)
360 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {233 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {
@@ -369,13 +242,14 @@ func TestBrokerStatusEnsureDefersToLifetimeOwnerBeforeSpawning(t *testing.T) {
369 }242 }
370 return false, nil243 return false, nil
371 }244 }
372- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps))245+ _, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
246+ require.NoError(t, err)
373 require.Equal(t, int32(1), rec.spawns.Load(), "a live lifetime owner must suppress the spawn until it is gone")247 require.Equal(t, int32(1), rec.spawns.Load(), "a live lifetime owner must suppress the spawn until it is gone")
374- require.Equal(t, int32(1), rec.elections.Load(), "the election is taken only after the live owner releases the sandbox")248+ require.Equal(t, int32(1), rec.elections.Load(), "the election is taken only after the live owner releases the broker")
375 }249 }
376 250
377 func TestBrokerStatusEnsureRecoversAfterSpawnerDeath(t *testing.T) {251 func TestBrokerStatusEnsureRecoversAfterSpawnerDeath(t *testing.T) {
378- layout := emptySandboxRoot(t, "")252+ layout := emptyProductionBrokerLayout(t, "")
379 var electionAttempts int32253 var electionAttempts int32
380 deps, rec := testStatusDeps(t)254 deps, rec := testStatusDeps(t)
381 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {255 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {
@@ -394,210 +268,96 @@ func TestBrokerStatusEnsureRecoversAfterSpawnerDeath(t *testing.T) {
394 rec.locks = append(rec.locks, lock)268 rec.locks = append(rec.locks, lock)
395 return lock, nil269 return lock, nil
396 }270 }
397- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps))271+ report, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
272+ require.NoError(t, err)
398 require.Equal(t, int32(1), rec.spawns.Load(), "a waiter must take over once the elected spawner dies")273 require.Equal(t, int32(1), rec.spawns.Load(), "a waiter must take over once the elected spawner dies")
399- require.Equal(t, "ready", rec.report(t).Status)274+ require.Equal(t, "ready", report.Status)
400 }275 }
401 276
402 func TestBrokerStatusEnsureIncompatibleEndpointFailsWithoutSpawning(t *testing.T) {277 func TestBrokerStatusEnsureIncompatibleEndpointFailsWithoutSpawning(t *testing.T) {
403- layout := emptySandboxRoot(t, "")278+ layout := emptyProductionBrokerLayout(t, "")
404 deps, rec := testStatusDeps(t)279 deps, rec := testStatusDeps(t)
405 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {280 rec.setProbe(&deps, func(_ int, socketPath string) (brokerStatusReport, error) {
406 return brokerStatusReport{}, incompatibleError(socketPath, errors.New("foreign endpoint"))281 return brokerStatusReport{}, incompatibleError(socketPath, errors.New("foreign endpoint"))
407 })282 })
408- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps)283+ _, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
409 require.ErrorIs(t, err, errBrokerIncompatible)284 require.ErrorIs(t, err, errBrokerIncompatible)
410 require.Zero(t, rec.spawns.Load())285 require.Zero(t, rec.spawns.Load())
411 require.Zero(t, rec.elections.Load())286 require.Zero(t, rec.elections.Load())
412- report := rec.report(t)
413- require.Equal(t, "offline", report.Status)
414- require.Equal(t, 3, exitCode(err))
415 }287 }
416 288
417 func TestBrokerStatusEnsureBoundedTimeoutReportsOffline(t *testing.T) {289 func TestBrokerStatusEnsureBoundedTimeoutReportsOffline(t *testing.T) {
418- layout := emptySandboxRoot(t, "")290+ layout := emptyProductionBrokerLayout(t, "")
419 deps, rec := testStatusDeps(t)291 deps, rec := testStatusDeps(t)
420- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: 50 * time.Millisecond}, deps)292+ _, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, 50*time.Millisecond), deps)
421 require.ErrorIs(t, err, errBrokerNotReady)293 require.ErrorIs(t, err, errBrokerNotReady)
422- require.Equal(t, 3, exitCode(err))
423 require.Equal(t, int32(1), rec.spawns.Load())294 require.Equal(t, int32(1), rec.spawns.Load())
424- require.Equal(t, "offline", rec.report(t).Status)
425 }295 }
426 296
427 func TestBrokerStatusEnsureSpawnFailureIsReported(t *testing.T) {297 func TestBrokerStatusEnsureSpawnFailureIsReported(t *testing.T) {
428- layout := emptySandboxRoot(t, "")298+ layout := emptyProductionBrokerLayout(t, "")
429 spawnErr := errors.New("launcher exec failed")299 spawnErr := errors.New("launcher exec failed")
430- deps, rec := testStatusDeps(t)300+ deps, _ := testStatusDeps(t)
431 deps.spawn = func(context.Context, string, time.Duration, bool) error {301 deps.spawn = func(context.Context, string, time.Duration, bool) error {
432- rec.spawns.Add(1)
433 return spawnErr302 return spawnErr
434 }303 }
435- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps)304+ _, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
436 require.ErrorIs(t, err, spawnErr)305 require.ErrorIs(t, err, spawnErr)
437- require.Equal(t, 3, exitCode(err))
438- require.Equal(t, "offline", rec.report(t).Status)
439 }306 }
440 307
441-func TestBrokerStatusEnsureRejectsUnsafeRoot(t *testing.T) {308+// A corrupt production broker.json is rejected before broker ownership.
442- isolateSandboxEnv(t)309+func TestBrokerEnsureProductionConfigFailure(t *testing.T) {
443- tests := []struct {310+ layout := emptyProductionBrokerLayout(t, "")
444- name string311+ require.NoError(t, os.WriteFile(productionBrokerConfigPath(), []byte("not json"), 0o600))
445- root string312+ deps, _ := testBrokerServeDeps(newSandboxClock())
446- }{313+ err := runBrokerServe(context.Background(), brokerServeOptions{production: true}, deps)
447- {name: "relative root", root: "relative"},
448- {name: "production overlap", root: ipc.SocketDir()},
449- }
450- for _, tt := range tests {
451- t.Run(tt.name, func(t *testing.T) {
452- deps, rec := testStatusDeps(t)
453- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: tt.root, ensure: true, timeout: time.Second}, deps)
454- require.Error(t, err)
455- require.Equal(t, 3, exitCode(err), "a layout failure must still exit 3")
456- require.Equal(t, "offline", rec.report(t).Status, "a layout failure must still emit one offline report")
457- require.Zero(t, rec.spawns.Load())
458- })
459- }
460-}
461-
462-// TestBrokerStatusEnsureConfigFailureReportsOffline proves a malformed or
463-// missing configuration still leaves the caller with one offline JSON document
464-// and a code-3 failure instead of a bare error.
465-func TestBrokerStatusEnsureConfigFailureReportsOffline(t *testing.T) {
466- isolateSandboxEnv(t)
467- root := filepath.Join(shortTempDir(t, "vevm"), "s")
468- require.NoError(t, os.MkdirAll(root, 0o700))
469- deps, rec := testStatusDeps(t)
470- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: root, ensure: true, timeout: time.Second}, deps)
471 require.Error(t, err)314 require.Error(t, err)
472- require.Contains(t, err.Error(), brokerconfig.ConfigFileName)315+ require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"))
473- require.Equal(t, 3, exitCode(err))
474- require.Equal(t, "offline", rec.report(t).Status)
475- require.Zero(t, rec.spawns.Load())
476 }316 }
477 317
478-// TestBrokerStatusEnsureSecuresRootBeforeSpawning proves the ensure path318+// TestBrokerStatusEnsureSecuresRootBeforeSpawning rejects an insecure
479-// validates and secures the sandbox root exactly as the launcher does, before319+// production spawn directory before electing a launcher.
480-// the spawn directory is created or any election lock is taken.
481 func TestBrokerStatusEnsureSecuresRootBeforeSpawning(t *testing.T) {320 func TestBrokerStatusEnsureSecuresRootBeforeSpawning(t *testing.T) {
482- layout := emptySandboxRoot(t, "")321+ layout := emptyProductionBrokerLayout(t, "")
483- require.NoError(t, os.Chmod(layout.Root, 0o755))322+ require.NoError(t, os.MkdirAll(layout.Runtime, 0o700))
323+ require.NoError(t, os.MkdirAll(layout.Spawn, 0o700))
324+ require.NoError(t, os.Chmod(layout.Spawn, 0o755))
484 deps, rec := testStatusDeps(t)325 deps, rec := testStatusDeps(t)
485- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps)326+ _, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, time.Second), deps)
486 require.Error(t, err)327 require.Error(t, err)
487 require.Contains(t, err.Error(), "0755")328 require.Contains(t, err.Error(), "0755")
488- require.Equal(t, 3, exitCode(err))
489- require.Equal(t, "offline", rec.report(t).Status)
490 require.Zero(t, rec.spawns.Load())329 require.Zero(t, rec.spawns.Load())
491 require.Zero(t, rec.elections.Load())330 require.Zero(t, rec.elections.Load())
492- require.NoFileExists(t, filepath.Join(layout.Runtime, brokerconfig.SpawnDirName), "an insecure root must be refused before the spawn directory is created")331+ require.NoFileExists(t, filepath.Join(layout.Spawn, "lifecycle.lock"), "an insecure spawn directory must be refused before election")
493 }332 }
494 333
495 // TestBrokerStatusEnsureDeadlineBoundsSpawnWait proves the overall ensure334 // TestBrokerStatusEnsureDeadlineBoundsSpawnWait proves the overall ensure
496 // deadline bounds a launcher spawn that never returns, so a blocking launcher335 // deadline bounds a launcher spawn that never returns, so a blocking launcher
497-// cannot wedge the status command.336+// cannot wedge connect-or-spawn.
498 func TestBrokerStatusEnsureDeadlineBoundsSpawnWait(t *testing.T) {337 func TestBrokerStatusEnsureDeadlineBoundsSpawnWait(t *testing.T) {
499- layout := emptySandboxRoot(t, "")338+ layout := emptyProductionBrokerLayout(t, "")
500- deps, rec := testStatusDeps(t)339+ deps, _ := testStatusDeps(t)
501 deps.spawn = func(ctx context.Context, _ string, _ time.Duration, _ bool) error {340 deps.spawn = func(ctx context.Context, _ string, _ time.Duration, _ bool) error {
502 <-ctx.Done()341 <-ctx.Done()
503 return ctx.Err()342 return ctx.Err()
504 }343 }
505 start := time.Now()344 start := time.Now()
506- err := runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: 200 * time.Millisecond}, deps)345+ _, err := ensureBrokerReady(context.Background(), productionStatusRequest(layout, 200*time.Millisecond), deps)
507 require.ErrorIs(t, err, context.DeadlineExceeded)346 require.ErrorIs(t, err, context.DeadlineExceeded)
508- require.Equal(t, 3, exitCode(err))
509 require.Less(t, time.Since(start), 5*time.Second, "the overall deadline must bound the launcher spawn wait")347 require.Less(t, time.Since(start), 5*time.Second, "the overall deadline must bound the launcher spawn wait")
510- require.Equal(t, "offline", rec.report(t).Status)
511-}
512-
513-// TestBrokerStatusDialOnlyAppliesTimeout proves the dial-only probe is bounded
514-// by --timeout as one absolute bound and is never silently replaced by the
515-// ensure probe budget.
516-func TestBrokerStatusDialOnlyAppliesTimeout(t *testing.T) {
517- tests := []struct {
518- name string
519- timeout time.Duration
520- }{
521- {name: "short timeout is the probe bound", timeout: 250 * time.Millisecond},
522- {name: "default timeout is the probe bound", timeout: brokerStatusDefaultTimeout},
523- }
524- for _, tt := range tests {
525- t.Run(tt.name, func(t *testing.T) {
526- layout := emptySandboxRoot(t, "")
527- deps, _ := testStatusDeps(t)
528- var observed time.Duration
529- sawDeadline := false
530- deps.probe = func(ctx context.Context, socketPath string) (brokerStatusReport, error) {
531- deadline, ok := ctx.Deadline()
532- sawDeadline = ok
533- if ok {
534- observed = time.Until(deadline)
535- }
536- return brokerStatusReport{}, absentError(socketPath)
537- }
538- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, timeout: tt.timeout}, deps))
539- require.True(t, sawDeadline, "dial-only must bound the probe with the timeout context")
540- require.InDelta(t, tt.timeout.Seconds(), observed.Seconds(), 0.5)
541- })
542- }
543-}
544-
545-// TestBrokerStatusDialOnlyBoundedByTimeout proves a live-but-silent endpoint is
546-// bounded by --timeout rather than the ensure probe budget.
547-func TestBrokerStatusDialOnlyBoundedByTimeout(t *testing.T) {
548- layout := emptySandboxRoot(t, "")
549- require.NoError(t, os.MkdirAll(layout.Runtime, 0o700))
550- _ = bindUnixSocket(t, brokeripc.SocketPath(layout.Runtime))
551- deps := defaultBrokerStatusDeps()
552- var out bytes.Buffer
553- deps.stdout = &out
554- start := time.Now()
555- require.NoError(t, runBrokerStatus(context.Background(), brokerStatusOptions{offlineRoot: layout.Root, timeout: 300 * time.Millisecond}, deps))
556- require.Less(t, time.Since(start), time.Second, "the dial-only probe must be bounded by --timeout, not the ensure budget")
557- require.Equal(t, "offline", decodeOneReport(t, out.String()).Status)
558 }348 }
559 349
560 func TestBrokerStatusEnsureHonorsCancellation(t *testing.T) {350 func TestBrokerStatusEnsureHonorsCancellation(t *testing.T) {
561- layout := emptySandboxRoot(t, "")351+ layout := emptyProductionBrokerLayout(t, "")
562 deps, _ := testStatusDeps(t)352 deps, _ := testStatusDeps(t)
563 ctx, cancel := context.WithCancel(context.Background())353 ctx, cancel := context.WithCancel(context.Background())
564 cancel()354 cancel()
565- err := runBrokerStatus(ctx, brokerStatusOptions{offlineRoot: layout.Root, ensure: true, timeout: time.Second}, deps)355+ _, err := ensureBrokerReady(ctx, productionStatusRequest(layout, time.Second), deps)
566 require.ErrorIs(t, err, context.Canceled)356 require.ErrorIs(t, err, context.Canceled)
567 }357 }
568 358
569-func TestBrokerStatusReportIsBoundedJSON(t *testing.T) {
570- var out bytes.Buffer
571- require.NoError(t, writeBrokerStatusReport(&out, brokerStatusReport{
572- Status: "ready", Endpoint: brokeripc.SocketPath("/tmp/sandbox/runtime"), Epoch: 3, Revision: 9, HostCount: 4,
573- }))
574- var fields map[string]json.RawMessage
575- require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(out.String())), &fields))
576- require.Equal(t, []string{"endpoint", "epoch", "host_count", "revision", "status"}, sortedKeys(fields))
577- require.LessOrEqual(t, out.Len(), brokerStatusMaxReportBytes)
578-}
579-
580-// sortedKeys returns the sorted keys of one JSON object.
581-func sortedKeys(fields map[string]json.RawMessage) []string {
582- keys := make([]string, 0, len(fields))
583- for key := range fields {
584- keys = append(keys, key)
585- }
586- slicesSort(keys)
587- return keys
588-}
589-
590-// slicesSort is a tiny insertion sort so the test file needs no extra import.
591-func slicesSort(values []string) {
592- for i := 1; i < len(values); i++ {
593- for j := i; j > 0 && values[j] < values[j-1]; j-- {
594- values[j], values[j-1] = values[j-1], values[j]
595- }
596- }
597-}
598-
599 func TestProbeBrokerStatusClassifiesAbsenceAndIncompatibility(t *testing.T) {359 func TestProbeBrokerStatusClassifiesAbsenceAndIncompatibility(t *testing.T) {
600- layout := emptySandboxRoot(t, "")360+ layout := emptyProductionBrokerLayout(t, "")
601 socketPath := brokeripc.SocketPath(layout.Runtime)361 socketPath := brokeripc.SocketPath(layout.Runtime)
602 362
603 t.Run("missing endpoint is absence", func(t *testing.T) {363 t.Run("missing endpoint is absence", func(t *testing.T) {
@@ -637,7 +397,7 @@ func TestProbeBrokerStatusClassifiesAbsenceAndIncompatibility(t *testing.T) {
637 }397 }
638 398
639 func TestProbeBrokerStatusSilentRegistrationIsBounded(t *testing.T) {399 func TestProbeBrokerStatusSilentRegistrationIsBounded(t *testing.T) {
640- layout := emptySandboxRoot(t, "")400+ layout := emptyProductionBrokerLayout(t, "")
641 socketPath := brokeripc.SocketPath(layout.Runtime)401 socketPath := brokeripc.SocketPath(layout.Runtime)
642 require.NoError(t, os.MkdirAll(layout.Runtime, 0o700))402 require.NoError(t, os.MkdirAll(layout.Runtime, 0o700))
643 // A listener that never reads or answers: the dial succeeds and the broker403 // A listener that never reads or answers: the dial succeeds and the broker
@@ -674,11 +434,3 @@ func bindStaleUnixSocket(t *testing.T, path string) {
674 require.NoError(t, listener.Close())434 require.NoError(t, listener.Close())
675 require.FileExists(t, path)435 require.FileExists(t, path)
676 }436 }
677-
678-// Ensure the helper fixture contract used elsewhere stays coherent.
679-func TestStatusFixtureDocument(t *testing.T) {
680- document := sandboxEmptyDocument("1s")
681- require.Equal(t, brokerconfig.Marker, document["marker"])
682- require.Equal(t, "1s", document["idleGrace"])
683- require.Empty(t, sandboxEmptyDocument("")["idleGrace"])
684-}

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