Files
fleetd/bridged/src/test/java/dev/ltms/fleet/msg/FakeLeadChannel.java
T
Dai Ha 6058b8472b
CI / contract (pull_request) Successful in 41s
CI / build (pull_request) Failing after 1m22s
lead comms: wire LeadMailbox into the daemon so lead-to-lead messages flow
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.
2026-08-24 17:39:29 +02:00

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);
}
}