Implementation: Fleetd.main's boot composition now lives in FleetdAssembly (fleetd #612)

Figure 2's intro and caption said main assembles the graph; that has not been
true since #620. Adds the seam's contract: FleetdAssembly/FleetdRuntime/
ResourcePorts, why there is deliberately no inert default, and the two traps a
boot test hits — the late attachApp, and the one-FakeHerdr fixture that cannot
tell leadAgents() from memberAgents() when memberHerdrSocket is unset.
lead
2026-09-22 12:48:50 +07:00
parent 269d2f5cb5
commit 7bd4616ffb
+44 -6
@@ -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<br/>(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`)