This test fails the build the moment a NEW cycle appears between the top-level + * {@code dev.ltms.fleet.*} packages. Today's cycles are recorded below as explicit, + * narrow exceptions: each one ignores dependencies between exactly the two named + * packages, in both directions, and nothing else. A cycle through any other pair of + * packages -- or a brand new pair -- still fails this test. + * + *
Main code only. The import excludes test classes + * ({@link ImportOption.Predefined#DO_NOT_INCLUDE_TESTS}). Test code legitimately wires + * across many packages for setup and mocking; that is not part of the shipped + * architecture this rule protects. Verified: importing test classes too pulls in a much + * larger, noisier cycle set -- {@code herdr}, {@code member}, {@code peer}, {@code + * config}, {@code guard} and {@code placement} all show up in cycles that disappear the + * moment test classes are excluded. Scanning off the classpath via {@code + * importPackages(...)} (not a hardcoded {@code target/classes} path) also keeps this + * test correct regardless of the working directory the build is invoked from. + * + *
No package moves here -- ticket #131 is explicit that removing a cycle is
+ * its own, later PR. See the comment on each exception below for which ticket step
+ * removes it.
+ */
+class PackageCyclesTest {
+
+ @Test
+ void packagesAreFreeOfCycles() {
+ var classes = new ClassFileImporter()
+ .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
+ .importPackages("dev.ltms.fleet");
+
+ SliceRule rule = SlicesRuleDefinition.slices()
+ .matching("dev.ltms.fleet.(*)..")
+ .should().beFreeOfCycles();
+
+ // fleetd #131 step 1: move ConnectionIdentity so authz stops depending on the
+ // MCP layer. Evidence: auth/CallerResolver.java:3 imports mcp.ConnectionIdentity;
+ // mcp/FleetMcp.java:3-7 imports auth.AuditLog, Authz, CallerResolver, Principal,
+ // Role.
+ rule = ignoreCycle(rule, "auth", "mcp");
+
+ // fleetd #131 step 2: PrimaryRegistry is used by loops in msg; move it, or put
+ // an interface between msg and mcp. Evidence: msg/ReplyPushLoop.java:5 and
+ // msg/LeadHeartbeatLoop.java:5 import mcp.PrimaryRegistry; mcp/FleetMcp.java:15-18
+ // imports msg.LeadChannel, LeadMessage, MessageService, Rendezvous.
+ rule = ignoreCycle(rule, "mcp", "msg");
+
+ // fleetd #131 -- found while implementing this test, NOT one of the ticket's
+ // original three; it names its own follow-up step before removal. Evidence:
+ // inject/CompletionResolver.java:4-5, inject/Injector.java:6 and
+ // inject/TurnListener.java:3 import msg.Rendezvous / msg.TurnToken;
+ // msg/MessageService.java:6 imports inject.Injector.
+ rule = ignoreCycle(rule, "inject", "msg");
+
+ // fleetd #131 -- same as above, its own follow-up. Evidence:
+ // metrics/FleetMetrics.java:3 imports msg.ReplyInbox; msg/MessageService.java:7-8,
+ // msg/LeadHeartbeatLoop.java:6-7 and msg/ReplyPushLoop.java:6-7 import
+ // metrics.FleetMetrics / metrics.Metrics.
+ rule = ignoreCycle(rule, "metrics", "msg");
+
+ // fleetd #131 -- same as above, its own follow-up. Evidence:
+ // session/SessionManager.java:7 imports msg.TurnToken;
+ // msg/LeadHeartbeatLoop.java:8 imports session.MemberSession.
+ rule = ignoreCycle(rule, "msg", "session");
+
+ rule.check(classes);
+ }
+
+ /**
+ * Accepts today's known cycle between two top-level packages, and nothing else.
+ * Ignoring both directions removes exactly this pair from cycle detection; every
+ * other dependency -- including any new one added later, between these same two
+ * packages or any other pair -- is still checked.
+ */
+ private static SliceRule ignoreCycle(SliceRule rule, String packageA, String packageB) {
+ return rule
+ .ignoreDependency(residesIn(packageA), residesIn(packageB))
+ .ignoreDependency(residesIn(packageB), residesIn(packageA));
+ }
+
+ private static DescribedPredicate