6058b8472b
The sibling ticket landed the mechanism (LeadMailbox, LeadMessage, the
coordinator: config block) but nothing opened it, nothing sent through it, and
nothing read it. This is the wiring.
- LeadChannel: a small interface LeadMailbox now implements (publish/peek/ack
plus a selfCoordId() accessor). It exists so FleetMcp and the receive loop can
be tested with a fake instead of a live broker. LeadMailbox's AMQP logic is
untouched — the diff is the implements clause, four @Override marks and the
accessor.
- Fleetd.openLeadMailbox: opens this daemon's mailbox after the reply inbox is
selected, with the same env-injected seam selectReplyInbox uses. Every "off"
path returns null and the daemon still starts: no coordinator block (silent),
a uriEnv that does not resolve (INFO), a configured broker with no selfId
(WARN — a mailbox is named after the coord-id that owns it), or a broker that
refuses at boot (WARN, credentials stripped). Closed in the ordered shutdown
hook, after the loop that reads it has stopped.
- fleet_send{coordId}: publishes a LeadMessage(from=selfCoordId, to=coordId) to
the peer's mailbox and returns the broker-confirmed receipt. coordId is
mutually exclusive with sessionId/turnId and is rejected by name rather than
resolved by precedence. An unroutable/nacked/timed-out publish comes back as a
tool error naming the coordId, never a crash. The worker send/reply path is
not touched.
- LeadCoordLoop: the receive half. Each tick peeks the mailbox, resolves the
local lead pane, and — only at a turn boundary — injects "[lead <from>] <text>"
and acks. Anything not delivered stays unacked and is retried, so a message is
never dropped; one message per tick, so every delivery is gated on a status
read that already saw the previous one.
- fleet_list reports {selfId, configured} when coordination is on, so an
operator can find the coord-id a peer must use to reach them. Omitted
entirely when it is off.
Tests: 20 new hermetic tests (no broker) across routing, delivery and startup
selection. mvn clean install: Tests run: 944, Failures: 0, Errors: 0, Skipped: 0
— BUILD SUCCESS.
76 lines
2.4 KiB
Java
76 lines
2.4 KiB
Java
package dev.ltms.fleet.msg;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* Hermetic stand-in for {@link LeadChannel}: an in-memory mailbox that records what was published
|
|
* and what was acked, with no broker anywhere.
|
|
*
|
|
* <p>Its whole point is that {@link LeadMailbox} — the production implementation — owns a live AMQP
|
|
* connection, so every test of the code AROUND it would otherwise need a broker and end up tagged
|
|
* {@code contract}. The broker round trip is covered once, by {@code LeadMailboxTest}; the routing
|
|
* ({@code FleetMcp}) and the delivery ({@link LeadCoordLoop}) are covered here.
|
|
*
|
|
* <p>Thread-safe: {@link LeadCoordLoop} calls it from its own scheduler thread while a test reads
|
|
* the recorded lists.
|
|
*/
|
|
public final class FakeLeadChannel implements LeadChannel {
|
|
|
|
private final String selfCoordId;
|
|
private final List<LeadMessage> held = Collections.synchronizedList(new ArrayList<>());
|
|
private final List<LeadMessage> published = Collections.synchronizedList(new ArrayList<>());
|
|
private final List<String> acked = Collections.synchronizedList(new ArrayList<>());
|
|
/** When set, every {@link #publish} throws it — the unroutable/nacked/timed-out peer. */
|
|
private volatile IllegalStateException publishFailure;
|
|
|
|
public FakeLeadChannel(String selfCoordId) {
|
|
this.selfCoordId = selfCoordId;
|
|
}
|
|
|
|
/** Make every publish fail as an unreachable peer would. */
|
|
public FakeLeadChannel failPublishWith(String message) {
|
|
this.publishFailure = new IllegalStateException(message);
|
|
return this;
|
|
}
|
|
|
|
/** Put a message in this mailbox as if a peer had sent it. */
|
|
public FakeLeadChannel hold(LeadMessage m) {
|
|
held.add(m);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public void publish(String toCoordId, LeadMessage m) {
|
|
if (publishFailure != null) {
|
|
throw publishFailure;
|
|
}
|
|
published.add(m);
|
|
}
|
|
|
|
@Override
|
|
public List<LeadMessage> peek() {
|
|
return List.copyOf(held);
|
|
}
|
|
|
|
@Override
|
|
public void ack(String msgId) {
|
|
held.removeIf(m -> m.msgId().equals(msgId));
|
|
acked.add(msgId);
|
|
}
|
|
|
|
@Override
|
|
public String selfCoordId() {
|
|
return selfCoordId;
|
|
}
|
|
|
|
public List<LeadMessage> published() {
|
|
return List.copyOf(published);
|
|
}
|
|
|
|
public List<String> acked() {
|
|
return List.copyOf(acked);
|
|
}
|
|
}
|