diff --git a/9-Implementation.md b/9-Implementation.md index 8813a4b..6b10d34 100644 --- a/9-Implementation.md +++ b/9-Implementation.md @@ -306,7 +306,10 @@ loop (`rest/FleetApp.java:143-158`). **There is no `/events` route**, and the wo ## Bootstrap wiring -`Fleetd.main` assembles the object graph in dependency order, then starts both faces. +`Fleetd.main` loads the config, reports it and runs `cfg.validateAll()`. Everything after that — +the whole object graph, in dependency order, then starting both faces — lives in +`FleetdAssembly.assembleAndStart(AssemblyInputs, ResourcePorts)` (fleetd #612). `main` is now three +statements and one call. ```mermaid flowchart LR @@ -324,11 +327,46 @@ flowchart LR mcp --> app["rest.FleetApp
(start Javalin)"] ``` -*Figure 2 — startup wiring in `Fleetd.main`. The guard asserts the primary's own environment is -clean before anything else; the two adapters plug into one `CompositePeerLauncher`; -`reapOrphanWorkers()` clears stale panes left by a prior daemon restart; `CallerResolver` is built -last among the core services because it needs the live lead and architect bindings that everything -above it produces.* +*Figure 2 — startup wiring, now inside `FleetdAssembly.assembleAndStart`. The guard asserts the +primary's own environment is clean before anything else; the two adapters plug into one +`CompositePeerLauncher`; `reapOrphanWorkers()` clears stale panes left by a prior daemon restart; +`CallerResolver` is built last among the core services because it needs the live lead and architect +bindings that everything above it produces.* + +### Why the assembly is a separate method + +Nothing used to call `Fleetd.main` far enough to observe what it passed. So any injected wiring +could be swapped for its inert variant — `X.none()`, `_ -> null`, `() -> Map.of()` — and the whole +suite stayed green. That is not a theory: #602/#606 shipped exactly that defect, and a later sweep +found 16 more call sites with the same hole. + +Three types carry the fix: + +- **`FleetdAssembly.assembleAndStart`** — the same statements `main` used to run inline, in the same + order. Construction and start order is preserved deliberately; it was *not* rebuilt into + "construct everything, then start everything", because that would change boot timing. +- **`FleetdRuntime`** — owns the assembled objects and their single ordered `close()`. Its + package-private accessors return **the identical instances the running daemon uses**, never a + copy. A test that inspected a snapshot built alongside the real objects could pass while + production silently received something else, which is the defect being closed. +- **`ResourcePorts`** — every boot-time side effect a real daemon must do for real and a test must + not: read the environment, connect a herdr client, open a broker, read a clock, start a scheduler, + register the shutdown hook, bind the HTTP server. `ResourcePorts.system()` is the one production + implementation. + +**There is deliberately no `ResourcePorts.none()` and no smaller overload of `assembleAndStart`.** +Adding an inert default, even for tests, would hand a future edit the exact compiling substitute +this work exists to rule out. A test writes its own fake and owns that choice. + +Two consequences worth knowing before you write a boot test: + +- One statement could not move without reordering startup. `main` registered its shutdown hook + *before* building the Javalin app, so `FleetdRuntime` is built first and the app attached + afterwards via `attachApp`, still before HTTP listens. See that field's javadoc. +- A wiring that differs between the lead and member herdr daemons cannot be pinned by a fixture with + one `FakeHerdr`. When `memberHerdrSocket` is unset the assembly falls back to `memberHerdr = herdr`, + so `router.leadAgents()` and `router.memberAgents()` become the same object and a test cannot tell + them apart. Configure two sockets. This cost two rounds of rework on #612 alone. ## Flow: forward rendezvous (`fleet_send` → `fleet_reply`)