Skip to main content

Checking your tools in CI

The starter gatool-spring-boot-starter-test brings two assertions. Add it in test scope beside spring-boot-starter-test:

<dependency>
<groupId>io.gatool</groupId>
<artifactId>gatool-spring-boot-starter-test</artifactId>
<version>0.1.0</version>
<scope>test</scope>
</dependency>

The operation files validate. This assertion is the CI check, the OperationFilesAssert test. It runs the checks that startup runs on the operation files and the schema. It runs from a plain JUnit test, without an issuer, a running application or a reachable schema URL. A broken file therefore fails the build that changed it. Five stops depend on the rest of the configuration, so they run at startup alone:

  • A tool name that clashes with one of the dynamic tools, the tools generated when gatool.dev.experimental.generate-tools is set to dynamic-three-step.

  • A tool name that clashes with a tool the application declares.

  • The scope rules under a shared credential, which is client credentials or a static header, one identity for every caller.

  • The scopes of gatool.mcp.stdio.granted-scopes.

  • An image mime type beside an output schema.

A test that starts the context covers those five:

@Test
void operationFiles_shouldValidate() {
OperationFilesAssert.assertValid(Path.of("src/main/resources/movies.graphqls"),
List.of(Path.of("src/main/resources/gatool/mcp")), List.of(), CheckSettings.defaults());
}

The second argument lists the folders of operation files served over the Model Context Protocol (MCP), the MCP folders. The third lists the folders of operation files served in process, the in-process folders. The CI check reads each folder under that folder's own rules. An application with only one of the two passes List.of() for the other. A listed folder that does not exist fails the assertion, and the message names the folder. A renamed folder therefore cannot pass with zero tools. The CI check follows a symbolic link to a folder. It lists the tools in path order, the way startup reads a location. The fourth argument is a CheckSettings. It carries the naming strategy, the output schema switch and the scalar fragments the application configures. A scalar fragment is a piece of JSON Schema configured under scalar-schemas. The CI check then runs under the same settings startup uses. An application on every default passes CheckSettings.defaults(). One with gatool.naming.strategy: snake-case passes CheckSettings.defaults().withNamingStrategy(ToolNamingStrategy.snakeCase()). One that publishes output schemas or declares scalar fragments builds its settings with the other with... methods.

The tool contract matches its snapshot. A tool name is what the model calls and what a client configuration records. A renamed tool or a changed input schema therefore shows up in review as a changed file:

@SpringBootTest
class ToolContractTests {

@Autowired
GAToolCatalog catalog;

@Test
void tools_shouldMatchTheSnapshot() {
ToolContractSnapshot.of(catalog).assertMatches(Path.of("src/test/resources/gatool-contract.json"));
}
}

When the snapshot is missing, the test writes it and then fails, asking for the file to be committed. A run in CI without the file is therefore a failure. A deliberate change is accepted with -Dgatool.snapshot.update=true. That flag rewrites the file for review. The snapshot records each tool's scopes beside its schemas, because a changed scope list is a contract change a client notices. The file is written with line feeds on every platform. The path is resolved against the JVM's working directory. Under Maven, Gradle and the IDEs that is the module directory. Under Gradle the flag reaches the test JVM through systemProperty on the test task.