From 6ea345eaf2be05d5776242de02a3656cee6e5066 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 23 Apr 2026 12:45:28 +0000 Subject: [PATCH 1/2] refactor(config): centralize CLI startup overrides in a config-package helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse the four production call sites that mutate the CodeIqConfig Spring singleton (ServeCommand, EnrichCommand, CliOutput, Analyzer) through a single package-adjacent helper. This pins the "mutation happens once at CLI startup" contract in one place and sets up a follow-up commit to tighten the bean's setter visibility to package-private. - New CliStartupConfigOverrides with applyServeOverrides / applyCacheDir / applyServiceName. Null/blank inputs are no-ops — never overwrite an in-code default with an absent value. - Analyzer.runSmartWithCache now routes service-name propagation through the helper (same semantics, same guard condition). - Six unit tests verify each helper mutates only the intended fields on a freshly-adapted CodeIqConfig and that null/blank inputs are no-ops. --- .../randomcodespace/iq/analyzer/Analyzer.java | 3 +- .../randomcodespace/iq/cli/CliOutput.java | 6 +- .../randomcodespace/iq/cli/EnrichCommand.java | 6 +- .../randomcodespace/iq/cli/ServeCommand.java | 6 +- .../iq/config/CliStartupConfigOverrides.java | 71 ++++++++++++ .../config/CliStartupConfigOverridesTest.java | 104 ++++++++++++++++++ 6 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 src/main/java/io/github/randomcodespace/iq/config/CliStartupConfigOverrides.java create mode 100644 src/test/java/io/github/randomcodespace/iq/config/CliStartupConfigOverridesTest.java diff --git a/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java b/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java index c4f19427..49a4705e 100644 --- a/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java +++ b/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java @@ -5,6 +5,7 @@ import io.github.randomcodespace.iq.cache.AnalysisCache; import io.github.randomcodespace.iq.cache.FileHasher; import io.github.randomcodespace.iq.cli.VersionCommand; +import io.github.randomcodespace.iq.config.CliStartupConfigOverrides; import io.github.randomcodespace.iq.config.CodeIqConfig; import io.github.randomcodespace.iq.config.unified.CodeIqUnifiedConfig; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; @@ -814,7 +815,7 @@ private AnalysisResult runSmartWithCache(Path root, Integer parallelism, int bat report.accept("Service: " + infraRegistry.getServiceName()); // Propagate to config if not already set if (config.getServiceName() == null || config.getServiceName().isBlank()) { - config.setServiceName(infraRegistry.getServiceName()); + CliStartupConfigOverrides.applyServiceName(config, infraRegistry.getServiceName()); } } diff --git a/src/main/java/io/github/randomcodespace/iq/cli/CliOutput.java b/src/main/java/io/github/randomcodespace/iq/cli/CliOutput.java index ba852c68..c092f59f 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/CliOutput.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/CliOutput.java @@ -95,11 +95,13 @@ static void configureFromOptions(io.github.randomcodespace.iq.config.CodeIqConfi java.nio.file.Path graphDir, String serviceName) { if (graphDir != null) { java.nio.file.Path sharedDir = graphDir.toAbsolutePath().normalize(); - config.setCacheDir(sharedDir.toString()); + io.github.randomcodespace.iq.config.CliStartupConfigOverrides.applyCacheDir( + config, sharedDir.toString()); info(" Graph dir: " + sharedDir + " (shared multi-repo)"); } if (serviceName != null && !serviceName.isBlank()) { - config.setServiceName(serviceName); + io.github.randomcodespace.iq.config.CliStartupConfigOverrides.applyServiceName( + config, serviceName); info(" Service name: " + serviceName); } } diff --git a/src/main/java/io/github/randomcodespace/iq/cli/EnrichCommand.java b/src/main/java/io/github/randomcodespace/iq/cli/EnrichCommand.java index 64e4be03..f3700340 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/EnrichCommand.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/EnrichCommand.java @@ -4,6 +4,7 @@ import io.github.randomcodespace.iq.analyzer.LayerClassifier; import io.github.randomcodespace.iq.analyzer.linker.Linker; import io.github.randomcodespace.iq.cache.AnalysisCache; +import io.github.randomcodespace.iq.config.CliStartupConfigOverrides; import io.github.randomcodespace.iq.config.CodeIqConfig; import io.github.randomcodespace.iq.intelligence.RepositoryIdentity; import io.github.randomcodespace.iq.intelligence.extractor.LanguageEnricher; @@ -91,8 +92,9 @@ public Integer call() { // If --graph is set, override cache directory to shared location if (graphDir != null) { - config.setCacheDir(graphDir.toAbsolutePath().normalize().toString()); - CliOutput.info(" Graph dir: " + graphDir.toAbsolutePath().normalize() + " (shared multi-repo)"); + Path sharedDir = graphDir.toAbsolutePath().normalize(); + CliStartupConfigOverrides.applyCacheDir(config, sharedDir.toString()); + CliOutput.info(" Graph dir: " + sharedDir + " (shared multi-repo)"); } // 1. Open H2 file diff --git a/src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java b/src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java index e5276473..f0ce1751 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java @@ -1,5 +1,6 @@ package io.github.randomcodespace.iq.cli; +import io.github.randomcodespace.iq.config.CliStartupConfigOverrides; import io.github.randomcodespace.iq.config.CodeIqConfig; import io.github.randomcodespace.iq.config.GraphBootstrapper; import io.github.randomcodespace.iq.graph.GraphStore; @@ -71,10 +72,7 @@ public class ServeCommand implements Callable { @Override public Integer call() { Path root = path.toAbsolutePath().normalize(); - config.setRootPath(root.toString()); - if (readOnly) { - config.setReadOnly(true); - } + CliStartupConfigOverrides.applyServeOverrides(config, root, readOnly); NumberFormat nf = NumberFormat.getIntegerInstance(Locale.US); // Bootstrap Neo4j from the H2 analysis cache if Neo4j is empty. This is diff --git a/src/main/java/io/github/randomcodespace/iq/config/CliStartupConfigOverrides.java b/src/main/java/io/github/randomcodespace/iq/config/CliStartupConfigOverrides.java new file mode 100644 index 00000000..5d79c967 --- /dev/null +++ b/src/main/java/io/github/randomcodespace/iq/config/CliStartupConfigOverrides.java @@ -0,0 +1,71 @@ +package io.github.randomcodespace.iq.config; + +import java.nio.file.Path; + +/** + * Centralised, CLI-startup-only mutation of the {@link CodeIqConfig} Spring + * singleton. + * + *

