diff --git a/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java b/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java index 556d6b2..22cb8e2 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java +++ b/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java @@ -780,6 +780,46 @@ public final class FleetMcp { return server.listTools(); } + /** + * fleetd #612 B3 — same reason as {@link #registeredTools()}: a test that must drive the REAL + * {@link QuarantineSource} (and the real {@link BackendQuarantine} it wraps) this daemon was + * assembled with, rather than scraping {@code FleetdAssembly.java}'s source text for the + * constructor call that built it. Unlike {@link #registeredTools()}'s callers, that test cannot + * live in this package: it also builds the {@code ResourcePorts} that drives + * {@code FleetdAssembly.assembleAndStart}, and {@code ResourcePorts}' methods return + * {@code Fleetd}-nested types that are only visible from package {@code dev.ltms.fleet} — so + * this accessor is {@code public}, not package-private, to stay reachable from there. {@code + * FleetdBackendQuarantineAssemblyTest} quarantines a credential twice through this exact + * instance and checks the second cooldown is longer than the first — the one behavioural + * difference {@link BackendQuarantine#withEscalation} and the flat two-argument constructor + * actually produce. + */ + public QuarantineSource quarantineSource() { + return quarantine; + } + + /** + * fleetd #612 B3 — as {@link #quarantineSource()}, {@code public} for the same cross-package + * reason, for the real {@link LeadSeatSource} this daemon was assembled with. {@code + * FleetdLeadSeatAssemblyTest} calls {@code seatsFor} on this exact instance and checks it + * reports a live lead's seat, which {@link LeadSeatSource#none()} can never do (it is a + * constant-zero function regardless of input). + */ + public LeadSeatSource leadSeatSource() { + return leadSeats; + } + + /** + * fleetd #612 B3 — as {@link #quarantineSource()}, {@code public} for the same cross-package + * reason, for the real {@link LeadRollover} (or {@code null}) this daemon was assembled with. + * {@code FleetdLeadRolloverAssemblyTest} drives {@code open}/{@code confirm} on this exact + * instance and waits for the real continuation to send {@code /clear} and {@code bootstrapText} + * through the real {@code router.leadAgents()}. + */ + public LeadRollover leadRollover() { + return leadRollover; + } + // --- tool logic (thin adapters over the services; unit-testable) --------------------------- /** diff --git a/fleetd/src/test/java/dev/ltms/fleet/FleetdBackendQuarantineAssemblyTest.java b/fleetd/src/test/java/dev/ltms/fleet/FleetdBackendQuarantineAssemblyTest.java new file mode 100644 index 0000000..2852739 --- /dev/null +++ b/fleetd/src/test/java/dev/ltms/fleet/FleetdBackendQuarantineAssemblyTest.java @@ -0,0 +1,199 @@ +package dev.ltms.fleet; + +import dev.ltms.fleet.config.ConfigRef; +import dev.ltms.fleet.config.FleetConfig; +import dev.ltms.fleet.guard.SubscriptionGuard; +import dev.ltms.fleet.herdr.FakeHerdr; +import dev.ltms.fleet.herdr.HerdrClient; +import dev.ltms.fleet.msg.ReplyInbox; +import dev.ltms.fleet.placement.BackendQuarantine; +import io.javalin.Javalin; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.OptionalLong; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.LongSupplier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * fleetd #612 B3 — replaces {@code FleetdBackendQuarantineWiringTest} (fleetd #466), a source-text + * test that scraped {@code Fleetd.java} (now {@code FleetdAssembly.java}, moved there by fleetd #612 + * Unit A) for the {@code BackendQuarantine.withEscalation(...)} call, and separately asserted the + * flat two-argument constructor's text was ABSENT. That proves the right method NAME appears in + * source; it proves nothing about what the constructed object actually DOES. + * + *
This test instead drives the REAL {@link BackendQuarantine} the real {@link
+ * FleetdAssembly#assembleAndStart} builds — reached through {@link
+ * dev.ltms.fleet.mcp.FleetMcp#quarantineSource()} on the real, live {@code FleetMcp} {@code
+ * FleetdRuntime} owns — and asserts the ONE behavioural difference {@code withEscalation} and the
+ * flat constructor actually produce (see {@link BackendQuarantine}'s own class doc, "Mechanism"):
+ * quarantining the same credential twice in a row, within one base cooldown of the first deadline,
+ * must escalate the second cooldown past the first. A flat instance reports the identical cooldown
+ * both times.
+ */
+class FleetdBackendQuarantineAssemblyTest {
+
+ /** Base cooldown used throughout — long enough that rounding never blurs the 2x escalation. */
+ private static final int COOLDOWN_SECONDS = 100;
+
+ private static final class RecordingResourcePorts implements ResourcePorts {
+
+ final FakeHerdr herdr = new FakeHerdr();
+ final SentinelReplyInbox replyInbox = new SentinelReplyInbox();
+ final AtomicLong clockNanos = new AtomicLong(0L);
+
+ @Override
+ public Map Measured directly: reverting {@code main} to {@code new BackendQuarantine(System::nanoTime,
- * TimeUnit.SECONDS.toNanos(cfg.quarantineCooldownSeconds()))} — the pre-#466 flat call — compiles
- * with 0 errors and leaves the entire 1608-test suite (including every {@code BackendQuarantineTest}
- * case) green, because no other test constructs its {@code BackendQuarantine} through {@code main};
- * every one of them builds its own instance directly. That silent regression is exactly the shape
- * {@link FleetdLeadSeatWiringTest} and {@link FleetdCompletionResolverWiringTest} already guard
- * against for their own constructor arguments — this is the same class of gap for fleetd #466's
- * factory choice, following their approach.
- *
- * This test checks source text, not runtime behaviour. It never constructs a {@code
- * BackendQuarantine} and never runs {@code main} — a green result here proves only that the exact
- * text {@code main} calls {@code BackendQuarantine.withEscalation(...)} rather than the flat
- * constructor. It does not prove that call actually executes at startup (no test here starts the
- * daemon), and it does not prove the escalation reaches a real backend or credential — only
- * {@code BackendQuarantineTest} proves the factory's own behaviour, and only a live daemon proves
- * the wiring runs.
- */
-class FleetdBackendQuarantineWiringTest {
-
- private static String fleetdSource() throws Exception {
- return Files.readString(Path.of("src/main/java/dev/ltms/fleet/Fleetd.java"));
- }
-
- @Test
- @DisplayName("[SOURCE TEXT] main's BackendQuarantine local is still built from BackendQuarantine.withEscalation(...)")
- void mainStillWiresTheEscalatingQuarantineFactory() throws Exception {
- String source = fleetdSource();
- assertTrue(source.contains(
- "BackendQuarantine quarantine = BackendQuarantine.withEscalation(System::nanoTime,\n"
- + " TimeUnit.SECONDS.toNanos(cfg.quarantineCooldownSeconds()));"),
- "Fleetd.main's BackendQuarantine local must still be built from "
- + "BackendQuarantine.withEscalation(System::nanoTime, "
- + "TimeUnit.SECONDS.toNanos(cfg.quarantineCooldownSeconds())). Reverting to the flat "
- + "two-argument constructor (fleetd #466's measured regression) compiles with 0 errors "
- + "and leaves the whole suite green, including every BackendQuarantineTest case that "
- + "proves the escalation itself works — this source check is what must go red instead. "
- + "A reverted daemon would go back to retrying a weekly subscription limit on every "
- + "flat ~30-minute cooldown, about 336 times across the week.");
-
- // Negative form of the same check: the pre-#466 flat call, if it ever reappears at this
- // declaration, must not be mistaken for the escalating one by a looser positive-only check.
- assertFalse(source.contains(
- "BackendQuarantine quarantine = new BackendQuarantine(System::nanoTime,\n"
- + " TimeUnit.SECONDS.toNanos(cfg.quarantineCooldownSeconds()));"),
- "main's BackendQuarantine local must never regress to the flat two-argument constructor");
- }
-}
diff --git a/fleetd/src/test/java/dev/ltms/fleet/FleetdLeadRolloverAssemblyTest.java b/fleetd/src/test/java/dev/ltms/fleet/FleetdLeadRolloverAssemblyTest.java
new file mode 100644
index 0000000..b15eefb
--- /dev/null
+++ b/fleetd/src/test/java/dev/ltms/fleet/FleetdLeadRolloverAssemblyTest.java
@@ -0,0 +1,242 @@
+package dev.ltms.fleet;
+
+import dev.ltms.fleet.config.ConfigRef;
+import dev.ltms.fleet.config.FleetConfig;
+import dev.ltms.fleet.guard.SubscriptionGuard;
+import dev.ltms.fleet.herdr.AgentControl;
+import dev.ltms.fleet.herdr.FakeHerdr;
+import dev.ltms.fleet.herdr.HerdrClient;
+import dev.ltms.fleet.lead.LeadRollover;
+import dev.ltms.fleet.msg.ReplyInbox;
+import io.javalin.Javalin;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.LongSupplier;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * fleetd #612 B3 — replaces {@code FleetdLeadRolloverWiringTest} (fleetd #480). That class was a
+ * source-text test scraping {@code Fleetd.java} (now {@code FleetdAssembly.java}, moved there by
+ * fleetd #612 Unit A) with three methods: {@code unrelatedAnchorStillPresent} (a scaffold anchor,
+ * not an independent claim — needs no replacement of its own), {@code
+ * mainStillCallsTheLeadRolloverFactory} (the call-site pin replaced by {@link
+ * #assembledLeadRolloverRunsTheRealClearAndBootstrapSequence}), and {@code
+ * factoryGatesOnConfigPresence} (the absent-config claim replaced by {@link
+ * #absentLeadRolloverConfigMeansNoRolloverIsBuilt} — a claim this ticket found was NOT actually
+ * covered behaviourally anywhere else: {@code LeadRolloverTest}'s only related assertion is
+ * vacuous, {@code assertNull(null)}, and never calls the real factory).
+ */
+class FleetdLeadRolloverAssemblyTest {
+
+ private static final class RecordingResourcePorts implements ResourcePorts {
+
+ final FakeHerdr herdr = new FakeHerdr();
+ final SentinelReplyInbox replyInbox = new SentinelReplyInbox();
+
+ @Override
+ public Map What this class still covers, and what it never claimed to. {@code LeadRolloverTest}
- * constructs its own {@code LeadRollover} directly (as every prior test of an extracted factory
- * does) with a hand-built lookup, so a mutation that deletes the {@code leadRollover(...)} call
- * from {@code main} — or replaces one of its arguments with something that still compiles, e.g.
- * {@code router.leadAgents()} swapped for {@code null}, or the whole assignment swapped for a bare
- * {@code null} literal — leaves every behavioural test green. This is a plain string read, guarded
- * by an unrelated anchor assertion so a broken or empty file read cannot pass as a real change.
- *
- * This test checks source text, not runtime behaviour. It never constructs a {@code
- * LeadRollover} and never runs {@code main}. It pins the {@code leadRollover(...)} CALL SITE's
- * argument list — that {@code main} still passes {@code leads} at all — never what the factory
- * DOES with that argument once inside its own body.
- *
- * Correction (fleetd #480 relative-handover-path follow-up): that gap used to be real, and
- * now is not — but not here. This class's javadoc previously claimed "no behavioural test can
- * catch this wiring dropping out" for the whole factory, including the lambda {@code
- * leadRollover(...)} builds internally (terminal → lead name → {@code Leader.cwd()}). That claim
- * was proven true at the time — mutating that lambda's body to {@code String leadName = null;}
- * (always "no lead found", which silently reintroduces the daemon-cwd bug this ticket fixes) left
- * the full suite green, {@code Tests run: 1669, Failures: 0}. It is no longer true: {@code
- * FleetdLeadRolloverWorkspaceLookupTest} now calls {@code Fleetd.leadRollover(...)} directly with a
- * real {@link dev.ltms.fleet.config.ConfigRef} built from a temp {@code fleetd.yaml}, and fails
- * against that exact one-line mutation. So: THIS class still covers only the call site's argument
- * list; {@code FleetdLeadRolloverWorkspaceLookupTest} is what now covers the lambda's body. Neither
- * one subsumes the other — keep both.
- */
-class FleetdLeadRolloverWiringTest {
-
- private static String fleetdSource() throws Exception {
- return Files.readString(Path.of("src/main/java/dev/ltms/fleet/Fleetd.java"));
- }
-
- @Test
- @DisplayName("[SOURCE TEXT] unrelated anchor: Fleetd.java still declares the Fleetd class")
- void unrelatedAnchorStillPresent() throws Exception {
- // Guards the two assertions below: without this, a bad read (empty string, wrong file,
- // truncated file) could vacuously fail to contain the leadRollover(...) call too, and a
- // test that only asserts "contains X" would report a false pass for the wrong reason if X
- // happened to match. Asserting an unrelated, structurally distant string first proves the
- // read actually pulled real file content.
- String source = fleetdSource();
- assertTrue(source.contains("public final class Fleetd"),
- "sanity anchor failed — the file read did not return real Fleetd.java source; the "
- + "leadRollover(...) wiring assertions below cannot be trusted until this passes");
- }
-
- @Test
- @DisplayName("[SOURCE TEXT] main still constructs LeadRollover via the leadRollover(...) factory, exactly as heartbeat is constructed")
- void mainStillCallsTheLeadRolloverFactory() throws Exception {
- String source = fleetdSource();
- assertTrue(source.contains(
- "LeadRollover leadRollover = leadRollover(cfg, router.leadAgents(), config, leads);"),
- "Fleetd.main must still assign `LeadRollover leadRollover = leadRollover(cfg, "
- + "router.leadAgents(), config, leads);`. Dropping this call, or swapping one of "
- + "its arguments for something that still compiles (e.g. null in place of "
- + "router.leadAgents()), leaves every behavioural test green — this source check is "
- + "what must go red instead. fleetd #480 correction 2 deliberately dropped "
- + "primaryRegistry from this call — see LeadRollover's class javadoc for why a "
- + "single-slot lookup was wrong here. The fleetd #480 relative-handover-path "
- + "follow-up added `leads` (terminal → lead name) so the factory can resolve a "
- + "relative handoverPath against the calling lead's own workspace.");
- }
-
- @Test
- @DisplayName("[SOURCE TEXT] the leadRollover(...) factory itself gates construction on cfg.leadRollover() != null")
- void factoryGatesOnConfigPresence() throws Exception {
- String source = fleetdSource();
- assertTrue(source.contains("if (cfg.leadRollover() == null) {"),
- "Fleetd.leadRollover(...) must refuse to construct a LeadRollover when the "
- + "leadRollover: block is absent — an upgraded daemon must never silently acquire "
- + "the ability to clear the lead's own pane. See LeadHeartbeatLoop's construction "
- + "gate (cfg.leadHeartbeat() != null) for the pattern this mirrors.");
- }
-}
diff --git a/fleetd/src/test/java/dev/ltms/fleet/FleetdLeadSeatAssemblyTest.java b/fleetd/src/test/java/dev/ltms/fleet/FleetdLeadSeatAssemblyTest.java
new file mode 100644
index 0000000..aff5704
--- /dev/null
+++ b/fleetd/src/test/java/dev/ltms/fleet/FleetdLeadSeatAssemblyTest.java
@@ -0,0 +1,172 @@
+package dev.ltms.fleet;
+
+import dev.ltms.fleet.config.ConfigRef;
+import dev.ltms.fleet.config.FleetConfig;
+import dev.ltms.fleet.guard.SubscriptionGuard;
+import dev.ltms.fleet.herdr.FakeHerdr;
+import dev.ltms.fleet.herdr.HerdrClient;
+import dev.ltms.fleet.mcp.FleetMcp;
+import dev.ltms.fleet.msg.ReplyInbox;
+import io.javalin.Javalin;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.LongSupplier;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * fleetd #612 B3 — replaces {@code FleetdLeadSeatWiringTest} (fleetd #176), a source-text test that
+ * scraped {@code Fleetd.java} (now {@code FleetdAssembly.java}, moved there by fleetd #612 Unit A)
+ * for the exact {@code new FleetMcp.LeadSeatSource(Fleetd.leadSeatLookup(...))} constructor-call
+ * text. That proves the right symbols appear in source; it proves nothing about what the daemon's
+ * live {@code fleet_list} actually reports.
+ *
+ * This test instead drives the REAL {@link FleetMcp.LeadSeatSource} the real {@link
+ * FleetdAssembly#assembleAndStart} builds — including the REAL {@code LeadTabScanner} it wires
+ * {@code Fleetd.leadSeatLookup} through — reached via {@link FleetMcp#leadSeatSource()} on the
+ * live {@code FleetMcp} {@code FleetdRuntime} owns. It seeds one FakeHerdr tab labelled to match a
+ * configured {@code fleet.leaders.opus.tab}, with a live agent already in it (FakeHerdr's own
+ * default {@code agent.list}/{@code pane.list} entries for {@code term_a}/{@code w2:p7}/{@code
+ * w2:t7} — no FakeHerdr change needed), and asserts the assembled seat source reports exactly the
+ * seat {@link FleetMcp.LeadSeatSource#none()} (the inert stand-in) could never produce: 1, not 0.
+ */
+class FleetdLeadSeatAssemblyTest {
+
+ private static final class RecordingResourcePorts implements ResourcePorts {
+
+ final FakeHerdr herdr = new FakeHerdr();
+ final SentinelReplyInbox replyInbox = new SentinelReplyInbox();
+
+ @Override
+ public Map {@link FleetdLeadSeatLookupTest} proves the factory's own matching logic; this class is the
- * plain source-text assertion that proves {@code main} still passes its result in, mirroring
- * {@code FleetdCompletionResolverWiringTest}'s approach for the same class of gap.
- *
- * This test checks source text, not runtime behaviour. It never constructs a
- * {@code FleetMcp} and never runs {@code main}.
- */
-class FleetdLeadSeatWiringTest {
-
- private static String fleetdSource() throws Exception {
- return Files.readString(Path.of("src/main/java/dev/ltms/fleet/Fleetd.java"));
- }
-
- @Test
- @DisplayName("[SOURCE TEXT] FleetMcp's construction call still passes a LeadSeatSource built from leadSeatLookup(...)")
- void fleetMcpConstructionStillWiresLeadSeatLookup() throws Exception {
- String source = fleetdSource();
- assertTrue(source.contains("new FleetMcp.LeadSeatSource(leadSeatLookup(() -> config.get().profiles(), "
- + "leaders, leads))"),
- "FleetMcp's construction call must still pass a LeadSeatSource built from "
- + "Fleetd.leadSeatLookup(...). Dropping it or swapping in "
- + "FleetMcp.LeadSeatSource.none() (fleetd #176's would-be silent regression, the same "
- + "shape as fleetd #248's measured mutations) compiles with 0 errors and leaves every "
- + "existing behavioural test green — this source check is what must go red instead.");
- }
-}