test(app): run broker serve tests against the production layout
- commit
- c00536eab682de0f82f8cadd9a59e01608dac974
- author
- brice · · 2026-09-23 15:00 UTC
- parent
- b5b719e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +36 -121
M internal/app/broker_offline_test.go +36 -121
| @@ -215,101 +215,26 @@ func TestDrainBrokerAcceptClassifiesTerminalFailure(t *testing.T) { | |||
| 215 | 215 | } | |
| 216 | 216 | } | |
| 217 | 217 | ||
| 218 | - | func TestParseBrokerServeArgs(t *testing.T) { | |
| 219 | - | tests := []struct { | |
| 220 | - | name string | |
| 221 | - | args []string | |
| 222 | - | want brokerServeOptions | |
| 223 | - | wantErr string | |
| 224 | - | }{ | |
| 225 | - | {name: "root only", args: []string{"--offline-root", "/srv/sandbox"}, want: brokerServeOptions{offlineRoot: "/srv/sandbox"}}, | |
| 226 | - | { | |
| 227 | - | name: "root and grace", | |
| 228 | - | args: []string{"--offline-root", "/srv/sandbox", "--idle-grace", "90s"}, | |
| 229 | - | want: brokerServeOptions{offlineRoot: "/srv/sandbox", idleGrace: 90 * time.Second}, | |
| 230 | - | }, | |
| 231 | - | {name: "missing root", args: nil, wantErr: "requires --offline-root"}, | |
| 232 | - | {name: "missing root value", args: []string{"--offline-root"}, wantErr: "requires a path"}, | |
| 233 | - | {name: "empty root value", args: []string{"--offline-root", ""}, wantErr: "requires a path"}, | |
| 234 | - | {name: "duplicate root", args: []string{"--offline-root", "/a", "--offline-root", "/b"}, wantErr: "duplicate --offline-root"}, | |
| 235 | - | {name: "duplicate grace", args: []string{"--offline-root", "/a", "--idle-grace", "1s", "--idle-grace", "2s"}, wantErr: "duplicate --idle-grace"}, | |
| 236 | - | {name: "missing grace value", args: []string{"--offline-root", "/a", "--idle-grace"}, wantErr: "requires a duration"}, | |
| 237 | - | {name: "invalid duration", args: []string{"--offline-root", "/a", "--idle-grace", "soon"}, wantErr: "not a duration"}, | |
| 238 | - | {name: "zero duration", args: []string{"--offline-root", "/a", "--idle-grace", "0s"}, wantErr: "must be positive"}, | |
| 239 | - | {name: "negative duration", args: []string{"--offline-root", "/a", "--idle-grace", "-1s"}, wantErr: "must be positive"}, | |
| 240 | - | {name: "unknown flag", args: []string{"--offline-root", "/a", "--verbose"}, wantErr: "unknown flag"}, | |
| 241 | - | {name: "positional", args: []string{"--offline-root", "/a", "extra"}, wantErr: "positional"}, | |
| 242 | - | {name: "equals form is refused", args: []string{"--offline-root=/a"}, wantErr: "unknown flag"}, | |
| 243 | - | } | |
| 244 | - | for _, tt := range tests { | |
| 245 | - | t.Run(tt.name, func(t *testing.T) { | |
| 246 | - | command, err := parseBrokerServeArgs(tt.args) | |
| 247 | - | if tt.wantErr != "" { | |
| 248 | - | require.Error(t, err) | |
| 249 | - | require.Contains(t, err.Error(), tt.wantErr) | |
| 250 | - | return | |
| 251 | - | } | |
| 252 | - | require.NoError(t, err) | |
| 253 | - | require.Equal(t, kindBrokerServe, command.kind) | |
| 254 | - | require.Equal(t, tt.want, command.brokerServe) | |
| 255 | - | }) | |
| 256 | - | } | |
| 257 | - | } | |
| 258 | - | ||
| 259 | - | func TestBrokerServeIsHiddenFromPublicHelp(t *testing.T) { | |
| 260 | - | require.NotContains(t, usageText, brokerServeCommand) | |
| 261 | - | _, err := parseArgs([]string{brokerServeCommand, "--offline-root", "/srv/sandbox"}) | |
| 262 | - | require.NoError(t, err) | |
| 263 | - | _, err = parseArgs([]string{"--help"}) | |
| 264 | - | require.NoError(t, err) | |
| 265 | - | } | |
| 266 | - | ||
| 267 | - | func TestBrokerServeRejectsUnsafeRoot(t *testing.T) { | |
| 268 | - | root, _, _ := isolateSandboxEnv(t) | |
| 269 | - | ctx := context.Background() | |
| 218 | + | func TestProductionBrokerServeRejectsUnsafeRoot(t *testing.T) { | |
| 219 | + | layout := emptyProductionBrokerLayout(t, "") | |
| 220 | + | require.NoError(t, os.MkdirAll(layout.Root, 0o755)) | |
| 270 | 221 | deps, _ := testBrokerServeDeps(newSandboxClock()) | |
| 271 | - | ||
| 272 | - | t.Run("relative root", func(t *testing.T) { | |
| 273 | - | err := runBrokerServe(ctx, brokerServeOptions{offlineRoot: "relative"}, deps) | |
| 274 | - | require.Error(t, err) | |
| 275 | - | require.Contains(t, err.Error(), "not absolute") | |
| 276 | - | }) | |
| 277 | - | t.Run("unclean root", func(t *testing.T) { | |
| 278 | - | err := runBrokerServe(ctx, brokerServeOptions{offlineRoot: root + "/../sandbox"}, deps) | |
| 279 | - | require.Error(t, err) | |
| 280 | - | require.Contains(t, err.Error(), "not clean") | |
| 281 | - | }) | |
| 282 | - | t.Run("production overlap", func(t *testing.T) { | |
| 283 | - | err := runBrokerServe(ctx, brokerServeOptions{offlineRoot: ipc.SocketDir()}, deps) | |
| 284 | - | require.Error(t, err) | |
| 285 | - | require.Contains(t, err.Error(), "overlaps production path") | |
| 286 | - | }) | |
| 287 | - | t.Run("symlinked root", func(t *testing.T) { | |
| 288 | - | link := filepath.Join(t.TempDir(), "link") | |
| 289 | - | require.NoError(t, os.Symlink(t.TempDir(), link)) | |
| 290 | - | err := runBrokerServe(ctx, brokerServeOptions{offlineRoot: link}, deps) | |
| 291 | - | require.Error(t, err) | |
| 292 | - | require.Contains(t, err.Error(), "symlink") | |
| 293 | - | }) | |
| 294 | - | t.Run("missing config", func(t *testing.T) { | |
| 295 | - | err := runBrokerServe(ctx, brokerServeOptions{offlineRoot: root}, deps) | |
| 296 | - | require.Error(t, err) | |
| 297 | - | require.Contains(t, err.Error(), brokerconfig.ConfigFileName) | |
| 298 | - | }) | |
| 222 | + | err := runBrokerServe(context.Background(), brokerServeOptions{production: true}, deps) | |
| 223 | + | require.ErrorContains(t, err, "0755") | |
| 224 | + | require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock")) | |
| 299 | 225 | } | |
| 300 | 226 | ||
| 301 | 227 | func TestBrokerServeRoundTrip(t *testing.T) { | |
| 302 | - | root, prodRuntime, prodState := isolateSandboxEnv(t) | |
| 228 | + | emptyProductionBrokerLayout(t, "") | |
| 303 | 229 | policy := brokerTestPolicy() | |
| 304 | 230 | fixture := newBrokerMuxFixture(t, policy) | |
| 305 | - | require.NoError(t, os.MkdirAll(root, 0o700)) | |
| 306 | - | writeSandboxConfig(t, root, sandboxRegistrationDocument(fixture.route)) | |
| 231 | + | writeProductionBrokerDocument(t, sandboxRegistrationDocument(fixture.route)) | |
| 307 | 232 | ||
| 308 | 233 | clk := newSandboxClock() | |
| 309 | 234 | deps, ready := testBrokerServeDeps(clk) | |
| 310 | 235 | ctx, cancel := context.WithCancel(context.Background()) | |
| 311 | 236 | defer cancel() | |
| 312 | - | done := runSandbox(ctx, brokerServeOptions{offlineRoot: root}, deps) | |
| 237 | + | done := runSandbox(ctx, brokerServeOptions{production: true}, deps) | |
| 313 | 238 | ||
| 314 | 239 | socketPath := awaitSandboxReady(t, ready, done) | |
| 315 | 240 | ||
| @@ -356,7 +281,14 @@ func TestBrokerServeRoundTrip(t *testing.T) { | |||
| 356 | 281 | ||
| 357 | 282 | awaitSandboxCancel(t, cancel, done) | |
| 358 | 283 | requireSocketRemoved(t, socketPath) | |
| 359 | - | requireProductionUntouched(t, prodRuntime, prodState) | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | // writeProductionBrokerDocument replaces only the private per-test bootstrap. | |
| 287 | + | func writeProductionBrokerDocument(t *testing.T, document any) { | |
| 288 | + | t.Helper() | |
| 289 | + | raw, err := json.Marshal(document) | |
| 290 | + | require.NoError(t, err) | |
| 291 | + | require.NoError(t, os.WriteFile(productionBrokerConfigPath(), raw, 0o600)) | |
| 360 | 292 | } | |
| 361 | 293 | ||
| 362 | 294 | // awaitSandboxReady waits for the sandbox readiness hook and returns the | |
| @@ -402,51 +334,47 @@ func waitForSandboxShutdown(t *testing.T, clk *sandboxClock, done <-chan error) | |||
| 402 | 334 | } | |
| 403 | 335 | ||
| 404 | 336 | func TestBrokerServeIdleShutdownOnEmptyConfig(t *testing.T) { | |
| 405 | - | root, prodRuntime, prodState := isolateSandboxEnv(t) | |
| 337 | + | layout := emptyProductionBrokerLayout(t, "") | |
| 406 | 338 | clk := newSandboxClock() | |
| 407 | - | require.NoError(t, os.MkdirAll(root, 0o700)) | |
| 408 | - | writeSandboxConfig(t, root, map[string]any{"marker": brokerconfig.Marker, "registrations": []any{}}) | |
| 409 | 339 | ||
| 410 | 340 | deps, ready := testBrokerServeDeps(clk) | |
| 411 | 341 | ctx, cancel := context.WithCancel(context.Background()) | |
| 412 | 342 | defer cancel() | |
| 413 | - | done := runSandbox(ctx, brokerServeOptions{offlineRoot: root}, deps) | |
| 343 | + | done := runSandbox(ctx, brokerServeOptions{production: true}, deps) | |
| 414 | 344 | ||
| 415 | 345 | socketPath := awaitSandboxReady(t, ready, done) | |
| 416 | 346 | waitForSandboxShutdown(t, clk, done) | |
| 417 | 347 | ||
| 418 | 348 | requireSocketRemoved(t, socketPath) | |
| 419 | - | requireOwnerLockReleased(t, root) | |
| 420 | - | requireProductionUntouched(t, prodRuntime, prodState) | |
| 349 | + | requireOwnerLockReleased(t, layout.Runtime) | |
| 350 | + | ||
| 421 | 351 | } | |
| 422 | 352 | ||
| 423 | 353 | // requireOwnerLockReleased proves the lifetime lock is no longer held. | |
| 424 | - | func requireOwnerLockReleased(t *testing.T, root string) { | |
| 354 | + | func requireOwnerLockReleased(t *testing.T, runtimeDir string) { | |
| 425 | 355 | t.Helper() | |
| 426 | - | owner, err := lifecycle.TryAcquire(filepath.Join(root, brokerconfig.RuntimeDirName)) | |
| 356 | + | owner, err := lifecycle.TryAcquire(runtimeDir) | |
| 427 | 357 | require.NoError(t, err) | |
| 428 | 358 | require.NoError(t, owner.Release()) | |
| 429 | 359 | } | |
| 430 | 360 | ||
| 431 | 361 | func TestBrokerServeCancelCleanup(t *testing.T) { | |
| 432 | - | root, prodRuntime, prodState := isolateSandboxEnv(t) | |
| 362 | + | layout := emptyProductionBrokerLayout(t, "") | |
| 433 | 363 | clk := newSandboxClock() | |
| 434 | - | require.NoError(t, os.MkdirAll(root, 0o700)) | |
| 435 | - | writeSandboxConfig(t, root, map[string]any{"marker": brokerconfig.Marker, "registrations": []any{}}) | |
| 436 | 364 | ||
| 437 | 365 | deps, ready := testBrokerServeDeps(clk) | |
| 438 | 366 | ctx, cancel := context.WithCancel(context.Background()) | |
| 439 | 367 | defer cancel() | |
| 440 | - | done := runSandbox(ctx, brokerServeOptions{offlineRoot: root}, deps) | |
| 368 | + | done := runSandbox(ctx, brokerServeOptions{production: true}, deps) | |
| 441 | 369 | ||
| 442 | 370 | socketPath := awaitSandboxReady(t, ready, done) | |
| 443 | 371 | awaitSandboxCancel(t, cancel, done) | |
| 444 | 372 | ||
| 445 | 373 | requireSocketRemoved(t, socketPath) | |
| 446 | - | requireOwnerLockReleased(t, root) | |
| 447 | - | _, err := os.Lstat(filepath.Join(root, brokerconfig.StateDirName, "state.json")) | |
| 448 | - | require.NoError(t, err, "durable sandbox state must remain after shutdown") | |
| 449 | - | requireProductionUntouched(t, prodRuntime, prodState) | |
| 374 | + | requireOwnerLockReleased(t, layout.Runtime) | |
| 375 | + | _, err := os.Lstat(filepath.Join(layout.State, "state.json")) | |
| 376 | + | require.NoError(t, err, "durable broker state must remain after shutdown") | |
| 377 | + | ||
| 450 | 378 | } | |
| 451 | 379 | ||
| 452 | 380 | // wedgedListener is a ports.BrokerListener whose Accept always reports one | |
| @@ -469,10 +397,8 @@ func (l *wedgedListener) Addr() string { return l.addr } | |||
| 469 | 397 | // terminal accept failure commits shutdown and drains the listener, instead of | |
| 470 | 398 | // leaving a bound broker whose socket can no longer admit work. | |
| 471 | 399 | func TestBrokerServeShutsDownOnUnexpectedAcceptFailure(t *testing.T) { | |
| 472 | - | root, prodRuntime, prodState := isolateSandboxEnv(t) | |
| 400 | + | layout := emptyProductionBrokerLayout(t, "") | |
| 473 | 401 | clk := newSandboxClock() | |
| 474 | - | require.NoError(t, os.MkdirAll(root, 0o700)) | |
| 475 | - | writeSandboxConfig(t, root, map[string]any{"marker": brokerconfig.Marker, "registrations": []any{}}) | |
| 476 | 402 | ||
| 477 | 403 | listener := &wedgedListener{err: errors.New("carriage accept failed"), addr: "wedged"} | |
| 478 | 404 | deps, ready := testBrokerServeDeps(clk) | |
| @@ -481,7 +407,7 @@ func TestBrokerServeShutsDownOnUnexpectedAcceptFailure(t *testing.T) { | |||
| 481 | 407 | } | |
| 482 | 408 | ctx, cancel := context.WithCancel(context.Background()) | |
| 483 | 409 | defer cancel() | |
| 484 | - | done := runSandbox(ctx, brokerServeOptions{offlineRoot: root}, deps) | |
| 410 | + | done := runSandbox(ctx, brokerServeOptions{production: true}, deps) | |
| 485 | 411 | ||
| 486 | 412 | awaitSandboxReady(t, ready, done) | |
| 487 | 413 | select { | |
| @@ -491,25 +417,23 @@ func TestBrokerServeShutsDownOnUnexpectedAcceptFailure(t *testing.T) { | |||
| 491 | 417 | t.Fatal("the broker wedged on an unexpected accept failure") | |
| 492 | 418 | } | |
| 493 | 419 | require.True(t, listener.closed.Load(), "shutdown must close the wedged listener") | |
| 494 | - | requireOwnerLockReleased(t, root) | |
| 495 | - | requireProductionUntouched(t, prodRuntime, prodState) | |
| 420 | + | requireOwnerLockReleased(t, layout.Runtime) | |
| 421 | + | ||
| 496 | 422 | } | |
| 497 | 423 | ||
| 498 | 424 | func TestBrokerServeRefusesDuplicateOwnership(t *testing.T) { | |
| 499 | - | root, _, _ := isolateSandboxEnv(t) | |
| 425 | + | emptyProductionBrokerLayout(t, "") | |
| 500 | 426 | clk := newSandboxClock() | |
| 501 | - | require.NoError(t, os.MkdirAll(root, 0o700)) | |
| 502 | - | writeSandboxConfig(t, root, map[string]any{"marker": brokerconfig.Marker, "registrations": []any{}}) | |
| 503 | 427 | ||
| 504 | 428 | deps, ready := testBrokerServeDeps(clk) | |
| 505 | 429 | ctx, cancel := context.WithCancel(context.Background()) | |
| 506 | 430 | defer cancel() | |
| 507 | - | done := runSandbox(ctx, brokerServeOptions{offlineRoot: root}, deps) | |
| 431 | + | done := runSandbox(ctx, brokerServeOptions{production: true}, deps) | |
| 508 | 432 | socketPath := awaitSandboxReady(t, ready, done) | |
| 509 | 433 | ||
| 510 | 434 | // A second foreground owner fails fast on the lifetime lock. | |
| 511 | 435 | secondDeps, _ := testBrokerServeDeps(newSandboxClock()) | |
| 512 | - | err := runBrokerServe(context.Background(), brokerServeOptions{offlineRoot: root}, secondDeps) | |
| 436 | + | err := runBrokerServe(context.Background(), brokerServeOptions{production: true}, secondDeps) | |
| 513 | 437 | require.ErrorIs(t, err, lifecycle.ErrBusy) | |
| 514 | 438 | ||
| 515 | 439 | // A duplicate listener on the live socket is refused as well. | |
| @@ -713,12 +637,3 @@ func TestSandboxClockFiresOnAdvance(t *testing.T) { | |||
| 713 | 637 | t.Fatal("manual timer did not fire") | |
| 714 | 638 | } | |
| 715 | 639 | } | |
| 716 | - | ||
| 717 | - | // Ensure the fixture and registration helpers stay coherent. | |
| 718 | - | func TestBrokerSandboxFixtures(t *testing.T) { | |
| 719 | - | require.NoError(t, brokerTestRegistration().Validate()) | |
| 720 | - | require.NoError(t, brokerTestPolicy().Validate()) | |
| 721 | - | document := sandboxRegistrationDocument("/tmp/mux.sock") | |
| 722 | - | require.Equal(t, brokerconfig.Marker, document["marker"]) | |
| 723 | - | require.Len(t, document["registrations"], 1) | |
| 724 | - | } | |
| @@ -215,101 +215,26 @@ func TestDrainBrokerAcceptClassifiesTerminalFailure(t *testing.T) { | |||||
| 215 | } | 215 | } | ||
| 216 | } | 216 | } | ||
| 217 | 217 | ||||
| 218 | - | func TestParseBrokerServeArgs(t *testing.T) { | 218 | + | func TestProductionBrokerServeRejectsUnsafeRoot(t *testing.T) { |
| 219 | - | tests := []struct { | 219 | + | layout := emptyProductionBrokerLayout(t, "") |
| 220 | - | name string | 220 | + | require.NoError(t, os.MkdirAll(layout.Root, 0o755)) |
| 221 | - | args []string | |||
| 222 | - | want brokerServeOptions | |||
| 223 | - | wantErr string | |||
| 224 | - | }{ | |||
| 225 | - | {name: "root only", args: []string{"--offline-root", "/srv/sandbox"}, want: brokerServeOptions{offlineRoot: "/srv/sandbox"}}, | |||
| 226 | - | { | |||
| 227 | - | name: "root and grace", | |||
| 228 | - | args: []string{"--offline-root", "/srv/sandbox", "--idle-grace", "90s"}, | |||
| 229 | - | want: brokerServeOptions{offlineRoot: "/srv/sandbox", idleGrace: 90 * time.Second}, | |||
| 230 | - | }, | |||
| 231 | - | {name: "missing root", args: nil, wantErr: "requires --offline-root"}, | |||
| 232 | - | {name: "missing root value", args: []string{"--offline-root"}, wantErr: "requires a path"}, | |||
| 233 | - | {name: "empty root value", args: []string{"--offline-root", ""}, wantErr: "requires a path"}, | |||
| 234 | - | {name: "duplicate root", args: []string{"--offline-root", "/a", "--offline-root", "/b"}, wantErr: "duplicate --offline-root"}, | |||
| 235 | - | {name: "duplicate grace", args: []string{"--offline-root", "/a", "--idle-grace", "1s", "--idle-grace", "2s"}, wantErr: "duplicate --idle-grace"}, | |||
| 236 | - | {name: "missing grace value", args: []string{"--offline-root", "/a", "--idle-grace"}, wantErr: "requires a duration"}, | |||
| 237 | - | {name: "invalid duration", args: []string{"--offline-root", "/a", "--idle-grace", "soon"}, wantErr: "not a duration"}, | |||
| 238 | - | {name: "zero duration", args: []string{"--offline-root", "/a", "--idle-grace", "0s"}, wantErr: "must be positive"}, | |||
| 239 | - | {name: "negative duration", args: []string{"--offline-root", "/a", "--idle-grace", "-1s"}, wantErr: "must be positive"}, | |||
| 240 | - | {name: "unknown flag", args: []string{"--offline-root", "/a", "--verbose"}, wantErr: "unknown flag"}, | |||
| 241 | - | {name: "positional", args: []string{"--offline-root", "/a", "extra"}, wantErr: "positional"}, | |||
| 242 | - | {name: "equals form is refused", args: []string{"--offline-root=/a"}, wantErr: "unknown flag"}, | |||
| 243 | - | } | |||
| 244 | - | for _, tt := range tests { | |||
| 245 | - | t.Run(tt.name, func(t *testing.T) { | |||
| 246 | - | command, err := parseBrokerServeArgs(tt.args) | |||
| 247 | - | if tt.wantErr != "" { | |||
| 248 | - | require.Error(t, err) | |||
| 249 | - | require.Contains(t, err.Error(), tt.wantErr) | |||
| 250 | - | return | |||
| 251 | - | } | |||
| 252 | - | require.NoError(t, err) | |||
| 253 | - | require.Equal(t, kindBrokerServe, command.kind) | |||
| 254 | - | require.Equal(t, tt.want, command.brokerServe) | |||
| 255 | - | }) | |||
| 256 | - | } | |||
| 257 | - | } | |||
| 258 | - | ||||
| 259 | - | func TestBrokerServeIsHiddenFromPublicHelp(t *testing.T) { | |||
| 260 | - | require.NotContains(t, usageText, brokerServeCommand) | |||
| 261 | - | _, err := parseArgs([]string{brokerServeCommand, "--offline-root", "/srv/sandbox"}) | |||
| 262 | - | require.NoError(t, err) | |||
| 263 | - | _, err = parseArgs([]string{"--help"}) | |||
| 264 | - | require.NoError(t, err) | |||
| 265 | - | } | |||
| 266 | - | ||||
| 267 | - | func TestBrokerServeRejectsUnsafeRoot(t *testing.T) { | |||
| 268 | - | root, _, _ := isolateSandboxEnv(t) | |||
| 269 | - | ctx := context.Background() | |||
| 270 | deps, _ := testBrokerServeDeps(newSandboxClock()) | 221 | deps, _ := testBrokerServeDeps(newSandboxClock()) | ||
| 271 | - | 222 | + | err := runBrokerServe(context.Background(), brokerServeOptions{production: true}, deps) | |
| 272 | - | t.Run("relative root", func(t *testing.T) { | 223 | + | require.ErrorContains(t, err, "0755") |
| 273 | - | err := runBrokerServe(ctx, brokerServeOptions{offlineRoot: "relative"}, deps) | 224 | + | require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock")) |
| 274 | - | require.Error(t, err) | |||
| 275 | - | require.Contains(t, err.Error(), "not absolute") | |||
| 276 | - | }) | |||
| 277 | - | t.Run("unclean root", func(t *testing.T) { | |||
| 278 | - | err := runBrokerServe(ctx, brokerServeOptions{offlineRoot: root + "/../sandbox"}, deps) | |||
| 279 | - | require.Error(t, err) | |||
| 280 | - | require.Contains(t, err.Error(), "not clean") | |||
| 281 | - | }) | |||
| 282 | - | t.Run("production overlap", func(t *testing.T) { | |||
| 283 | - | err := runBrokerServe(ctx, brokerServeOptions{offlineRoot: ipc.SocketDir()}, deps) | |||
| 284 | - | require.Error(t, err) | |||
| 285 | - | require.Contains(t, err.Error(), "overlaps production path") | |||
| 286 | - | }) | |||
| 287 | - | t.Run("symlinked root", func(t *testing.T) { | |||
| 288 | - | link := filepath.Join(t.TempDir(), "link") | |||
| 289 | - | require.NoError(t, os.Symlink(t.TempDir(), link)) | |||
| 290 | - | err := runBrokerServe(ctx, brokerServeOptions{offlineRoot: link}, deps) | |||
| 291 | - | require.Error(t, err) | |||
| 292 | - | require.Contains(t, err.Error(), "symlink") | |||
| 293 | - | }) | |||
| 294 | - | t.Run("missing config", func(t *testing.T) { | |||
| 295 | - | err := runBrokerServe(ctx, brokerServeOptions{offlineRoot: root}, deps) | |||
| 296 | - | require.Error(t, err) | |||
| 297 | - | require.Contains(t, err.Error(), brokerconfig.ConfigFileName) | |||
| 298 | - | }) | |||
| 299 | } | 225 | } | ||
| 300 | 226 | ||||
| 301 | func TestBrokerServeRoundTrip(t *testing.T) { | 227 | func TestBrokerServeRoundTrip(t *testing.T) { | ||
| 302 | - | root, prodRuntime, prodState := isolateSandboxEnv(t) | 228 | + | emptyProductionBrokerLayout(t, "") |
| 303 | policy := brokerTestPolicy() | 229 | policy := brokerTestPolicy() | ||
| 304 | fixture := newBrokerMuxFixture(t, policy) | 230 | fixture := newBrokerMuxFixture(t, policy) | ||
| 305 | - | require.NoError(t, os.MkdirAll(root, 0o700)) | 231 | + | writeProductionBrokerDocument(t, sandboxRegistrationDocument(fixture.route)) |
| 306 | - | writeSandboxConfig(t, root, sandboxRegistrationDocument(fixture.route)) | |||
| 307 | 232 | ||||
| 308 | clk := newSandboxClock() | 233 | clk := newSandboxClock() | ||
| 309 | deps, ready := testBrokerServeDeps(clk) | 234 | deps, ready := testBrokerServeDeps(clk) | ||
| 310 | ctx, cancel := context.WithCancel(context.Background()) | 235 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 311 | defer cancel() | 236 | defer cancel() | ||
| 312 | - | done := runSandbox(ctx, brokerServeOptions{offlineRoot: root}, deps) | 237 | + | done := runSandbox(ctx, brokerServeOptions{production: true}, deps) |
| 313 | 238 | ||||
| 314 | socketPath := awaitSandboxReady(t, ready, done) | 239 | socketPath := awaitSandboxReady(t, ready, done) | ||
| 315 | 240 | ||||
| @@ -356,7 +281,14 @@ func TestBrokerServeRoundTrip(t *testing.T) { | |||||
| 356 | 281 | ||||
| 357 | awaitSandboxCancel(t, cancel, done) | 282 | awaitSandboxCancel(t, cancel, done) | ||
| 358 | requireSocketRemoved(t, socketPath) | 283 | requireSocketRemoved(t, socketPath) | ||
| 359 | - | requireProductionUntouched(t, prodRuntime, prodState) | 284 | + | } |
| 285 | + | ||||
| 286 | + | // writeProductionBrokerDocument replaces only the private per-test bootstrap. | |||
| 287 | + | func writeProductionBrokerDocument(t *testing.T, document any) { | |||
| 288 | + | t.Helper() | |||
| 289 | + | raw, err := json.Marshal(document) | |||
| 290 | + | require.NoError(t, err) | |||
| 291 | + | require.NoError(t, os.WriteFile(productionBrokerConfigPath(), raw, 0o600)) | |||
| 360 | } | 292 | } | ||
| 361 | 293 | ||||
| 362 | // awaitSandboxReady waits for the sandbox readiness hook and returns the | 294 | // awaitSandboxReady waits for the sandbox readiness hook and returns the | ||
| @@ -402,51 +334,47 @@ func waitForSandboxShutdown(t *testing.T, clk *sandboxClock, done <-chan error) | |||||
| 402 | } | 334 | } | ||
| 403 | 335 | ||||
| 404 | func TestBrokerServeIdleShutdownOnEmptyConfig(t *testing.T) { | 336 | func TestBrokerServeIdleShutdownOnEmptyConfig(t *testing.T) { | ||
| 405 | - | root, prodRuntime, prodState := isolateSandboxEnv(t) | 337 | + | layout := emptyProductionBrokerLayout(t, "") |
| 406 | clk := newSandboxClock() | 338 | clk := newSandboxClock() | ||
| 407 | - | require.NoError(t, os.MkdirAll(root, 0o700)) | |||
| 408 | - | writeSandboxConfig(t, root, map[string]any{"marker": brokerconfig.Marker, "registrations": []any{}}) | |||
| 409 | 339 | ||||
| 410 | deps, ready := testBrokerServeDeps(clk) | 340 | deps, ready := testBrokerServeDeps(clk) | ||
| 411 | ctx, cancel := context.WithCancel(context.Background()) | 341 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 412 | defer cancel() | 342 | defer cancel() | ||
| 413 | - | done := runSandbox(ctx, brokerServeOptions{offlineRoot: root}, deps) | 343 | + | done := runSandbox(ctx, brokerServeOptions{production: true}, deps) |
| 414 | 344 | ||||
| 415 | socketPath := awaitSandboxReady(t, ready, done) | 345 | socketPath := awaitSandboxReady(t, ready, done) | ||
| 416 | waitForSandboxShutdown(t, clk, done) | 346 | waitForSandboxShutdown(t, clk, done) | ||
| 417 | 347 | ||||
| 418 | requireSocketRemoved(t, socketPath) | 348 | requireSocketRemoved(t, socketPath) | ||
| 419 | - | requireOwnerLockReleased(t, root) | 349 | + | requireOwnerLockReleased(t, layout.Runtime) |
| 420 | - | requireProductionUntouched(t, prodRuntime, prodState) | 350 | + | |
| 421 | } | 351 | } | ||
| 422 | 352 | ||||
| 423 | // requireOwnerLockReleased proves the lifetime lock is no longer held. | 353 | // requireOwnerLockReleased proves the lifetime lock is no longer held. | ||
| 424 | - | func requireOwnerLockReleased(t *testing.T, root string) { | 354 | + | func requireOwnerLockReleased(t *testing.T, runtimeDir string) { |
| 425 | t.Helper() | 355 | t.Helper() | ||
| 426 | - | owner, err := lifecycle.TryAcquire(filepath.Join(root, brokerconfig.RuntimeDirName)) | 356 | + | owner, err := lifecycle.TryAcquire(runtimeDir) |
| 427 | require.NoError(t, err) | 357 | require.NoError(t, err) | ||
| 428 | require.NoError(t, owner.Release()) | 358 | require.NoError(t, owner.Release()) | ||
| 429 | } | 359 | } | ||
| 430 | 360 | ||||
| 431 | func TestBrokerServeCancelCleanup(t *testing.T) { | 361 | func TestBrokerServeCancelCleanup(t *testing.T) { | ||
| 432 | - | root, prodRuntime, prodState := isolateSandboxEnv(t) | 362 | + | layout := emptyProductionBrokerLayout(t, "") |
| 433 | clk := newSandboxClock() | 363 | clk := newSandboxClock() | ||
| 434 | - | require.NoError(t, os.MkdirAll(root, 0o700)) | |||
| 435 | - | writeSandboxConfig(t, root, map[string]any{"marker": brokerconfig.Marker, "registrations": []any{}}) | |||
| 436 | 364 | ||||
| 437 | deps, ready := testBrokerServeDeps(clk) | 365 | deps, ready := testBrokerServeDeps(clk) | ||
| 438 | ctx, cancel := context.WithCancel(context.Background()) | 366 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 439 | defer cancel() | 367 | defer cancel() | ||
| 440 | - | done := runSandbox(ctx, brokerServeOptions{offlineRoot: root}, deps) | 368 | + | done := runSandbox(ctx, brokerServeOptions{production: true}, deps) |
| 441 | 369 | ||||
| 442 | socketPath := awaitSandboxReady(t, ready, done) | 370 | socketPath := awaitSandboxReady(t, ready, done) | ||
| 443 | awaitSandboxCancel(t, cancel, done) | 371 | awaitSandboxCancel(t, cancel, done) | ||
| 444 | 372 | ||||
| 445 | requireSocketRemoved(t, socketPath) | 373 | requireSocketRemoved(t, socketPath) | ||
| 446 | - | requireOwnerLockReleased(t, root) | 374 | + | requireOwnerLockReleased(t, layout.Runtime) |
| 447 | - | _, err := os.Lstat(filepath.Join(root, brokerconfig.StateDirName, "state.json")) | 375 | + | _, err := os.Lstat(filepath.Join(layout.State, "state.json")) |
| 448 | - | require.NoError(t, err, "durable sandbox state must remain after shutdown") | 376 | + | require.NoError(t, err, "durable broker state must remain after shutdown") |
| 449 | - | requireProductionUntouched(t, prodRuntime, prodState) | 377 | + | |
| 450 | } | 378 | } | ||
| 451 | 379 | ||||
| 452 | // wedgedListener is a ports.BrokerListener whose Accept always reports one | 380 | // wedgedListener is a ports.BrokerListener whose Accept always reports one | ||
| @@ -469,10 +397,8 @@ func (l *wedgedListener) Addr() string { return l.addr } | |||||
| 469 | // terminal accept failure commits shutdown and drains the listener, instead of | 397 | // terminal accept failure commits shutdown and drains the listener, instead of | ||
| 470 | // leaving a bound broker whose socket can no longer admit work. | 398 | // leaving a bound broker whose socket can no longer admit work. | ||
| 471 | func TestBrokerServeShutsDownOnUnexpectedAcceptFailure(t *testing.T) { | 399 | func TestBrokerServeShutsDownOnUnexpectedAcceptFailure(t *testing.T) { | ||
| 472 | - | root, prodRuntime, prodState := isolateSandboxEnv(t) | 400 | + | layout := emptyProductionBrokerLayout(t, "") |
| 473 | clk := newSandboxClock() | 401 | clk := newSandboxClock() | ||
| 474 | - | require.NoError(t, os.MkdirAll(root, 0o700)) | |||
| 475 | - | writeSandboxConfig(t, root, map[string]any{"marker": brokerconfig.Marker, "registrations": []any{}}) | |||
| 476 | 402 | ||||
| 477 | listener := &wedgedListener{err: errors.New("carriage accept failed"), addr: "wedged"} | 403 | listener := &wedgedListener{err: errors.New("carriage accept failed"), addr: "wedged"} | ||
| 478 | deps, ready := testBrokerServeDeps(clk) | 404 | deps, ready := testBrokerServeDeps(clk) | ||
| @@ -481,7 +407,7 @@ func TestBrokerServeShutsDownOnUnexpectedAcceptFailure(t *testing.T) { | |||||
| 481 | } | 407 | } | ||
| 482 | ctx, cancel := context.WithCancel(context.Background()) | 408 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 483 | defer cancel() | 409 | defer cancel() | ||
| 484 | - | done := runSandbox(ctx, brokerServeOptions{offlineRoot: root}, deps) | 410 | + | done := runSandbox(ctx, brokerServeOptions{production: true}, deps) |
| 485 | 411 | ||||
| 486 | awaitSandboxReady(t, ready, done) | 412 | awaitSandboxReady(t, ready, done) | ||
| 487 | select { | 413 | select { | ||
| @@ -491,25 +417,23 @@ func TestBrokerServeShutsDownOnUnexpectedAcceptFailure(t *testing.T) { | |||||
| 491 | t.Fatal("the broker wedged on an unexpected accept failure") | 417 | t.Fatal("the broker wedged on an unexpected accept failure") | ||
| 492 | } | 418 | } | ||
| 493 | require.True(t, listener.closed.Load(), "shutdown must close the wedged listener") | 419 | require.True(t, listener.closed.Load(), "shutdown must close the wedged listener") | ||
| 494 | - | requireOwnerLockReleased(t, root) | 420 | + | requireOwnerLockReleased(t, layout.Runtime) |
| 495 | - | requireProductionUntouched(t, prodRuntime, prodState) | 421 | + | |
| 496 | } | 422 | } | ||
| 497 | 423 | ||||
| 498 | func TestBrokerServeRefusesDuplicateOwnership(t *testing.T) { | 424 | func TestBrokerServeRefusesDuplicateOwnership(t *testing.T) { | ||
| 499 | - | root, _, _ := isolateSandboxEnv(t) | 425 | + | emptyProductionBrokerLayout(t, "") |
| 500 | clk := newSandboxClock() | 426 | clk := newSandboxClock() | ||
| 501 | - | require.NoError(t, os.MkdirAll(root, 0o700)) | |||
| 502 | - | writeSandboxConfig(t, root, map[string]any{"marker": brokerconfig.Marker, "registrations": []any{}}) | |||
| 503 | 427 | ||||
| 504 | deps, ready := testBrokerServeDeps(clk) | 428 | deps, ready := testBrokerServeDeps(clk) | ||
| 505 | ctx, cancel := context.WithCancel(context.Background()) | 429 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 506 | defer cancel() | 430 | defer cancel() | ||
| 507 | - | done := runSandbox(ctx, brokerServeOptions{offlineRoot: root}, deps) | 431 | + | done := runSandbox(ctx, brokerServeOptions{production: true}, deps) |
| 508 | socketPath := awaitSandboxReady(t, ready, done) | 432 | socketPath := awaitSandboxReady(t, ready, done) | ||
| 509 | 433 | ||||
| 510 | // A second foreground owner fails fast on the lifetime lock. | 434 | // A second foreground owner fails fast on the lifetime lock. | ||
| 511 | secondDeps, _ := testBrokerServeDeps(newSandboxClock()) | 435 | secondDeps, _ := testBrokerServeDeps(newSandboxClock()) | ||
| 512 | - | err := runBrokerServe(context.Background(), brokerServeOptions{offlineRoot: root}, secondDeps) | 436 | + | err := runBrokerServe(context.Background(), brokerServeOptions{production: true}, secondDeps) |
| 513 | require.ErrorIs(t, err, lifecycle.ErrBusy) | 437 | require.ErrorIs(t, err, lifecycle.ErrBusy) | ||
| 514 | 438 | ||||
| 515 | // A duplicate listener on the live socket is refused as well. | 439 | // A duplicate listener on the live socket is refused as well. | ||
| @@ -713,12 +637,3 @@ func TestSandboxClockFiresOnAdvance(t *testing.T) { | |||||
| 713 | t.Fatal("manual timer did not fire") | 637 | t.Fatal("manual timer did not fire") | ||
| 714 | } | 638 | } | ||
| 715 | } | 639 | } | ||
| 716 | - | ||||
| 717 | - | // Ensure the fixture and registration helpers stay coherent. | |||
| 718 | - | func TestBrokerSandboxFixtures(t *testing.T) { | |||
| 719 | - | require.NoError(t, brokerTestRegistration().Validate()) | |||
| 720 | - | require.NoError(t, brokerTestPolicy().Validate()) | |||
| 721 | - | document := sandboxRegistrationDocument("/tmp/mux.sock") | |||
| 722 | - | require.Equal(t, brokerconfig.Marker, document["marker"]) | |||
| 723 | - | require.Len(t, document["registrations"], 1) | |||
| 724 | - | } | |||