Call contract: these helpers are invoked exactly once per JVM + * invocation, from a Picocli command's {@code call()} entry point, before + * any downstream consumer reads config state. Treat the config as frozen + * afterwards. + * + *

Do not invoke from request handlers, background workers, controllers, + * MCP tools, or any serving-layer code path. The {@link CodeIqConfig} bean is a + * Spring singleton shared across every consumer — mutating it at runtime is a + * correctness hazard and was the motivation for collapsing all existing call + * sites into this one package-private surface. + * + *

Visibility is package-private by design: only other classes inside + * {@code io.github.randomcodespace.iq.config} can reach {@link CodeIqConfig}'s + * package-private setters via this helper. CLI callers in + * {@code io.github.randomcodespace.iq.cli} and analyzer callers in + * {@code io.github.randomcodespace.iq.analyzer} route through the public + * {@code apply*} methods below. + */ +public final class CliStartupConfigOverrides { + + private CliStartupConfigOverrides() {} + + /** + * Apply the {@code serve} command's startup overrides to the config bean: + * absolute root path, and read-only mode when the {@code --read-only} flag + * was set. + * + * @param config the Spring-managed {@link CodeIqConfig} singleton + * @param root absolute, normalised root path (must not be {@code null}) + * @param readOnly whether the {@code --read-only} CLI flag was set + */ + public static void applyServeOverrides(CodeIqConfig config, Path root, boolean readOnly) { + if (config == null || root == null) { + return; + } + config.setRootPath(root.toString()); + if (readOnly) { + config.setReadOnly(true); + } + } + + /** + * Override the cache directory. No-op if {@code cacheDir} is {@code null} + * or blank — we never overwrite the in-code default with an absent value. + */ + public static void applyCacheDir(CodeIqConfig config, String cacheDir) { + if (config == null || cacheDir == null || cacheDir.isBlank()) { + return; + } + config.setCacheDir(cacheDir); + } + + /** + * Override the service-name tag used in multi-repo graph mode. No-op if + * {@code name} is {@code null} or blank. + */ + public static void applyServiceName(CodeIqConfig config, String name) { + if (config == null || name == null || name.isBlank()) { + return; + } + config.setServiceName(name); + } +} diff --git a/src/test/java/io/github/randomcodespace/iq/config/CliStartupConfigOverridesTest.java b/src/test/java/io/github/randomcodespace/iq/config/CliStartupConfigOverridesTest.java new file mode 100644 index 00000000..8729fd43 --- /dev/null +++ b/src/test/java/io/github/randomcodespace/iq/config/CliStartupConfigOverridesTest.java @@ -0,0 +1,104 @@ +package io.github.randomcodespace.iq.config; + +import io.github.randomcodespace.iq.config.unified.ConfigDefaults; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies {@link CliStartupConfigOverrides} mutates only the intended fields + * on a freshly-adapted {@link CodeIqConfig} and leaves every other field at + * the built-in default. Protects the CLI startup contract: one helper per + * override group, no collateral writes, null/blank inputs are no-ops. + */ +class CliStartupConfigOverridesTest { + + private CodeIqConfig freshConfig() { + return UnifiedConfigAdapter.toCodeIqConfig(ConfigDefaults.builtIn()); + } + + @Test + void applyServeOverrides_sets_rootPath_and_readOnly_only() { + CodeIqConfig cfg = freshConfig(); + String originalCacheDir = cfg.getCacheDir(); + int originalMaxDepth = cfg.getMaxDepth(); + int originalBatchSize = cfg.getBatchSize(); + String originalGraphPath = cfg.getGraph().getPath(); + + Path root = Paths.get("/tmp/some-repo").toAbsolutePath().normalize(); + CliStartupConfigOverrides.applyServeOverrides(cfg, root, true); + + assertEquals(root.toString(), cfg.getRootPath()); + assertTrue(cfg.isReadOnly()); + // no collateral mutation + assertEquals(originalCacheDir, cfg.getCacheDir()); + assertEquals(originalMaxDepth, cfg.getMaxDepth()); + assertEquals(originalBatchSize, cfg.getBatchSize()); + assertEquals(originalGraphPath, cfg.getGraph().getPath()); + } + + @Test + void applyServeOverrides_readOnly_false_leaves_flag_at_default() { + CodeIqConfig cfg = freshConfig(); + Path root = Paths.get("/tmp/other-repo").toAbsolutePath().normalize(); + CliStartupConfigOverrides.applyServeOverrides(cfg, root, false); + assertEquals(root.toString(), cfg.getRootPath()); + assertFalse(cfg.isReadOnly()); + } + + @Test + void applyCacheDir_sets_cacheDir_only() { + CodeIqConfig cfg = freshConfig(); + String originalRoot = cfg.getRootPath(); + String originalServiceName = cfg.getServiceName(); + boolean originalReadOnly = cfg.isReadOnly(); + + CliStartupConfigOverrides.applyCacheDir(cfg, "/shared/graph"); + + assertEquals("/shared/graph", cfg.getCacheDir()); + assertEquals(originalRoot, cfg.getRootPath()); + assertEquals(originalServiceName, cfg.getServiceName()); + assertEquals(originalReadOnly, cfg.isReadOnly()); + } + + @Test + void applyCacheDir_null_or_blank_is_noop() { + CodeIqConfig cfg = freshConfig(); + String before = cfg.getCacheDir(); + CliStartupConfigOverrides.applyCacheDir(cfg, null); + assertEquals(before, cfg.getCacheDir()); + CliStartupConfigOverrides.applyCacheDir(cfg, " "); + assertEquals(before, cfg.getCacheDir()); + } + + @Test + void applyServiceName_sets_serviceName_only() { + CodeIqConfig cfg = freshConfig(); + String originalRoot = cfg.getRootPath(); + String originalCacheDir = cfg.getCacheDir(); + int originalMaxDepth = cfg.getMaxDepth(); + + CliStartupConfigOverrides.applyServiceName(cfg, "payments-api"); + + assertEquals("payments-api", cfg.getServiceName()); + assertEquals(originalRoot, cfg.getRootPath()); + assertEquals(originalCacheDir, cfg.getCacheDir()); + assertEquals(originalMaxDepth, cfg.getMaxDepth()); + } + + @Test + void applyServiceName_null_or_blank_is_noop() { + CodeIqConfig cfg = freshConfig(); + CliStartupConfigOverrides.applyServiceName(cfg, null); + assertEquals(null, cfg.getServiceName()); // default is null + CliStartupConfigOverrides.applyServiceName(cfg, ""); + assertEquals(null, cfg.getServiceName()); + CliStartupConfigOverrides.applyServiceName(cfg, " "); + assertEquals(null, cfg.getServiceName()); + } +} From c032373c7941bcec5fb07cd65f8f3945c18b2958 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 23 Apr 2026 12:49:08 +0000 Subject: [PATCH 2/2] refactor(config): tighten CodeIqConfig setter visibility to package-private MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop `public` from every setter on CodeIqConfig and its Graph inner class. Production mutation is now restricted at compile time to: - UnifiedConfigAdapter.toCodeIqConfig (once, at Spring startup) - CliStartupConfigOverrides (once per JVM, at CLI entry) Both live in `io.github.randomcodespace.iq.config` and reach the package-private setters directly. Every other code path — controllers, MCP tools, background workers — loses the compile-time ability to mutate the Spring singleton. This is the mutation hazard cleanup #49 called for. Test migration: - Two in-package tests (CodeIqConfigTest, GraphBootstrapperTest) keep working unchanged. - 15 out-of-package test classes across `iq.api`, `iq.cli`, `iq.intelligence.*`, `iq.mcp`, `iq.query` are migrated to route setter calls through a new test-only helper CodeIqConfigTestSupport (lives in src/test/java/io/github/randomcodespace/iq/config/, so tests compile against the package-private setters). Fluent API keeps call sites readable: `CodeIqConfigTestSupport.override(config).rootPath(x).done();` The name makes the test-only intent unmistakable and the helper is not reachable from production code paths. - 51 call sites rewritten; semantics preserved verbatim. Full suite green: 3278 tests, 0 failures, 31 skipped (baseline unchanged). --- .../iq/config/CodeIqConfig.java | 40 ++++++++------ .../iq/api/GraphControllerTest.java | 17 +++--- .../iq/api/IntelligenceControllerTest.java | 3 +- .../api/TopologyControllerExtendedTest.java | 3 +- .../iq/api/TopologyEndpointTest.java | 7 +-- .../iq/cli/BundleCommandExtendedTest.java | 3 +- .../iq/cli/BundleCommandTest.java | 5 +- .../iq/cli/CacheCommandTest.java | 9 ++-- .../iq/cli/StatsCommandTest.java | 3 +- .../iq/cli/TopologyCommandExtendedTest.java | 17 +++--- .../iq/config/CodeIqConfigTestSupport.java | 53 +++++++++++++++++++ .../EvidencePackAssemblerExtendedTest.java | 11 ++-- .../evidence/EvidencePackAssemblerTest.java | 7 +-- .../lexical/LexicalQueryServiceTest.java | 3 +- .../iq/mcp/McpToolsExpandedTest.java | 3 +- .../randomcodespace/iq/mcp/McpToolsTest.java | 17 +++--- .../iq/query/QueryServiceTest.java | 9 ++-- 17 files changed, 142 insertions(+), 68 deletions(-) create mode 100644 src/test/java/io/github/randomcodespace/iq/config/CodeIqConfigTestSupport.java diff --git a/src/main/java/io/github/randomcodespace/iq/config/CodeIqConfig.java b/src/main/java/io/github/randomcodespace/iq/config/CodeIqConfig.java index efa3b342..4b05e240 100644 --- a/src/main/java/io/github/randomcodespace/iq/config/CodeIqConfig.java +++ b/src/main/java/io/github/randomcodespace/iq/config/CodeIqConfig.java @@ -7,13 +7,19 @@ * Task 11 moved bean production to {@link UnifiedConfigBeans#codeIqConfig}, which * adapts a {@link io.github.randomcodespace.iq.config.unified.CodeIqUnifiedConfig} * (single source of truth) via {@link UnifiedConfigAdapter#toCodeIqConfig}. The - * getter/setter surface is preserved unchanged so the ~100 call sites that still - * depend on this bean continue to work. + * getter surface is preserved unchanged so the ~100 call sites that read this + * bean continue to work. * *

This class is intentionally a plain POJO (no {@code @Configuration}, * no {@code @ConfigurationProperties}); Spring Boot no longer instantiates it - * from {@code application.yml}. Instantiable directly in tests via the public - * no-arg constructor and setters. + * from {@code application.yml}. + * + *

Setters are package-private. Only {@link UnifiedConfigAdapter} + * (at Spring startup) and {@link CliStartupConfigOverrides} (once per JVM at + * CLI entry) mutate instances of this class. External-package callers go + * through {@link CliStartupConfigOverrides}. External-package tests that need + * a populated instance construct one via + * {@link UnifiedConfigAdapter#toCodeIqConfig(io.github.randomcodespace.iq.config.unified.CodeIqUnifiedConfig)}. */ public class CodeIqConfig { @@ -48,7 +54,7 @@ public static class Graph { private String path = ".code-iq/graph/graph.db"; public String getPath() { return path; } - public void setPath(String path) { this.path = path; } + void setPath(String path) { this.path = path; } } /** Read-only mode for serving — no lock files, no writes. For read-only filesystems (AKS). */ @@ -57,13 +63,13 @@ public static class Graph { /** Service name tag for multi-repo graph mode. */ private String serviceName; - // --- Getters and Setters --- + // --- Getters (public) and Setters (package-private) --- public String getRootPath() { return rootPath; } - public void setRootPath(String rootPath) { + void setRootPath(String rootPath) { this.rootPath = rootPath; } @@ -71,7 +77,7 @@ public String getCacheDir() { return cacheDir; } - public void setCacheDir(String cacheDir) { + void setCacheDir(String cacheDir) { this.cacheDir = cacheDir; } @@ -79,7 +85,7 @@ public int getMaxDepth() { return maxDepth; } - public void setMaxDepth(int maxDepth) { + void setMaxDepth(int maxDepth) { this.maxDepth = maxDepth; } @@ -87,7 +93,7 @@ public int getMaxFiles() { return maxFiles; } - public void setMaxFiles(int maxFiles) { + void setMaxFiles(int maxFiles) { this.maxFiles = Math.max(1, maxFiles); } @@ -95,7 +101,7 @@ public int getMaxRadius() { return maxRadius; } - public void setMaxRadius(int maxRadius) { + void setMaxRadius(int maxRadius) { this.maxRadius = maxRadius; } @@ -103,7 +109,7 @@ public int getBatchSize() { return batchSize; } - public void setBatchSize(int batchSize) { + void setBatchSize(int batchSize) { this.batchSize = Math.max(1, batchSize); } @@ -111,7 +117,7 @@ public boolean isReadOnly() { return readOnly; } - public void setReadOnly(boolean readOnly) { + void setReadOnly(boolean readOnly) { this.readOnly = readOnly; } @@ -119,7 +125,7 @@ public String getServiceName() { return serviceName; } - public void setServiceName(String serviceName) { + void setServiceName(String serviceName) { this.serviceName = serviceName; } @@ -127,7 +133,7 @@ public Graph getGraph() { return graph; } - public void setGraph(Graph graph) { + void setGraph(Graph graph) { this.graph = graph; } @@ -135,7 +141,7 @@ public boolean isUiEnabled() { return uiEnabled; } - public void setUiEnabled(boolean uiEnabled) { + void setUiEnabled(boolean uiEnabled) { this.uiEnabled = uiEnabled; } @@ -143,7 +149,7 @@ public int getMaxSnippetLines() { return maxSnippetLines; } - public void setMaxSnippetLines(int maxSnippetLines) { + void setMaxSnippetLines(int maxSnippetLines) { this.maxSnippetLines = Math.max(1, maxSnippetLines); } } diff --git a/src/test/java/io/github/randomcodespace/iq/api/GraphControllerTest.java b/src/test/java/io/github/randomcodespace/iq/api/GraphControllerTest.java index 189a49c0..30257617 100644 --- a/src/test/java/io/github/randomcodespace/iq/api/GraphControllerTest.java +++ b/src/test/java/io/github/randomcodespace/iq/api/GraphControllerTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; /** * Tests for the REST API controller using standalone MockMvc (no Spring context needed). @@ -46,9 +47,9 @@ class GraphControllerTest { @BeforeEach void setUp() { config = new CodeIqConfig(); - config.setMaxDepth(10); - config.setMaxRadius(10); - config.setRootPath("."); + CodeIqConfigTestSupport.override(config).maxDepth(10).done(); + CodeIqConfigTestSupport.override(config).maxRadius(10).done(); + CodeIqConfigTestSupport.override(config).rootPath(".").done(); var controller = new GraphController(queryService, config); mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); } @@ -499,7 +500,7 @@ void searchGraphShouldReturnResults() throws Exception { @Test void readFileShouldReturnContent(@TempDir Path tempDir) throws Exception { Files.writeString(tempDir.resolve("hello.txt"), "Hello World", StandardCharsets.UTF_8); - config.setRootPath(tempDir.toAbsolutePath().toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempDir.toAbsolutePath().toString()).done(); var controller = new GraphController(queryService, config); var fileMvc = MockMvcBuilders.standaloneSetup(controller).build(); @@ -510,7 +511,7 @@ void readFileShouldReturnContent(@TempDir Path tempDir) throws Exception { @Test void readFileShouldReturn404ForMissing(@TempDir Path tempDir) throws Exception { - config.setRootPath(tempDir.toAbsolutePath().toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempDir.toAbsolutePath().toString()).done(); var controller = new GraphController(queryService, config); var fileMvc = MockMvcBuilders.standaloneSetup(controller).build(); @@ -520,7 +521,7 @@ void readFileShouldReturn404ForMissing(@TempDir Path tempDir) throws Exception { @Test void readFileShouldBlockPathTraversal(@TempDir Path tempDir) throws Exception { - config.setRootPath(tempDir.toAbsolutePath().toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempDir.toAbsolutePath().toString()).done(); var controller = new GraphController(queryService, config); var fileMvc = MockMvcBuilders.standaloneSetup(controller).build(); @@ -533,7 +534,7 @@ void readFileShouldBlockPathTraversal(@TempDir Path tempDir) throws Exception { void readFileShouldReturnLineRange(@TempDir Path tempDir) throws Exception { Files.writeString(tempDir.resolve("multi.txt"), "line1\nline2\nline3\nline4\nline5", StandardCharsets.UTF_8); - config.setRootPath(tempDir.toAbsolutePath().toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempDir.toAbsolutePath().toString()).done(); var controller = new GraphController(queryService, config); var fileMvc = MockMvcBuilders.standaloneSetup(controller).build(); @@ -548,7 +549,7 @@ void readFileShouldReturnLineRange(@TempDir Path tempDir) throws Exception { @Test void readFileShouldReturnFullContentWithoutLineParams(@TempDir Path tempDir) throws Exception { Files.writeString(tempDir.resolve("full.txt"), "aaa\nbbb\nccc", StandardCharsets.UTF_8); - config.setRootPath(tempDir.toAbsolutePath().toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempDir.toAbsolutePath().toString()).done(); var controller = new GraphController(queryService, config); var fileMvc = MockMvcBuilders.standaloneSetup(controller).build(); diff --git a/src/test/java/io/github/randomcodespace/iq/api/IntelligenceControllerTest.java b/src/test/java/io/github/randomcodespace/iq/api/IntelligenceControllerTest.java index 28329842..00ae8544 100644 --- a/src/test/java/io/github/randomcodespace/iq/api/IntelligenceControllerTest.java +++ b/src/test/java/io/github/randomcodespace/iq/api/IntelligenceControllerTest.java @@ -22,6 +22,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; class IntelligenceControllerTest { @@ -41,7 +42,7 @@ void setUp() { when(metadataProvider.current()).thenReturn(metadata); CodeIqConfig config = new CodeIqConfig(); - config.setRootPath(System.getProperty("java.io.tmpdir")); + CodeIqConfigTestSupport.override(config).rootPath(System.getProperty("java.io.tmpdir")).done(); IntelligenceController controller = new IntelligenceController(assembler, metadataProvider, config); mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); diff --git a/src/test/java/io/github/randomcodespace/iq/api/TopologyControllerExtendedTest.java b/src/test/java/io/github/randomcodespace/iq/api/TopologyControllerExtendedTest.java index 7e9e5fcd..b6807e1b 100644 --- a/src/test/java/io/github/randomcodespace/iq/api/TopologyControllerExtendedTest.java +++ b/src/test/java/io/github/randomcodespace/iq/api/TopologyControllerExtendedTest.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; /** * Extended tests for TopologyController that exercise the actual REST endpoints @@ -60,7 +61,7 @@ class TopologyControllerExtendedTest { void setUp() { var config = new CodeIqConfig(); // Use the temp dir as rootPath so H2 fallback finds no cache file - config.setRootPath(tempDir.toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempDir.toString()).done(); controller = new TopologyController(topologyService, graphStore, config); mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); } diff --git a/src/test/java/io/github/randomcodespace/iq/api/TopologyEndpointTest.java b/src/test/java/io/github/randomcodespace/iq/api/TopologyEndpointTest.java index d3af11d1..97527837 100644 --- a/src/test/java/io/github/randomcodespace/iq/api/TopologyEndpointTest.java +++ b/src/test/java/io/github/randomcodespace/iq/api/TopologyEndpointTest.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; /** * Tests for the /api/topology endpoint and related components. @@ -61,9 +62,9 @@ class TopologyEndpointTest { @BeforeEach void setUp() { config = new CodeIqConfig(); - config.setMaxDepth(10); - config.setMaxRadius(10); - config.setRootPath("."); + CodeIqConfigTestSupport.override(config).maxDepth(10).done(); + CodeIqConfigTestSupport.override(config).maxRadius(10).done(); + CodeIqConfigTestSupport.override(config).rootPath(".").done(); objectMapper = new ObjectMapper(); diff --git a/src/test/java/io/github/randomcodespace/iq/cli/BundleCommandExtendedTest.java b/src/test/java/io/github/randomcodespace/iq/cli/BundleCommandExtendedTest.java index 96d8ba26..5a47f8da 100644 --- a/src/test/java/io/github/randomcodespace/iq/cli/BundleCommandExtendedTest.java +++ b/src/test/java/io/github/randomcodespace/iq/cli/BundleCommandExtendedTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; /** * Extended tests for BundleCommand covering additional branches: @@ -249,7 +250,7 @@ void bundleWithH2CacheReportsStats(@TempDir Path tempDir) throws IOException { Files.createDirectories(cacheDir); var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); Path zipPath = tempDir.resolve("out.zip"); var cmd = new BundleCommand(config, java.util.Optional.empty(), java.util.Optional.empty()); var cmdLine = new picocli.CommandLine(cmd); diff --git a/src/test/java/io/github/randomcodespace/iq/cli/BundleCommandTest.java b/src/test/java/io/github/randomcodespace/iq/cli/BundleCommandTest.java index eb7bcd77..7bd7a50d 100644 --- a/src/test/java/io/github/randomcodespace/iq/cli/BundleCommandTest.java +++ b/src/test/java/io/github/randomcodespace/iq/cli/BundleCommandTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; @ExtendWith(MockitoExtension.class) class BundleCommandTest { @@ -70,7 +71,7 @@ void bundleCreatesZipWithCorrectStructure(@TempDir Path tempDir) throws IOExcept Files.writeString(tempDir.resolve("App.java"), "class App {}", StandardCharsets.UTF_8); var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); when(flowEngine.renderInteractive(anyString())).thenReturn("flow"); @@ -186,7 +187,7 @@ void bundleIncludesH2Cache(@TempDir Path tempDir) throws IOException { Files.writeString(cacheDir.resolve("analysis-cache.db"), "h2-data", StandardCharsets.UTF_8); var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); Path zipPath = tempDir.resolve("test-bundle.zip"); var cmd = new BundleCommand(config, (GraphStore) null, (FlowEngine) null); diff --git a/src/test/java/io/github/randomcodespace/iq/cli/CacheCommandTest.java b/src/test/java/io/github/randomcodespace/iq/cli/CacheCommandTest.java index adc65e8e..77856b92 100644 --- a/src/test/java/io/github/randomcodespace/iq/cli/CacheCommandTest.java +++ b/src/test/java/io/github/randomcodespace/iq/cli/CacheCommandTest.java @@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; class CacheCommandTest { @@ -36,7 +37,7 @@ void tearDown() { @Test void statsShowsNoCacheWhenMissing(@TempDir Path tempDir) { var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); var cmd = new CacheCommand.StatsSubcommand(config); var cmdLine = new picocli.CommandLine(cmd); @@ -56,7 +57,7 @@ void statsShowsCacheInfo(@TempDir Path tempDir) throws IOException { StandardCharsets.UTF_8); var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); var cmd = new CacheCommand.StatsSubcommand(config); var cmdLine = new picocli.CommandLine(cmd); @@ -76,7 +77,7 @@ void clearRemovesCache(@TempDir Path tempDir) throws IOException { StandardCharsets.UTF_8); var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); var cmd = new CacheCommand.ClearSubcommand(config); var cmdLine = new picocli.CommandLine(cmd); @@ -89,7 +90,7 @@ void clearRemovesCache(@TempDir Path tempDir) throws IOException { @Test void clearHandlesNoCacheGracefully(@TempDir Path tempDir) { var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); var cmd = new CacheCommand.ClearSubcommand(config); var cmdLine = new picocli.CommandLine(cmd); diff --git a/src/test/java/io/github/randomcodespace/iq/cli/StatsCommandTest.java b/src/test/java/io/github/randomcodespace/iq/cli/StatsCommandTest.java index d164e32a..94c743a4 100644 --- a/src/test/java/io/github/randomcodespace/iq/cli/StatsCommandTest.java +++ b/src/test/java/io/github/randomcodespace/iq/cli/StatsCommandTest.java @@ -22,6 +22,7 @@ import java.util.Map; import static org.junit.jupiter.api.Assertions.*; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; class StatsCommandTest { @@ -40,7 +41,7 @@ void setUp() { System.setErr(new PrintStream(captureErr, true, StandardCharsets.UTF_8)); statsService = new StatsService(); config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); } @AfterEach diff --git a/src/test/java/io/github/randomcodespace/iq/cli/TopologyCommandExtendedTest.java b/src/test/java/io/github/randomcodespace/iq/cli/TopologyCommandExtendedTest.java index 31591f4f..4f7d5cab 100644 --- a/src/test/java/io/github/randomcodespace/iq/cli/TopologyCommandExtendedTest.java +++ b/src/test/java/io/github/randomcodespace/iq/cli/TopologyCommandExtendedTest.java @@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; /** * Extended tests for TopologyCommand covering branches not hit by TopologyCommandTest: @@ -111,7 +112,7 @@ void prettyPrintsTopologyOverview(@TempDir Path tempDir) throws IOException { createRealCache(tempDir); var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); var svc = new TopologyService(); var cmd = new TopologyCommand(config, svc); var cmdLine = new picocli.CommandLine(cmd); @@ -131,7 +132,7 @@ void jsonFormatOutputsValidJson(@TempDir Path tempDir) throws IOException { createRealCache(tempDir); var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); var svc = new TopologyService(); var cmd = new TopologyCommand(config, svc); var cmdLine = new picocli.CommandLine(cmd); @@ -150,7 +151,7 @@ void serviceFlagOutputsServiceDetail(@TempDir Path tempDir) throws IOException { createRealCache(tempDir); var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); var svc = new TopologyService(); var cmd = new TopologyCommand(config, svc); var cmdLine = new picocli.CommandLine(cmd); @@ -168,7 +169,7 @@ void depsFlagOutputsDependencies(@TempDir Path tempDir) throws IOException { createRealCache(tempDir); var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); var svc = new TopologyService(); var cmd = new TopologyCommand(config, svc); var cmdLine = new picocli.CommandLine(cmd); @@ -184,7 +185,7 @@ void blastRadiusFlagOutputsBlastRadius(@TempDir Path tempDir) throws IOException createRealCache(tempDir); var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); var svc = new TopologyService(); var cmd = new TopologyCommand(config, svc); var cmdLine = new picocli.CommandLine(cmd); @@ -203,7 +204,7 @@ void serviceFlagWithJsonFormat(@TempDir Path tempDir) throws IOException { createRealCache(tempDir); var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); var svc = new TopologyService(); var cmd = new TopologyCommand(config, svc); var cmdLine = new picocli.CommandLine(cmd); @@ -221,7 +222,7 @@ void topologyServiceExceptionReturnsExitCode1(@TempDir Path tempDir) throws IOEx createRealCache(tempDir); var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); var svc = mock(TopologyService.class); when(svc.getTopology(anyList(), anyList())) .thenThrow(new RuntimeException("topology failed")); @@ -256,7 +257,7 @@ void prettyPrintWithConnections(@TempDir Path tempDir) throws IOException { } var config = new CodeIqConfig(); - config.setCacheDir(".code-iq/cache"); + CodeIqConfigTestSupport.override(config).cacheDir(".code-iq/cache").done(); var svc = new TopologyService(); var cmd = new TopologyCommand(config, svc); var cmdLine = new picocli.CommandLine(cmd); diff --git a/src/test/java/io/github/randomcodespace/iq/config/CodeIqConfigTestSupport.java b/src/test/java/io/github/randomcodespace/iq/config/CodeIqConfigTestSupport.java new file mode 100644 index 00000000..4d0a7cce --- /dev/null +++ b/src/test/java/io/github/randomcodespace/iq/config/CodeIqConfigTestSupport.java @@ -0,0 +1,53 @@ +package io.github.randomcodespace.iq.config; + +/** + * Test-support helper that wraps the package-private setters on + * {@link CodeIqConfig}. Intentionally a separate class so the name makes its + * purpose unmistakable: production code calls {@link UnifiedConfigAdapter} + * once at Spring startup and {@link CliStartupConfigOverrides} once per JVM + * at CLI entry; anything else goes through this class. + * + *

Use from tests that need to construct a {@link CodeIqConfig} with + * specific field values without taking on the ceremony of building a full + * {@link io.github.randomcodespace.iq.config.unified.CodeIqUnifiedConfig} + * record tree. Fluent so setups stay readable. + * + *

Do not use from production code. Any production caller of this + * class is a bug — production mutation must remain confined to the two + * entry points named above. + */ +public final class CodeIqConfigTestSupport { + + private final CodeIqConfig config; + + private CodeIqConfigTestSupport(CodeIqConfig config) { + this.config = config; + } + + /** Wrap an existing {@link CodeIqConfig} for fluent field overrides. */ + public static CodeIqConfigTestSupport override(CodeIqConfig config) { + return new CodeIqConfigTestSupport(config); + } + + public CodeIqConfigTestSupport rootPath(String v) { config.setRootPath(v); return this; } + public CodeIqConfigTestSupport cacheDir(String v) { config.setCacheDir(v); return this; } + public CodeIqConfigTestSupport maxDepth(int v) { config.setMaxDepth(v); return this; } + public CodeIqConfigTestSupport maxRadius(int v) { config.setMaxRadius(v); return this; } + public CodeIqConfigTestSupport maxFiles(int v) { config.setMaxFiles(v); return this; } + public CodeIqConfigTestSupport batchSize(int v) { config.setBatchSize(v); return this; } + public CodeIqConfigTestSupport readOnly(boolean v){ config.setReadOnly(v); return this; } + public CodeIqConfigTestSupport serviceName(String v) { config.setServiceName(v); return this; } + public CodeIqConfigTestSupport uiEnabled(boolean v){ config.setUiEnabled(v); return this; } + public CodeIqConfigTestSupport maxSnippetLines(int v) { config.setMaxSnippetLines(v); return this; } + + public CodeIqConfigTestSupport graph(CodeIqConfig.Graph g) { config.setGraph(g); return this; } + public CodeIqConfigTestSupport graphPath(String v) { + CodeIqConfig.Graph g = new CodeIqConfig.Graph(); + g.setPath(v); + config.setGraph(g); + return this; + } + + /** @return the wrapped config for chaining with downstream construction. */ + public CodeIqConfig done() { return config; } +} diff --git a/src/test/java/io/github/randomcodespace/iq/intelligence/evidence/EvidencePackAssemblerExtendedTest.java b/src/test/java/io/github/randomcodespace/iq/intelligence/evidence/EvidencePackAssemblerExtendedTest.java index 8356e673..3579c81e 100644 --- a/src/test/java/io/github/randomcodespace/iq/intelligence/evidence/EvidencePackAssemblerExtendedTest.java +++ b/src/test/java/io/github/randomcodespace/iq/intelligence/evidence/EvidencePackAssemblerExtendedTest.java @@ -28,6 +28,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; /** * Extended tests for EvidencePackAssembler covering uncovered branches: @@ -65,8 +66,8 @@ class EvidencePackAssemblerExtendedTest { void setUp() { queryPlanner = new QueryPlanner(); config = new CodeIqConfig(); - config.setRootPath(System.getProperty("java.io.tmpdir")); - config.setMaxSnippetLines(50); + CodeIqConfigTestSupport.override(config).rootPath(System.getProperty("java.io.tmpdir")).done(); + CodeIqConfigTestSupport.override(config).maxSnippetLines(50).done(); assembler = new EvidencePackAssembler(lexicalQueryService, snippetStore, queryPlanner, config, graphStore); metadata = new ArtifactMetadata( "https://github.com/example/repo", "abc123", Instant.now(), @@ -121,7 +122,7 @@ void symbolTakesPrecedenceOverFilePathLookup() { @Test void snippetsAreTruncatedToMaxSnippetLines() { - config.setMaxSnippetLines(3); + CodeIqConfigTestSupport.override(config).maxSnippetLines(3).done(); CodeNode node = new CodeNode("java:Big.java:class:Big", NodeKind.CLASS, "Big"); node.setFilePath("src/Big.java"); @@ -148,7 +149,7 @@ void snippetsAreTruncatedToMaxSnippetLines() { @Test void snippetsNotTruncatedWhenWithinMaxLines() { - config.setMaxSnippetLines(50); + CodeIqConfigTestSupport.override(config).maxSnippetLines(50).done(); CodeNode node = new CodeNode("java:Small.java:class:Small", NodeKind.CLASS, "Small"); node.setFilePath("src/Small.java"); @@ -290,7 +291,7 @@ void provenancePropertiesIncludeProvPrefixedProperties() { @Test void maxSnippetLinesCappedToConfiguredMax() { - config.setMaxSnippetLines(20); + CodeIqConfigTestSupport.override(config).maxSnippetLines(20).done(); CodeNode node = new CodeNode("java:X.java:class:X", NodeKind.CLASS, "X"); node.setFilePath("src/X.java"); diff --git a/src/test/java/io/github/randomcodespace/iq/intelligence/evidence/EvidencePackAssemblerTest.java b/src/test/java/io/github/randomcodespace/iq/intelligence/evidence/EvidencePackAssemblerTest.java index fa314acb..1484cd6c 100644 --- a/src/test/java/io/github/randomcodespace/iq/intelligence/evidence/EvidencePackAssemblerTest.java +++ b/src/test/java/io/github/randomcodespace/iq/intelligence/evidence/EvidencePackAssemblerTest.java @@ -24,6 +24,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; @ExtendWith(MockitoExtension.class) class EvidencePackAssemblerTest { @@ -44,8 +45,8 @@ class EvidencePackAssemblerTest { void setUp() { queryPlanner = new QueryPlanner(); config = new CodeIqConfig(); - config.setRootPath(System.getProperty("java.io.tmpdir")); - config.setMaxSnippetLines(50); + CodeIqConfigTestSupport.override(config).rootPath(System.getProperty("java.io.tmpdir")).done(); + CodeIqConfigTestSupport.override(config).maxSnippetLines(50).done(); assembler = new EvidencePackAssembler(lexicalQueryService, snippetStore, queryPlanner, config, graphStore); metadata = new ArtifactMetadata( "https://github.com/example/repo", "abc123", Instant.now(), @@ -120,7 +121,7 @@ void isDeterministic() { @Test void respectsMaxSnippetLinesFromConfig() { - config.setMaxSnippetLines(10); + CodeIqConfigTestSupport.override(config).maxSnippetLines(10).done(); assertThat(config.getMaxSnippetLines()).isEqualTo(10); } } diff --git a/src/test/java/io/github/randomcodespace/iq/intelligence/lexical/LexicalQueryServiceTest.java b/src/test/java/io/github/randomcodespace/iq/intelligence/lexical/LexicalQueryServiceTest.java index 199c6ef5..c2677b3f 100644 --- a/src/test/java/io/github/randomcodespace/iq/intelligence/lexical/LexicalQueryServiceTest.java +++ b/src/test/java/io/github/randomcodespace/iq/intelligence/lexical/LexicalQueryServiceTest.java @@ -15,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; /** * Unit tests for {@link LexicalQueryService}. @@ -35,7 +36,7 @@ void setUp() { graphStore = mock(GraphStore.class); snippetStore = mock(SnippetStore.class); config = new CodeIqConfig(); - config.setRootPath(tempRoot.toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempRoot.toString()).done(); service = new LexicalQueryService(graphStore, snippetStore, config); } diff --git a/src/test/java/io/github/randomcodespace/iq/mcp/McpToolsExpandedTest.java b/src/test/java/io/github/randomcodespace/iq/mcp/McpToolsExpandedTest.java index 5cc2bd22..620c9721 100644 --- a/src/test/java/io/github/randomcodespace/iq/mcp/McpToolsExpandedTest.java +++ b/src/test/java/io/github/randomcodespace/iq/mcp/McpToolsExpandedTest.java @@ -27,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; /** * Expanded McpTools tests targeting coverage gaps: @@ -61,7 +62,7 @@ class McpToolsExpandedTest { @BeforeEach void setUp() { config = new CodeIqConfig(); - config.setRootPath("."); + CodeIqConfigTestSupport.override(config).rootPath(".").done(); objectMapper = new ObjectMapper(); mcpTools = new McpTools( queryService, config, objectMapper, diff --git a/src/test/java/io/github/randomcodespace/iq/mcp/McpToolsTest.java b/src/test/java/io/github/randomcodespace/iq/mcp/McpToolsTest.java index 94430244..069b7316 100644 --- a/src/test/java/io/github/randomcodespace/iq/mcp/McpToolsTest.java +++ b/src/test/java/io/github/randomcodespace/iq/mcp/McpToolsTest.java @@ -33,6 +33,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; @ExtendWith(MockitoExtension.class) class McpToolsTest { @@ -59,7 +60,7 @@ class McpToolsTest { @BeforeEach void setUp() { config = new CodeIqConfig(); - config.setRootPath("."); + CodeIqConfigTestSupport.override(config).rootPath(".").done(); objectMapper = new ObjectMapper(); mcpTools = new McpTools(queryService, config, objectMapper, java.util.Optional.ofNullable(flowEngine), graphDb, statsService, new io.github.randomcodespace.iq.query.TopologyService(), graphStore, java.util.Optional.empty(), java.util.Optional.empty()); } @@ -459,7 +460,7 @@ void searchGraphShouldUseCustomLimit() throws IOException { @Test void readFileShouldReadContent(@TempDir Path tempDir) throws IOException { - config.setRootPath(tempDir.toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempDir.toString()).done(); Path file = tempDir.resolve("test.txt"); Files.writeString(file, "Hello, World!"); @@ -470,7 +471,7 @@ void readFileShouldReadContent(@TempDir Path tempDir) throws IOException { @Test void readFileShouldRejectPathTraversal(@TempDir Path tempDir) throws IOException { - config.setRootPath(tempDir.toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempDir.toString()).done(); String result = mcpTools.readFile("../../etc/passwd", null, null); Map parsed = parseJson(result); @@ -480,7 +481,7 @@ void readFileShouldRejectPathTraversal(@TempDir Path tempDir) throws IOException @Test void readFileShouldHandleMissingFile(@TempDir Path tempDir) throws IOException { - config.setRootPath(tempDir.toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempDir.toString()).done(); String result = mcpTools.readFile("nonexistent.txt", null, null); Map parsed = parseJson(result); @@ -491,7 +492,7 @@ void readFileShouldHandleMissingFile(@TempDir Path tempDir) throws IOException { @Test void readFileShouldReturnLineRange(@TempDir Path tempDir) throws IOException { - config.setRootPath(tempDir.toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempDir.toString()).done(); Path file = tempDir.resolve("lines.txt"); Files.writeString(file, "line1\nline2\nline3\nline4\nline5"); @@ -502,7 +503,7 @@ void readFileShouldReturnLineRange(@TempDir Path tempDir) throws IOException { @Test void readFileShouldReturnFromStartLineToEnd(@TempDir Path tempDir) throws IOException { - config.setRootPath(tempDir.toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempDir.toString()).done(); Path file = tempDir.resolve("lines.txt"); Files.writeString(file, "line1\nline2\nline3\nline4\nline5"); @@ -513,7 +514,7 @@ void readFileShouldReturnFromStartLineToEnd(@TempDir Path tempDir) throws IOExce @Test void readFileShouldReturnFromStartToEndLine(@TempDir Path tempDir) throws IOException { - config.setRootPath(tempDir.toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempDir.toString()).done(); Path file = tempDir.resolve("lines.txt"); Files.writeString(file, "line1\nline2\nline3\nline4\nline5"); @@ -524,7 +525,7 @@ void readFileShouldReturnFromStartToEndLine(@TempDir Path tempDir) throws IOExce @Test void readFileShouldClampOutOfBoundsLineRange(@TempDir Path tempDir) throws IOException { - config.setRootPath(tempDir.toString()); + CodeIqConfigTestSupport.override(config).rootPath(tempDir.toString()).done(); Path file = tempDir.resolve("lines.txt"); Files.writeString(file, "line1\nline2\nline3"); diff --git a/src/test/java/io/github/randomcodespace/iq/query/QueryServiceTest.java b/src/test/java/io/github/randomcodespace/iq/query/QueryServiceTest.java index 0ebe054d..b68aa9fb 100644 --- a/src/test/java/io/github/randomcodespace/iq/query/QueryServiceTest.java +++ b/src/test/java/io/github/randomcodespace/iq/query/QueryServiceTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport; @ExtendWith(MockitoExtension.class) class QueryServiceTest { @@ -34,8 +35,8 @@ class QueryServiceTest { @BeforeEach void setUp() { config = new CodeIqConfig(); - config.setMaxDepth(10); - config.setMaxRadius(10); + CodeIqConfigTestSupport.override(config).maxDepth(10).done(); + CodeIqConfigTestSupport.override(config).maxRadius(10).done(); service = new QueryService(graphStore, config); } @@ -290,7 +291,7 @@ void findCyclesShouldReturnCycles() { @Test void traceImpactShouldCapDepth() { - config.setMaxDepth(5); + CodeIqConfigTestSupport.override(config).maxDepth(5).done(); var impacted = makeNode("n2", NodeKind.CLASS, "Service"); when(graphStore.traceImpact("n1", 5)).thenReturn(List.of(impacted)); @@ -304,7 +305,7 @@ void traceImpactShouldCapDepth() { @Test void egoGraphShouldCapRadius() { - config.setMaxRadius(5); + CodeIqConfigTestSupport.override(config).maxRadius(5).done(); when(graphStore.findEgoGraph("center", 5)).thenReturn(new ArrayList<>()); var centerNode = makeNode("center", NodeKind.MODULE, "app"); when(graphStore.findById("center")).thenReturn(Optional.of(centerNode));