diff --git a/README.md b/README.md index ac1d7a66..53e3e9a9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,13 @@ [![Project Status: Incubating](https://img.shields.io/badge/status-incubating-yellow.svg)](https://github.com/apache/solr-mcp) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=alert_status)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=security_rating)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=reliability_rating)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=sqale_rating)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=bugs)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=vulnerabilities)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=code_smells)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=ncloc)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=duplicated_lines_density)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) # Solr MCP Server diff --git a/build.gradle.kts b/build.gradle.kts index a9ab9a75..36072d6a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -579,6 +579,13 @@ jib { } } +// Jib's tasks read Task.project at execution time, which the configuration cache rejects. +// Opt them out so jibDockerBuild/jib work with org.gradle.configuration-cache=true; every +// other task keeps using the cache. +tasks.withType().configureEach { + notCompatibleWithConfigurationCache("Jib reads Task.project at execution time") +} + tasks.bootBuildImage { if (nativeBuild) { imageName.set("solr-mcp:$version-native-$nativeProfile") diff --git a/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/GenerateBinaryLicense.kt b/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/GenerateBinaryLicense.kt index 3da0c157..69c8b449 100644 --- a/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/GenerateBinaryLicense.kt +++ b/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/GenerateBinaryLicense.kt @@ -164,12 +164,15 @@ abstract class GenerateBinaryLicense : DefaultTask() { private fun licensesOf(component: Component): List { val choice = component.licenses ?: return emptyList() val out = LinkedHashMap() - for (license in choice.licenses.orEmpty()) { - val id = license.id - val label = id ?: license.name ?: "Unspecified" - out.putIfAbsent(label, license.url ?: id?.let { spdxUrl(it) }) + for (item in choice.items.orEmpty()) { + val license = item.license + if (license != null) { + val id = license.id + val label = id ?: license.name ?: "Unspecified" + out.putIfAbsent(label, license.url ?: id?.let { spdxUrl(it) }) + } + item.expression?.value?.let { out.putIfAbsent(it, null) } } - choice.expression?.value?.let { out.putIfAbsent(it, null) } return out.map { License(it.key, it.value) } } diff --git a/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/LicenseNoticeTasksTest.kt b/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/LicenseNoticeTasksTest.kt index 457ccd6d..e3f904a3 100644 --- a/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/LicenseNoticeTasksTest.kt +++ b/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/LicenseNoticeTasksTest.kt @@ -81,6 +81,24 @@ class LicenseNoticeTasksTest { assertTrue(text.contains("License: BSD licence — http://antlr.org/license.html")) } + @Test + fun `license reports an SPDX expression as written`() { + val task = licenseTask() + write("LICENSE", "BASE").let(task.baseLicense::set) + write( + "sbom.json", + """{"components":[{"group":"org.example","name":"dual","version":"1.0", + "licenses":[{"expression":"EPL-2.0 OR GPL-2.0-with-classpath-exception"}]}]}""", + ).let(task.sbom::set) + task.bundledCoordinates.set(listOf("org.example:dual:1.0")) + val out = File(tempDir, "out/LICENSE") + task.outputFile.set(out) + + task.generate() + + assertTrue(out.readText().contains("License: EPL-2.0 OR GPL-2.0-with-classpath-exception")) + } + @Test fun `license gate fails when a bundled dependency is absent from the SBOM`() { val task = licenseTask() diff --git a/dev-docs/graalvm-native-image.md b/dev-docs/graalvm-native-image.md index de0b7b37..930d052a 100644 --- a/dev-docs/graalvm-native-image.md +++ b/dev-docs/graalvm-native-image.md @@ -123,10 +123,10 @@ from the `PROFILES` env var; the native image already made it at *build time*. Spring AOT generates most hints automatically, but a few types are invisible to it and must be registered by hand. They live in -`SolrConfig`'s sibling `SolrNativeHints.java`, a `@Configuration` annotated -`@ImportRuntimeHints(...)`. Registering hints through a `RuntimeHintsRegistrar` -(rather than scattering `@RegisterReflection` annotations) keeps the whole -reflective surface in one reviewable place. The class is registered +`SolrNativeHints.java`, a `RuntimeHintsRegistrar` that `SolrConfig` imports with +`@ImportRuntimeHints(SolrNativeHints.class)`. Registering hints through one +registrar (rather than scattering `@RegisterReflection` annotations) keeps the +whole reflective surface in one reviewable place. The registrar is imported unconditionally; on the JVM path it is simply a no-op. ### Wire format choice underpins the small hint surface @@ -175,7 +175,7 @@ container and value types. 1. Reproduce with `./gradlew nativeTest -Pnative` (or run the native binary and trigger the failing path). GraalVM reports the missing reflective/resource element. -2. Add a targeted registration in `SolrNativeHints.Registrar` — a single +2. Add a targeted registration in `SolrNativeHints` — a single `registerType(...)` with `INVOKE_DECLARED_CONSTRUCTORS`, `INVOKE_DECLARED_METHODS`, `DECLARED_FIELDS`, or a `registerPattern(...)` for a resource. Prefer this over the tracing agent so the rule is explicit and diff --git a/src/main/java/org/apache/solr/mcp/server/Main.java b/src/main/java/org/apache/solr/mcp/server/Main.java index a328f3cf..8812d219 100644 --- a/src/main/java/org/apache/solr/mcp/server/Main.java +++ b/src/main/java/org/apache/solr/mcp/server/Main.java @@ -93,7 +93,9 @@ public class Main { /** Default constructor used by Spring Boot to bootstrap the application. */ - public Main() {} + public Main() { + // Declared only to carry the Javadoc above; javadoc warns on an implicit constructor. + } /** * Logback's own system property naming the status listener to install during its diff --git a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java index fa5a62f0..030b792a 100644 --- a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java +++ b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java @@ -824,8 +824,8 @@ private boolean isHandlerStatsEmpty(HandlerStats stats) { Long errors = getLong(coreMetrics, keyPrefix + ERRORS_FIELD); Long timeouts = getLong(coreMetrics, keyPrefix + TIMEOUTS_FIELD); Long totalTime = getLong(coreMetrics, keyPrefix + TOTAL_TIME_FIELD); - // avgTimePerRequest and avgRequestsPerSecond are not available as flat metrics; - // compute avgTimePerRequest from totalTime/requests when possible + // Solr has no flat metric for the average time per request or for requests per + // second, so the average time is derived from the total time and the request count. Float avgTimePerRequest = (requests > 0 && totalTime != null) ? (float) totalTime / requests : null; return new HandlerInfo(requests, errors, timeouts, totalTime, avgTimePerRequest, null); @@ -1051,7 +1051,7 @@ public String setupCollectionPrompt( required = false) @Nullable String purpose) { String purposeLine = - (purpose == null || purpose.isBlank()) ? "" : "\nPurpose: %s\n".formatted(purpose.strip()); + (purpose == null || purpose.isBlank()) ? "" : "\nPurpose: " + purpose.strip() + "\n"; return """ You are setting up a new Solr collection named `%s` through MCP tools.%s 1. Validate the name. diff --git a/src/main/java/org/apache/solr/mcp/server/collection/Dtos.java b/src/main/java/org/apache/solr/mcp/server/collection/Dtos.java index 3c389089..38cd1dc8 100644 --- a/src/main/java/org/apache/solr/mcp/server/collection/Dtos.java +++ b/src/main/java/org/apache/solr/mcp/server/collection/Dtos.java @@ -22,24 +22,6 @@ import java.time.Instant; import org.jspecify.annotations.Nullable; -/** - * Data Transfer Objects (DTOs) for the Apache Solr MCP Server. - * - *

This package contains all the data transfer objects used to serialize and deserialize Solr - * metrics, search results, and health status information for Model Context Protocol (MCP) clients. - * All DTOs use Java records for immutability and Jackson annotations for JSON serialization. - * - *

Key Features: - * - *

    - *
  • Automatic null value exclusion from JSON output using - * {@code @JsonInclude(JsonInclude.Include.NON_NULL)} - *
  • Resilient JSON parsing with {@code @JsonIgnoreProperties(ignoreUnknown = true)} - *
  • Immutable data structures using Java records - *
  • ISO 8601 timestamp formatting for consistent date serialization - *
- */ - /** * Top-level container for comprehensive Solr collection metrics. * @@ -67,6 +49,13 @@ * } * } * + * @param indexStats Index-related statistics including document counts and segment information + * @param queryStats Query performance metrics from the most recent search operations + * @param cacheStats Cache utilization statistics for query result, document, and filter caches (may + * be null) + * @param handlerStats Request handler performance metrics for select and update operations (may be + * null) + * @param timestamp Timestamp when these metrics were collected, formatted as ISO 8601 * @see IndexStats * @see QueryStats * @see CacheStats @@ -75,19 +64,10 @@ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record SolrMetrics( - /** Index-related statistics including document counts and segment information */ IndexStats indexStats, - - /** Query performance metrics from the most recent search operations */ QueryStats queryStats, - - /** Cache utilization statistics for query result, document, and filter caches (may be null) */ @Nullable CacheStats cacheStats, - - /** Request handler performance metrics for select and update operations (may be null) */ @Nullable HandlerStats handlerStats, - - /** Timestamp when these metrics were collected, formatted as ISO 8601 */ @JsonFormat(shape = JsonFormat.Shape.STRING) Instant timestamp) {} /** @@ -109,18 +89,14 @@ record SolrMetrics( *

High segment counts may indicate the need for index optimization to improve search * performance. The optimal segment count depends on index size and update frequency. * + * @param numDocs Total number of documents in the index (excluding deleted documents) + * @param segmentCount Number of Lucene segments in the index (lower numbers generally indicate + * better performance) * @see org.apache.solr.client.solrj.request.LukeRequest */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) -record IndexStats( - /** Total number of documents in the index (excluding deleted documents) */ - @Nullable Integer numDocs, - - /** - * Number of Lucene segments in the index (lower numbers generally indicate better performance) - */ - @Nullable Integer segmentCount) {} +record IndexStats(@Nullable Integer numDocs, @Nullable Integer segmentCount) {} /** * Query execution performance metrics from Solr search operations. @@ -142,22 +118,15 @@ record IndexStats( *

Query time metrics help identify slow queries that may need optimization, while result counts * and scores provide insight into search effectiveness and relevance tuning needs. * + * @param queryTime Time in milliseconds required to execute the most recent query + * @param totalResults Total number of documents matching the query criteria + * @param start Starting position for paginated results (0-based offset) + * @param maxScore Highest relevance score among the returned documents * @see org.apache.solr.client.solrj.response.QueryResponse */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) -record QueryStats( - /** Time in milliseconds required to execute the most recent query */ - Integer queryTime, - - /** Total number of documents matching the query criteria */ - Long totalResults, - - /** Starting position for paginated results (0-based offset) */ - Long start, - - /** Highest relevance score among the returned documents */ - @Nullable Float maxScore) {} +record QueryStats(Integer queryTime, Long totalResults, Long start, @Nullable Float maxScore) {} /** * Solr cache utilization statistics across all cache types. @@ -179,18 +148,16 @@ record QueryStats( *

Poor cache hit ratios may indicate undersized caches or query patterns that don't benefit from * caching. Cache evictions suggest memory pressure or cache size optimization needs. * + * @param queryResultCache Performance metrics for the query result cache + * @param documentCache Performance metrics for the document cache + * @param filterCache Performance metrics for the filter cache * @see CacheInfo */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record CacheStats( - /** Performance metrics for the query result cache */ @Nullable CacheInfo queryResultCache, - - /** Performance metrics for the document cache */ @Nullable CacheInfo documentCache, - - /** Performance metrics for the filter cache */ @Nullable CacheInfo filterCache) {} /** @@ -213,26 +180,22 @@ record CacheStats( * *

Optimal cache performance typically shows high hit ratios (>0.80) with minimal evictions. High * eviction rates suggest cache size increases may improve performance. + * + * @param lookups Total number of cache lookup requests + * @param hits Number of successful cache hits + * @param hitratio Cache hit ratio (hits/lookups) - higher values indicate better cache performance + * @param inserts Number of new entries added to the cache + * @param evictions Number of entries removed due to cache size limits (indicates memory pressure) + * @param size Current number of entries stored in the cache */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record CacheInfo( - /** Total number of cache lookup requests */ @Nullable Long lookups, - - /** Number of successful cache hits */ @Nullable Long hits, - - /** Cache hit ratio (hits/lookups) - higher values indicate better cache performance */ @Nullable Float hitratio, - - /** Number of new entries added to the cache */ @Nullable Long inserts, - - /** Number of entries removed due to cache size limits (indicates memory pressure) */ @Nullable Long evictions, - - /** Current number of entries stored in the cache */ @Nullable Long size) {} /** @@ -254,16 +217,13 @@ record CacheInfo( *

Handler metrics help identify bottlenecks in request processing and guide capacity planning * decisions. High error rates or response times indicate potential optimization needs. * + * @param selectHandler Performance metrics for the search/select request handler + * @param updateHandler Performance metrics for the document update request handler * @see HandlerInfo */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) -record HandlerStats( - /** Performance metrics for the search/select request handler */ - @Nullable HandlerInfo selectHandler, - - /** Performance metrics for the document update request handler */ - @Nullable HandlerInfo updateHandler) {} +record HandlerStats(@Nullable HandlerInfo selectHandler, @Nullable HandlerInfo updateHandler) {} /** * Detailed performance metrics for individual Solr request handlers. @@ -285,26 +245,22 @@ record HandlerStats( * *

High error rates may indicate system stress or configuration issues. Increasing response times * suggest capacity limits or optimization needs. + * + * @param requests Total number of requests processed by this handler + * @param errors Number of requests that resulted in errors + * @param timeouts Number of requests that exceeded timeout limits + * @param totalTime Cumulative time spent processing all requests (milliseconds) + * @param avgTimePerRequest Average time per request in milliseconds + * @param avgRequestsPerSecond Average throughput in requests per second */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record HandlerInfo( - /** Total number of requests processed by this handler */ @Nullable Long requests, - - /** Number of requests that resulted in errors */ @Nullable Long errors, - - /** Number of requests that exceeded timeout limits */ @Nullable Long timeouts, - - /** Cumulative time spent processing all requests (milliseconds) */ @Nullable Long totalTime, - - /** Average time per request in milliseconds */ @Nullable Float avgTimePerRequest, - - /** Average throughput in requests per second */ @Nullable Float avgRequestsPerSecond) {} /** @@ -336,26 +292,22 @@ record HandlerInfo( * logger.error("Collection unhealthy: " + status.getErrorMessage()); * } * } + * + * @param isHealthy Overall health status - true if collection is operational and responding + * @param errorMessage Detailed error message when isHealthy is false, null when healthy + * @param responseTime Response time in milliseconds for the health check ping request + * @param totalDocuments Total number of documents currently indexed in the collection + * @param lastChecked Timestamp when this health check was performed, formatted as ISO 8601 + * @param collection Name of the collection that was checked */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record SolrHealthStatus( - /** Overall health status - true if collection is operational and responding */ boolean isHealthy, - - /** Detailed error message when isHealthy is false, null when healthy */ @Nullable String errorMessage, - - /** Response time in milliseconds for the health check ping request */ @Nullable Long responseTime, - - /** Total number of documents currently indexed in the collection */ @Nullable Long totalDocuments, - - /** Timestamp when this health check was performed, formatted as ISO 8601 */ @JsonFormat(shape = JsonFormat.Shape.STRING) Instant lastChecked, - - /** Name of the collection that was checked */ String collection) {} /** @@ -364,18 +316,16 @@ record SolrHealthStatus( *

Returned by the {@code create-collection} MCP tool to communicate the outcome of a collection * creation request. On success, {@code success} is {@code true} and {@code createdAt} records when * the operation completed. + * + * @param name Name of the collection that was created + * @param success Whether the collection was successfully created + * @param message Optional message describing the outcome + * @param createdAt Timestamp when the collection was created, formatted as ISO 8601 */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record CollectionCreationResult( - /** Name of the collection that was created */ String name, - - /** Whether the collection was successfully created */ boolean success, - - /** Optional message describing the outcome */ String message, - - /** Timestamp when the collection was created, formatted as ISO 8601 */ @JsonFormat(shape = JsonFormat.Shape.STRING) Instant createdAt) {} diff --git a/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java b/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java index 48df20e6..ae0de2bf 100644 --- a/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java +++ b/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java @@ -24,6 +24,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.util.StringUtils; import tools.jackson.databind.json.JsonMapper; @@ -85,6 +86,7 @@ */ @Configuration @EnableConfigurationProperties(SolrConfigurationProperties.class) +@ImportRuntimeHints(SolrNativeHints.class) public class SolrConfig { private static final int CONNECTION_TIMEOUT_MS = 10000; @@ -92,7 +94,9 @@ public class SolrConfig { private static final String SOLR_PATH = "solr/"; /** Default constructor used by Spring to instantiate this configuration. */ - public SolrConfig() {} + public SolrConfig() { + // Declared only to carry the Javadoc above; javadoc warns on an implicit constructor. + } /** * Creates and configures a SolrClient bean for Apache Solr communication. @@ -170,7 +174,8 @@ private static SolrClient buildSolrClient( URI parsed = URI.create(properties.url()); String path = parsed.getPath() == null ? "" : parsed.getPath(); if (!path.endsWith("/")) { - path = path + "/"; + // A URL path separator is '/' on every platform; it is not the file separator S1075 assumes. + path = path + "/"; // NOSONAR java:S1075 } if (!path.contains("/" + SOLR_PATH)) { path = path + SOLR_PATH; diff --git a/src/main/java/org/apache/solr/mcp/server/config/SolrConfigurationProperties.java b/src/main/java/org/apache/solr/mcp/server/config/SolrConfigurationProperties.java index eab54226..57603b90 100644 --- a/src/main/java/org/apache/solr/mcp/server/config/SolrConfigurationProperties.java +++ b/src/main/java/org/apache/solr/mcp/server/config/SolrConfigurationProperties.java @@ -139,7 +139,7 @@ private static boolean isAbsoluteHttpUrlWithHost(@Nullable String url) { URI uri; try { uri = URI.create(url); - } catch (IllegalArgumentException ex) { + } catch (IllegalArgumentException _) { return false; } String scheme = uri.getScheme(); diff --git a/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java b/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java index 7fc28178..db17a355 100644 --- a/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java +++ b/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java @@ -30,8 +30,6 @@ import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.ImportRuntimeHints; /** * GraalVM native image reachability hints for SolrJ types this project actually uses. @@ -41,18 +39,13 @@ * native-image issues. The hints below cover the narrow remaining surface: response containers and * the {@code NamedList} admin shape returned by the mbeans path. * - *

This class is registered unconditionally — on the JVM path it is a no-op because no runtime - * reflection happens against these types at startup. It is only meaningful when running as a native - * image. + *

{@link SolrConfig} imports this registrar unconditionally — on the JVM path it is a no-op + * because no runtime reflection happens against these types at startup. It is only meaningful when + * running as a native image. * * @see SolrConfig */ -@Configuration -@ImportRuntimeHints(SolrNativeHints.Registrar.class) -public class SolrNativeHints { - - /** Default constructor used by Spring to instantiate this configuration. */ - public SolrNativeHints() {} +class SolrNativeHints implements RuntimeHintsRegistrar { /** * Record types used by {@code @McpTool} methods as responses or parameters. Jackson needs @@ -75,107 +68,105 @@ public SolrNativeHints() {} "org.apache.solr.mcp.server.search.SortClause", "org.apache.solr.mcp.server.schema.SchemaUpdateResult"); - static class Registrar implements RuntimeHintsRegistrar { - @Override - public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { - MemberCategory[] categories = { - MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.INVOKE_DECLARED_METHODS, - MemberCategory.ACCESS_DECLARED_FIELDS - }; - - // SolrJ response types - hints.reflection().registerType(QueryResponse.class, categories); - hints.reflection().registerType(UpdateResponse.class, categories); - hints.reflection().registerType(NamedList.class, categories); - hints.reflection().registerType(SimpleOrderedMap.class, categories); - hints.reflection().registerType(SolrDocument.class, categories); - hints.reflection().registerType(SolrDocumentList.class, categories); - - // SolrJ input/indexing types - hints.reflection().registerType(SolrInputDocument.class, categories); - hints.reflection().registerType(SolrInputField.class, categories); - - // SolrJ facet types - hints.reflection().registerType(FacetField.class, categories); - hints.reflection().registerType(FacetField.Count.class, categories); - - // SolrJ / Solr API model types used by CoreAdminResponse.getCoreStatus() - // which deserializes via Jackson reflection. - for (String solrApiType : - List.of( - "org.apache.solr.client.api.model.SolrJerseyResponse", - "org.apache.solr.client.api.model.SolrJerseyResponse$ResponseHeader", - "org.apache.solr.client.api.model.ErrorInfo", - "org.apache.solr.client.api.model.CoreStatusResponse", - "org.apache.solr.client.api.model.CoreStatusResponse$SingleCoreData", - "org.apache.solr.client.api.model.CoreStatusResponse$CloudDetails", - "org.apache.solr.client.api.model.CoreStatusResponse$IndexDetails")) { - hints.reflection().registerTypeIfPresent(classLoader, solrApiType, categories); - } - - // Spring AI MCP annotation internals — MetaUtils reflectively - // instantiates DefaultMetaProvider via its no-arg constructor. - hints - .reflection() - .registerTypeIfPresent( - classLoader, - "org.springframework.ai.mcp.annotation.context.DefaultMetaProvider", - categories); - - // SolrJ schema request types (needed for Jackson's convertValue in native - // image when add-field-types deserializes analyzer trees) - hints - .reflection() - .registerType( - org.apache.solr.client.solrj.request.schema.AnalyzerDefinition.class, categories); - hints - .reflection() - .registerType( - org.apache.solr.client.solrj.request.schema.FieldTypeDefinition.class, categories); - - // SolrJ schema response type — returned by the get-schema MCP tool and - // serialized to JSON by Spring AI for MCP clients. Without reflection - // hints the JSON Spring AI produces in native image is missing the - // fields/fieldTypes/dynamicFields/copyFields arrays, which silently - // breaks any consumer that introspects the schema. - hints - .reflection() - .registerType( - org.apache.solr.client.solrj.response.schema.SchemaRepresentation.class, categories); - - // MCP tool request/response records (some package-private, registered by - // name); request records are deserialized reflectively by Jackson when - // tool arguments are bound - for (String className : MCP_TOOL_RECORDS) { - hints.reflection().registerTypeIfPresent(classLoader, className, categories); - } - - // SolrJ EnvUtils loads these properties files in its static - // initializer; without them getResourceAsStream returns null - // and the throws NullPointerException. - hints.resources().registerPattern("EnvToSyspropMappings.properties"); - hints.resources().registerPattern("DeprecatedSystemPropertyMappings.properties"); - - // Older springaicommunity location of DefaultMetaProvider (Spring AI 1.x - // transitive). Spring AI 2.x relocated this to - // org.springframework.ai.mcp.annotation.context.DefaultMetaProvider, - // which is registered above. Kept defensively via registerTypeIfPresent - // so this is a no-op when the older class isn't on the classpath. - hints - .reflection() - .registerTypeIfPresent( - classLoader, - "org.springaicommunity.mcp.context.DefaultMetaProvider", - MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + MemberCategory[] categories = { + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_DECLARED_METHODS, + MemberCategory.ACCESS_DECLARED_FIELDS + }; + + // SolrJ response types + hints.reflection().registerType(QueryResponse.class, categories); + hints.reflection().registerType(UpdateResponse.class, categories); + hints.reflection().registerType(NamedList.class, categories); + hints.reflection().registerType(SimpleOrderedMap.class, categories); + hints.reflection().registerType(SolrDocument.class, categories); + hints.reflection().registerType(SolrDocumentList.class, categories); + + // SolrJ input/indexing types + hints.reflection().registerType(SolrInputDocument.class, categories); + hints.reflection().registerType(SolrInputField.class, categories); + + // SolrJ facet types + hints.reflection().registerType(FacetField.class, categories); + hints.reflection().registerType(FacetField.Count.class, categories); + + // SolrJ / Solr API model types used by CoreAdminResponse.getCoreStatus() + // which deserializes via Jackson reflection. + for (String solrApiType : + List.of( + "org.apache.solr.client.api.model.SolrJerseyResponse", + "org.apache.solr.client.api.model.SolrJerseyResponse$ResponseHeader", + "org.apache.solr.client.api.model.ErrorInfo", + "org.apache.solr.client.api.model.CoreStatusResponse", + "org.apache.solr.client.api.model.CoreStatusResponse$SingleCoreData", + "org.apache.solr.client.api.model.CoreStatusResponse$CloudDetails", + "org.apache.solr.client.api.model.CoreStatusResponse$IndexDetails")) { + hints.reflection().registerTypeIfPresent(classLoader, solrApiType, categories); + } - // logback-spring.xml is the only logback configuration; Spring Boot - // resolves it by convention. Under AOT the parsed model is replayed - // from META-INF/spring/logback-model, so this hint is belt-and-braces - // for the non-AOT path. Logback's own pre-Spring status output is - // silenced by Main, not by a second configuration file; see - // LoggingConfigurationTest. - hints.resources().registerPattern("logback-spring.xml"); + // Spring AI MCP annotation internals — MetaUtils reflectively + // instantiates DefaultMetaProvider via its no-arg constructor. + hints + .reflection() + .registerTypeIfPresent( + classLoader, + "org.springframework.ai.mcp.annotation.context.DefaultMetaProvider", + categories); + + // SolrJ schema request types (needed for Jackson's convertValue in native + // image when add-field-types deserializes analyzer trees) + hints + .reflection() + .registerType( + org.apache.solr.client.solrj.request.schema.AnalyzerDefinition.class, categories); + hints + .reflection() + .registerType( + org.apache.solr.client.solrj.request.schema.FieldTypeDefinition.class, categories); + + // SolrJ schema response type — returned by the get-schema MCP tool and + // serialized to JSON by Spring AI for MCP clients. Without reflection + // hints the JSON Spring AI produces in native image is missing the + // fields/fieldTypes/dynamicFields/copyFields arrays, which silently + // breaks any consumer that introspects the schema. + hints + .reflection() + .registerType( + org.apache.solr.client.solrj.response.schema.SchemaRepresentation.class, categories); + + // MCP tool request/response records (some package-private, registered by + // name); request records are deserialized reflectively by Jackson when + // tool arguments are bound + for (String className : MCP_TOOL_RECORDS) { + hints.reflection().registerTypeIfPresent(classLoader, className, categories); } + + // SolrJ EnvUtils loads these properties files in its static + // initializer; without them getResourceAsStream returns null + // and the throws NullPointerException. + hints.resources().registerPattern("EnvToSyspropMappings.properties"); + hints.resources().registerPattern("DeprecatedSystemPropertyMappings.properties"); + + // Older springaicommunity location of DefaultMetaProvider (Spring AI 1.x + // transitive). Spring AI 2.x relocated this to + // org.springframework.ai.mcp.annotation.context.DefaultMetaProvider, + // which is registered above. Kept defensively via registerTypeIfPresent + // so this is a no-op when the older class isn't on the classpath. + hints + .reflection() + .registerTypeIfPresent( + classLoader, + "org.springaicommunity.mcp.context.DefaultMetaProvider", + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + + // logback-spring.xml is the only logback configuration; Spring Boot + // resolves it by convention. Under AOT the parsed model is replayed + // from META-INF/spring/logback-model, so this hint is belt-and-braces + // for the non-AOT path. Logback's own pre-Spring status output is + // silenced by Main, not by a second configuration file; see + // LoggingConfigurationTest. + hints.resources().registerPattern("logback-spring.xml"); } } diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java index c48145d4..9571377a 100644 --- a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java +++ b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java @@ -218,14 +218,14 @@ private void processFieldValue(SolrInputDocument doc, JsonNode value, String fie * be added */ private void processArrayField(SolrInputDocument doc, JsonNode arrayValue, String fieldName) { - List values = new ArrayList<>(); - for (JsonNode item : arrayValue) { - // Skip objects and nested arrays alike: neither has a scalar - // representation, and asString() on a container node throws. - if (!item.isObject() && !item.isArray()) { - values.add(convertJsonValue(item)); - } - } + // Skip objects and nested arrays alike: neither has a scalar + // representation, and asString() on a container node throws. + List values = + arrayValue + .valueStream() + .filter(item -> !item.isContainer()) + .map(this::convertJsonValue) + .toList(); if (!values.isEmpty()) { doc.addField(fieldName, values); } diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java index bff112b7..8557dec3 100644 --- a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java +++ b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java @@ -33,7 +33,6 @@ import org.commonmark.node.Code; import org.commonmark.node.Heading; import org.commonmark.node.Node; -import org.commonmark.node.SourceSpan; import org.commonmark.node.Text; import org.commonmark.parser.IncludeSourceSpans; import org.commonmark.parser.Parser; @@ -136,7 +135,9 @@ public MarkdownDocumentCreator() { options, new Resolver() { @Override - protected void addImplicitResolvers() {} + protected void addImplicitResolvers() { + // Registers none, so every scalar stays a string. + } }); this.textContentRenderer = TextContentRenderer.builder().build(); } @@ -164,11 +165,7 @@ public List create(String markdown) throws DocumentProcessing return List.of(); } - List documents = new ArrayList<>(); - for (String record : splitRecords(markdown)) { - documents.add(createOne(record)); - } - return List.copyOf(documents); + return splitRecords(markdown).stream().map(this::createOne).toList(); } /** Creates the single document described by one record. */ @@ -291,10 +288,10 @@ private void addFrontMatterFields(String markdown, Node document, SolrInputDocum if (!(firstChild instanceof YamlFrontMatterBlock block)) { return; } - List lines = new ArrayList<>(); - for (SourceSpan span : block.getSourceSpans()) { - lines.add(markdown.substring(span.getInputIndex(), span.getInputIndex() + span.getLength())); - } + List lines = + block.getSourceSpans().stream() + .map(s -> markdown.substring(s.getInputIndex(), s.getInputIndex() + s.getLength())) + .toList(); // The first and last spans are the --- delimiters String text = String.join("\n", lines.subList(1, Math.max(1, lines.size() - 1))); Object data; @@ -311,7 +308,9 @@ private void addFrontMatterFields(String markdown, Node document, SolrInputDocum private static void addValue(SolrInputDocument doc, String fieldName, @Nullable Object value) { switch (value) { - case null -> {} + case null -> { + // A key with no value, such as `tags:`, adds no field. + } case Map nested -> nested.forEach((key, inner) -> addValue(doc, fieldName + "_" + key, inner)); case Iterable values -> values.forEach(element -> addValue(doc, fieldName, element)); diff --git a/src/test/java/org/apache/solr/mcp/server/BuildInfoReader.java b/src/test/java/org/apache/solr/mcp/server/BuildInfoReader.java index 60bccf5f..6d72be3c 100644 --- a/src/test/java/org/apache/solr/mcp/server/BuildInfoReader.java +++ b/src/test/java/org/apache/solr/mcp/server/BuildInfoReader.java @@ -99,7 +99,7 @@ public static String getVersion() { */ public static String getDockerImageName() { String suffix = System.getProperty("solr.mcp.docker.image.tag.suffix", ""); - return String.format("%s:%s%s", getArtifact(), getVersion(), suffix); + return getArtifact() + ":" + getVersion() + suffix; } /** @@ -108,6 +108,6 @@ public static String getDockerImageName() { * @return JAR file name (e.g., "solr-mcp-1.0.0-SNAPSHOT.jar") */ public static String getJarFileName() { - return String.format("%s-%s.jar", getArtifact(), getVersion()); + return getArtifact() + "-" + getVersion() + ".jar"; } } diff --git a/src/test/java/org/apache/solr/mcp/server/ClientHttp.java b/src/test/java/org/apache/solr/mcp/server/ClientHttp.java index b482d5c2..15bdad99 100644 --- a/src/test/java/org/apache/solr/mcp/server/ClientHttp.java +++ b/src/test/java/org/apache/solr/mcp/server/ClientHttp.java @@ -22,7 +22,7 @@ @Disabled("Enable only when MCP server is running in http mode") public class ClientHttp { - public static void main(String[] args) { + static void main() { var transport = HttpClientStreamableHttpTransport.builder("http://localhost:8080").build(); new SampleClient(transport).run(); } diff --git a/src/test/java/org/apache/solr/mcp/server/ClientStdio.java b/src/test/java/org/apache/solr/mcp/server/ClientStdio.java index 60055305..992e041f 100644 --- a/src/test/java/org/apache/solr/mcp/server/ClientStdio.java +++ b/src/test/java/org/apache/solr/mcp/server/ClientStdio.java @@ -27,7 +27,7 @@ public class ClientStdio { static void main() { - String jarName = String.format("build/libs/%s", BuildInfoReader.getJarFileName()); + String jarName = "build/libs/" + BuildInfoReader.getJarFileName(); var stdioParams = ServerParameters.builder("java").args("-jar", jarName).build(); diff --git a/src/test/java/org/apache/solr/mcp/server/MainTest.java b/src/test/java/org/apache/solr/mcp/server/MainTest.java index 96e19e21..e7a16d61 100644 --- a/src/test/java/org/apache/solr/mcp/server/MainTest.java +++ b/src/test/java/org/apache/solr/mcp/server/MainTest.java @@ -16,13 +16,17 @@ */ package org.apache.solr.mcp.server; +import static org.assertj.core.api.Assertions.assertThat; + import org.apache.solr.mcp.server.collection.CollectionService; import org.apache.solr.mcp.server.indexing.IndexingService; import org.apache.solr.mcp.server.schema.SchemaService; import org.apache.solr.mcp.server.search.SearchService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledInNativeImage; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -36,6 +40,8 @@ @DisabledInNativeImage class MainTest { + @Autowired private ApplicationContext context; + @MockitoBean private SearchService searchService; @MockitoBean private IndexingService indexingService; @@ -46,6 +52,10 @@ class MainTest { @Test void contextLoads() { - // Context loading test - all services are mocked to prevent Solr API calls + // The context starts with every Solr-calling service replaced by its mock + assertThat(context.getBean(SearchService.class)).isSameAs(searchService); + assertThat(context.getBean(IndexingService.class)).isSameAs(indexingService); + assertThat(context.getBean(CollectionService.class)).isSameAs(collectionService); + assertThat(context.getBean(SchemaService.class)).isSameAs(schemaService); } } diff --git a/src/test/java/org/apache/solr/mcp/server/SampleDataRuntimeHints.java b/src/test/java/org/apache/solr/mcp/server/SampleDataRuntimeHints.java index 25baf758..479a3188 100644 --- a/src/test/java/org/apache/solr/mcp/server/SampleDataRuntimeHints.java +++ b/src/test/java/org/apache/solr/mcp/server/SampleDataRuntimeHints.java @@ -42,9 +42,6 @@ */ public class SampleDataRuntimeHints implements TestRuntimeHintsRegistrar { - /** Default constructor used by Spring's AOT services loader. */ - public SampleDataRuntimeHints() {} - @Override public void registerHints(RuntimeHints hints, Class testClass, ClassLoader classLoader) { hints diff --git a/src/test/java/org/apache/solr/mcp/server/config/SolrNativeHintsTest.java b/src/test/java/org/apache/solr/mcp/server/config/SolrNativeHintsTest.java index 78bfba63..a8780a93 100644 --- a/src/test/java/org/apache/solr/mcp/server/config/SolrNativeHintsTest.java +++ b/src/test/java/org/apache/solr/mcp/server/config/SolrNativeHintsTest.java @@ -27,8 +27,8 @@ import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; /** - * Verifies that {@link SolrNativeHints.Registrar} registers the reflection and resource hints the - * native image depends on. + * Verifies that {@link SolrNativeHints} registers the reflection and resource hints the native + * image depends on. * *

The hints are otherwise only exercised by {@code nativeTest -Pnative} (a full GraalVM build), * so an accidentally removed registration would surface as a hard-to-diagnose native-only startup @@ -41,7 +41,7 @@ class SolrNativeHintsTest { @BeforeEach void registerHints() { - new SolrNativeHints.Registrar().registerHints(hints, getClass().getClassLoader()); + new SolrNativeHints().registerHints(hints, getClass().getClassLoader()); } @Test diff --git a/src/test/java/org/apache/solr/mcp/server/containerization/DockerImageStdioIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/containerization/DockerImageStdioIntegrationTest.java index 7e07c96e..6ccfdbbd 100644 --- a/src/test/java/org/apache/solr/mcp/server/containerization/DockerImageStdioIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/containerization/DockerImageStdioIntegrationTest.java @@ -115,13 +115,16 @@ class DockerImageStdioIntegrationTest { .withStartupTimeout(Duration.ofSeconds(60)); @BeforeAll - static void setup() throws InterruptedException { + static void setup() { log.info("Solr container started. Internal URL: http://solr:8983/solr/"); log.info("MCP Server container starting. Waiting for initialization..."); - // Give the MCP server a few seconds to initialize + // Give the MCP server a few seconds to initialize, failing early if it exits. // In STDIO mode, the app runs but doesn't produce logs we can monitor - Thread.sleep(5000); + await() + .during(Duration.ofSeconds(5)) + .atMost(Duration.ofSeconds(10)) + .until(mcpServerContainer::isRunning); log.info("Initialization wait complete. Beginning tests."); } diff --git a/src/test/java/org/apache/solr/mcp/server/observability/DistributedTracingTest.java b/src/test/java/org/apache/solr/mcp/server/observability/DistributedTracingTest.java index 4e887e9d..84c127dc 100644 --- a/src/test/java/org/apache/solr/mcp/server/observability/DistributedTracingTest.java +++ b/src/test/java/org/apache/solr/mcp/server/observability/DistributedTracingTest.java @@ -42,7 +42,6 @@ *

    *
  • Spans are created for @Observed methods *
  • Span attributes are correctly set - *
  • Span hierarchy is correct (parent-child relationships) *
  • Span names follow conventions *
* @@ -139,64 +138,6 @@ void shouldIncludeSpanAttributes() { }); } - @Test - void shouldCreateSpanHierarchy() { - // When: We execute a complex operation that triggers multiple spans - try { - searchService.search("test_collection", "*:*", null, null, null, null, null); - } catch (Exception _) { - // Ignore errors - } - - // Then: We should see spans created - await() - .atMost(5, TimeUnit.SECONDS) - .untilAsserted( - () -> { - var spans = tracer.getSpans(); - assertThat(spans).as("Should have created spans").isNotEmpty(); - }); - } - - @Test - void shouldSetCorrectSpanKind() { - // When: We execute a service method - try { - searchService.search("test_collection", "*:*", null, null, null, null, null); - } catch (Exception _) { - // Ignore errors - } - - // Then: Spans should have appropriate span kinds - await() - .atMost(5, TimeUnit.SECONDS) - .untilAsserted( - () -> { - var spans = tracer.getSpans(); - assertThat(spans).as("Should have created spans").isNotEmpty(); - }); - } - - @Test - void shouldIncludeServiceNameInResource() { - // When: We execute any operation - try { - searchService.search("test_collection", "*:*", null, null, null, null, null); - } catch (Exception _) { - // Ignore errors - } - - // Then: Spans should be created (service name is in resource attributes in - // OpenTelemetry) - await() - .atMost(5, TimeUnit.SECONDS) - .untilAsserted( - () -> { - var spans = tracer.getSpans(); - assertThat(spans).as("Should have created spans").isNotEmpty(); - }); - } - @Test void shouldRecordSpanDuration() { // When: We execute an operation diff --git a/src/test/java/org/apache/solr/mcp/server/schema/SchemaServiceTest.java b/src/test/java/org/apache/solr/mcp/server/schema/SchemaServiceTest.java index dd59e4b5..dba24d69 100644 --- a/src/test/java/org/apache/solr/mcp/server/schema/SchemaServiceTest.java +++ b/src/test/java/org/apache/solr/mcp/server/schema/SchemaServiceTest.java @@ -158,12 +158,9 @@ void testConstructor() { @Test void addFields_blankCollection_throws() { - assertThrows( - IllegalArgumentException.class, - () -> schemaService.addFields("", List.of(Map.of("name", "x", "type", "string")))); - assertThrows( - IllegalArgumentException.class, - () -> schemaService.addFields(" ", List.of(Map.of("name", "x", "type", "string")))); + List> fields = List.of(Map.of("name", "x", "type", "string")); + assertThrows(IllegalArgumentException.class, () -> schemaService.addFields("", fields)); + assertThrows(IllegalArgumentException.class, () -> schemaService.addFields(" ", fields)); } @Test @@ -205,23 +202,15 @@ void addFields_solrThrows_propagates() throws Exception { when(solrClient.request(any(SolrRequest.class), eq("col"))) .thenThrow(new SolrServerException("simulated")); - assertThrows( - SolrServerException.class, - () -> schemaService.addFields("col", List.of(Map.of("name", "x", "type", "string")))); + List> fields = List.of(Map.of("name", "x", "type", "string")); + assertThrows(SolrServerException.class, () -> schemaService.addFields("col", fields)); } @Test void addFieldTypes_blankCollection_throws() { - assertThrows( - IllegalArgumentException.class, - () -> - schemaService.addFieldTypes( - "", List.of(Map.of("name", "x", "class", "solr.StrField")))); - assertThrows( - IllegalArgumentException.class, - () -> - schemaService.addFieldTypes( - " ", List.of(Map.of("name", "x", "class", "solr.StrField")))); + List> types = List.of(Map.of("name", "x", "class", "solr.StrField")); + assertThrows(IllegalArgumentException.class, () -> schemaService.addFieldTypes("", types)); + assertThrows(IllegalArgumentException.class, () -> schemaService.addFieldTypes(" ", types)); } @Test diff --git a/src/test/java/org/apache/solr/mcp/server/search/SearchServiceIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/search/SearchServiceIntegrationTest.java index 1adb6de3..862a2682 100644 --- a/src/test/java/org/apache/solr/mcp/server/search/SearchServiceIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/search/SearchServiceIntegrationTest.java @@ -261,18 +261,13 @@ void searchWithUndefinedFieldInQueryReturnsGetSchemaHint() { @Test void searchWithUndefinedFieldInFilterQueryReturnsGetSchemaHint() { + List filterQueries = List.of("definitely_not_a_field:value"); IllegalArgumentException e = assertThrows( IllegalArgumentException.class, () -> searchService.search( - COLLECTION_NAME, - "*:*", - List.of("definitely_not_a_field:value"), - null, - null, - null, - null)); + COLLECTION_NAME, "*:*", filterQueries, null, null, null, null)); assertTrue( e.getMessage().contains(SearchService.GET_SCHEMA_HINT_FORMAT.formatted(COLLECTION_NAME)), () -> "expected get-schema hint, got: " + e.getMessage()); @@ -282,18 +277,12 @@ void searchWithUndefinedFieldInFilterQueryReturnsGetSchemaHint() { /** Faceting words it differently: {@code undefined field: "name"}. */ @Test void searchWithUndefinedFacetFieldReturnsGetSchemaHint() { + List facetFields = List.of("definitely_not_a_field"); IllegalArgumentException e = assertThrows( IllegalArgumentException.class, () -> - searchService.search( - COLLECTION_NAME, - "*:*", - null, - List.of("definitely_not_a_field"), - null, - null, - null)); + searchService.search(COLLECTION_NAME, "*:*", null, facetFields, null, null, null)); assertTrue( e.getMessage().contains(SearchService.GET_SCHEMA_HINT_FORMAT.formatted(COLLECTION_NAME)), () -> "expected get-schema hint, got: " + e.getMessage()); diff --git a/src/test/java/org/apache/solr/mcp/server/search/SearchServiceTest.java b/src/test/java/org/apache/solr/mcp/server/search/SearchServiceTest.java index 53124cf0..e5c697bd 100644 --- a/src/test/java/org/apache/solr/mcp/server/search/SearchServiceTest.java +++ b/src/test/java/org/apache/solr/mcp/server/search/SearchServiceTest.java @@ -136,18 +136,11 @@ void search_WithMixedBlankAndValidOptions_ShouldPreserveValidValues() throws Exc @Test void search_WithInvalidNonblankSortOrder_ShouldStillFail() { SearchService localService = new SearchService(mock(SolrClient.class)); + List sort = List.of(new SortClause("price", "descending")); IllegalArgumentException e = assertThrows( IllegalArgumentException.class, - () -> - localService.search( - "test_collection", - null, - null, - null, - List.of(new SortClause("price", "descending")), - null, - null)); + () -> localService.search("test_collection", null, null, null, sort, null, null)); assertTrue(e.getMessage().contains("'asc' or 'desc'")); } @@ -198,12 +191,12 @@ void search_FacetConversionFailure_ShouldNotReturnSuccessfulSearch() throws Exce when(mockResponse.getFacetFields()).thenThrow(failure); when(mockClient.query(eq("test_collection"), any(SolrQuery.class))).thenReturn(mockResponse); SearchService localService = new SearchService(mockClient); + List facetFields = List.of("platform"); IllegalStateException e = assertThrows( IllegalStateException.class, () -> - localService.search( - "test_collection", "*:*", null, List.of("platform"), null, null, null)); + localService.search("test_collection", "*:*", null, facetFields, null, null, null)); assertFalse(e.getMessage().contains("ClassCastException")); assertFalse(e.getMessage().contains("NamedList")); assertTrue(e.getMessage().contains("facetFields")); diff --git a/src/test/java/org/apache/solr/mcp/server/search/SortClauseTest.java b/src/test/java/org/apache/solr/mcp/server/search/SortClauseTest.java index f3a64a5b..8e3aa353 100644 --- a/src/test/java/org/apache/solr/mcp/server/search/SortClauseTest.java +++ b/src/test/java/org/apache/solr/mcp/server/search/SortClauseTest.java @@ -47,17 +47,16 @@ void defaultsMissingOrderToAscending() { @Test void rejectsInvalidOrderNamingTheValue() { + SortClause clause = new SortClause("price", "descending"); IllegalArgumentException e = - assertThrows( - IllegalArgumentException.class, - () -> new SortClause("price", "descending").toSolrSortClause()); + assertThrows(IllegalArgumentException.class, clause::toSolrSortClause); assertTrue(e.getMessage().contains("descending")); assertTrue(e.getMessage().contains("price")); } @Test void rejectsMissingField() { - assertThrows( - IllegalArgumentException.class, () -> new SortClause(" ", "asc").toSolrSortClause()); + SortClause clause = new SortClause(" ", "asc"); + assertThrows(IllegalArgumentException.class, clause::toSolrSortClause); } }