diff --git a/CHANGELOG.md b/CHANGELOG.md index 8278411..3d73382 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file. ## Unreleased +- **Fix: Test Suite No Longer Writes Into the Developer's LuCLI Home:** `ModuleArgBindingTest` and `McpCommandTest` installed their fixture modules into the real `~/.lucli/modules` and spawned `dev-lucli.sh` without passing a home, so `mvn test` compiled the fixtures into the real `~/.lucli/lucee-server`, and with an isolated test home (`-Duser.home=`) 17 tests failed because the child couldn't find the fixtures. Each class now uses a temporary LuCLI home (`@TempDir`), installs its fixture there and passes it to the child as `LUCLI_HOME`. Issue: #25 (upstream: cybersonic/LuCLI#144). Adds `ModuleArgBindingTest.childRunsInTheTemporaryLucliHome`. + - **Fix: Hints Name the Binary You Ran:** User-facing hints such as "Use 'lucli server new' …", "Run 'lucli secrets init' …" or "run 'lucli modules list' …" always said `lucli`, also under an aliased binary (e.g. `wheels`). They now use `LuCLI.cliName()`, the active profile's name. The bundled `lang` module's "Use 'lucli lang help'" hint does the same. picocli `@Command` help text (annotation constants) is unchanged. Adds `CliNameTest`. - **Fix: The Bundled `lang` Module Compiles:** eight help strings in `modules/lang/Module.cfc` contained an unescaped `#` (`# e.g., …`), which CFML reads as an expression delimiter, so the module failed to compile ("Invalid identifier") and `lang` was unusable. They are escaped as `##`. Upstream: cybersonic/LuCLI#146 (PR cybersonic/LuCLI#147). diff --git a/FORK.md b/FORK.md index 9a5fe17..5874655 100644 --- a/FORK.md +++ b/FORK.md @@ -89,6 +89,7 @@ No release is tagged for this queue yet; tagging needs the Wheels release owner' | 12 | wheels-dev/LuCLI [#24](https://github.com/wheels-dev/LuCLI/pull/24) (fork-only: follows row 2 / upstream PR [cybersonic/LuCLI#124](https://github.com/cybersonic/LuCLI/pull/124)) | Root options before a reserved root subcommand under an aliased binary run that subcommand instead of the module (`wheels --timing cfml 'x'`) | | 13 | wheels-dev/LuCLI [#26](https://github.com/wheels-dev/LuCLI/pull/26) (issue: upstream [cybersonic/LuCLI#137](https://github.com/cybersonic/LuCLI/issues/137)) | `lucli run .cfm` runs the built-in variables setup inside `` instead of printing it | | 14 | wheels-dev/LuCLI [#27](https://github.com/wheels-dev/LuCLI/pull/27) (fork-only polish, no upstream issue) | User-facing hints name the active binary (`wheels server new`, not `lucli server new`) via `LuCLI.cliName()` | +| 15 | wheels-dev/LuCLI [#28](https://github.com/wheels-dev/LuCLI/pull/28) (issue: fork [#25](https://github.com/wheels-dev/LuCLI/issues/25), upstream [cybersonic/LuCLI#144](https://github.com/cybersonic/LuCLI/issues/144)) | Test suite isolated from the developer's LuCLI home: `ModuleArgBindingTest` / `McpCommandTest` use a temporary `LUCLI_HOME` passed to the `dev-lucli.sh` child | Upstream (row 8): issue [cybersonic/LuCLI#133](https://github.com/cybersonic/LuCLI/issues/133), PR [cybersonic/LuCLI#134](https://github.com/cybersonic/LuCLI/pull/134) (a `cherry-pick -x` of diff --git a/src/test/java/org/lucee/lucli/cli/commands/McpCommandTest.java b/src/test/java/org/lucee/lucli/cli/commands/McpCommandTest.java index f336258..cdf1beb 100644 --- a/src/test/java/org/lucee/lucli/cli/commands/McpCommandTest.java +++ b/src/test/java/org/lucee/lucli/cli/commands/McpCommandTest.java @@ -23,7 +23,7 @@ import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.lucee.lucli.paths.LucliPaths; +import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -34,7 +34,7 @@ * Integration tests for {@link McpCommand}. * * Copies a small fixture module (src/test/resources/mcp-fixture-module) into - * the real LuCLI modules directory, spawns `dev-lucli.sh mcp mcpfixture` as a + * a temporary LuCLI home, spawns `dev-lucli.sh mcp mcpfixture` with that home as a * subprocess, and asserts on the JSON-RPC responses emitted on stdout. * * The fixture is removed in {@link #tearDown()}. @@ -51,6 +51,12 @@ class McpCommandTest { private static Path fixtureInstalledPath; private static String lucliBin; + // The LuCLI home for this class: the fixture is installed here and the + // dev-lucli.sh child gets it as LUCLI_HOME, so the suite never touches the + // developer's own home and passes in a clean one. + @TempDir + static Path lucliHome; + @BeforeAll static void setUp() throws Exception { // Subprocess-based tests need bash + dev-lucli.sh. Windows runners @@ -65,7 +71,7 @@ static void setUp() throws Exception { assertTrue(Files.isDirectory(fixtureSrc), "fixture source not found at " + fixtureSrc); - Path modulesDir = LucliPaths.resolve().modulesDir(); + Path modulesDir = lucliHome.resolve("modules"); Files.createDirectories(modulesDir); fixtureInstalledPath = modulesDir.resolve(FIXTURE_MODULE_NAME); @@ -312,7 +318,7 @@ void terminalInvocationNeverCarriesTheMcpMarker() throws Exception { for (String[] invocation : invocations) { List cmd = new ArrayList<>(List.of("/bin/bash", lucliBin, FIXTURE_MODULE_NAME)); cmd.addAll(List.of(invocation)); - ProcessBuilder pb = new ProcessBuilder(cmd); + ProcessBuilder pb = lucliProcess(cmd); pb.redirectErrorStream(true); Process proc = pb.start(); String output = new String(proc.getInputStream().readAllBytes(), StandardCharsets.UTF_8); @@ -324,7 +330,7 @@ void terminalInvocationNeverCarriesTheMcpMarker() throws Exception { @Test void declaredMarkerParameterIsNeverBoundFromATerminalPositional() throws Exception { // The fixture declares `__LuCliMcpCall` (a case variant): a positional must not fill it. - ProcessBuilder pb = new ProcessBuilder("/bin/bash", lucliBin, FIXTURE_MODULE_NAME, "declaredmarker", "yes"); + ProcessBuilder pb = lucliProcess("/bin/bash", lucliBin, FIXTURE_MODULE_NAME, "declaredmarker", "yes"); pb.redirectErrorStream(true); Process proc = pb.start(); String output = new String(proc.getInputStream().readAllBytes(), StandardCharsets.UTF_8); @@ -345,7 +351,7 @@ void declaredMarkerParameterIsNeverBoundFromATerminalPositional() throws Excepti // Send the given JSON-RPC lines to `dev-lucli.sh mcp mcpfixture` as a // subprocess. Returns each parsed response as a JsonNode. private List runMcpSession(List requests) throws Exception { - ProcessBuilder pb = new ProcessBuilder( + ProcessBuilder pb = lucliProcess( "/bin/bash", lucliBin, "mcp", FIXTURE_MODULE_NAME); pb.redirectErrorStream(false); Process proc = pb.start(); @@ -405,4 +411,15 @@ private static void deleteRecursively(Path path) throws IOException { } } + + // Every child gets this class's LuCLI home. + private static ProcessBuilder lucliProcess(String... command) { + return lucliProcess(List.of(command)); + } + + private static ProcessBuilder lucliProcess(List command) { + ProcessBuilder pb = new ProcessBuilder(command); + pb.environment().put("LUCLI_HOME", lucliHome.toString()); + return pb; + } } diff --git a/src/test/java/org/lucee/lucli/cli/commands/ModuleArgBindingTest.java b/src/test/java/org/lucee/lucli/cli/commands/ModuleArgBindingTest.java index f01c072..4ca99d9 100644 --- a/src/test/java/org/lucee/lucli/cli/commands/ModuleArgBindingTest.java +++ b/src/test/java/org/lucee/lucli/cli/commands/ModuleArgBindingTest.java @@ -19,7 +19,7 @@ import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.lucee.lucli.paths.LucliPaths; +import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -39,6 +39,12 @@ class ModuleArgBindingTest { private static Path fixtureInstalledPath; private static String lucliBin; + // The LuCLI home for this class: the fixture is installed here and the + // dev-lucli.sh child gets it as LUCLI_HOME, so the suite never touches the + // developer's own home and passes in a clean one. + @TempDir + static Path lucliHome; + @BeforeAll static void setUp() throws Exception { Assumptions.assumeFalse( @@ -50,7 +56,7 @@ static void setUp() throws Exception { assertTrue(Files.isDirectory(fixtureSrc), "fixture source not found at " + fixtureSrc); - Path modulesDir = LucliPaths.resolve().modulesDir(); + Path modulesDir = lucliHome.resolve("modules"); Files.createDirectories(modulesDir); fixtureInstalledPath = modulesDir.resolve(FIXTURE_MODULE_NAME); @@ -141,13 +147,21 @@ void clusteredRootFlagsAndAttachedValuesPreserveModuleTimeout() throws Exception } } + @Test + void childRunsInTheTemporaryLucliHome() throws Exception { + String output = runCli("typedbind", "report", "hello"); + assertTrue(Files.isDirectory(lucliHome.resolve("lucee-server")), + "the child must create its Lucee context under LUCLI_HOME, not the developer's home"); + assertTrue(output.contains("first=hello"), output); + } + private String runCli(String... args) throws IOException, InterruptedException { List cmd = new ArrayList<>(); cmd.add("/bin/bash"); cmd.add(lucliBin); for (String a : args) cmd.add(a); - ProcessBuilder pb = new ProcessBuilder(cmd); + ProcessBuilder pb = lucliProcess(cmd); pb.redirectErrorStream(true); Process proc = pb.start(); @@ -179,4 +193,11 @@ private static void deleteRecursively(Path path) throws IOException { .forEach(p -> { try { Files.delete(p); } catch (IOException e) { throw new RuntimeException(e); } }); } } + + // Every child gets this class's LuCLI home. + private static ProcessBuilder lucliProcess(List command) { + ProcessBuilder pb = new ProcessBuilder(command); + pb.environment().put("LUCLI_HOME", lucliHome.toString()); + return pb; + } }