diff --git a/fleetd/src/test/java/dev/ltms/fleet/mcp/CharterToolSurfaceTest.java b/fleetd/src/test/java/dev/ltms/fleet/mcp/CharterToolSurfaceTest.java new file mode 100644 index 0000000..41ed8b8 --- /dev/null +++ b/fleetd/src/test/java/dev/ltms/fleet/mcp/CharterToolSurfaceTest.java @@ -0,0 +1,72 @@ +package dev.ltms.fleet.mcp; + +import dev.ltms.fleet.config.FleetConfig; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** fleetd #464: launch charters must not name MCP tools the server does not register. */ +class CharterToolSurfaceTest { + + private static final Path MCP_SOURCE = Path.of("src/main/java/dev/ltms/fleet/mcp/FleetMcp.java"); + + private static Set matches(String text, String regex) { + Matcher m = Pattern.compile(regex).matcher(text); + Set found = new LinkedHashSet<>(); + while (m.find()) { + found.add(m.group(1)); + } + return found; + } + + /** Every {@code fleet_*} or legacy {@code bridge_*} token in the configured launch charters. */ + private static Set toolsNamedIn(FleetConfig config) { + return matches(String.join("\n", config.fleet().charters().values()), + "(fleet_[a-z_]+|bridge_[a-z_]+)"); + } + + /** Every tool {@link FleetMcp} registers, read from its {@code tool("…")} calls. */ + private static Set toolsTheServerRegisters() throws Exception { + return matches(Files.readString(MCP_SOURCE), "tool\\(\\\"(fleet_[a-z_]+)\\\""); + } + + @Test + @DisplayName("[SOURCE TEXT] every tool named in a configured charter is registered by the server") + void configuredChartersNameOnlyRegisteredTools(@TempDir Path dir) throws Exception { + Path configFile = dir.resolve("charters.yaml"); + Files.writeString(configFile, """ + fleet: + charters: + dev: | + Send the final handoff through fleet_reply. + reviewer: | + Use fleet_ask only for the lead's decision. + """); + + FleetConfig config = FleetConfig.load(configFile); + Set named = toolsNamedIn(config); + Set registered = toolsTheServerRegisters(); + + assertTrue(!named.isEmpty(), + "the charter fixture named no fleet_* or bridge_* tool. This test would check nothing; " + + "add charter text that names a tool before changing the extraction."); + assertTrue(!registered.isEmpty(), + "the FleetMcp registration scrape found no tools. This test would check nothing; " + + "repair the tool(\"…\") extraction before changing the assertion."); + + Set unknown = new LinkedHashSet<>(named); + unknown.removeAll(registered); + assertTrue(unknown.isEmpty(), + "configured charter text names " + unknown + ", but FleetMcp does not register it. " + + "Checked " + named + " against " + registered + ". Fix the charter text or " + + "register the tool; do NOT weaken this test."); + } +}