From 000eea85bc9d5b879169a0831f661d56156ee0b8 Mon Sep 17 00:00:00 2001 From: Loong Wan Date: Sun, 20 Sep 2026 18:52:50 +0800 Subject: [PATCH 1/3] test(cli): expose probe timeout regression --- .../codex/cli/CodexCliExecutorTest.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/test/java/io/github/easy4j/codex/cli/CodexCliExecutorTest.java b/src/test/java/io/github/easy4j/codex/cli/CodexCliExecutorTest.java index 51432e3..387914e 100644 --- a/src/test/java/io/github/easy4j/codex/cli/CodexCliExecutorTest.java +++ b/src/test/java/io/github/easy4j/codex/cli/CodexCliExecutorTest.java @@ -18,6 +18,12 @@ import io.github.easy4j.codex.CodexClientConfig; import org.junit.jupiter.api.Test; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -158,6 +164,37 @@ void shouldReportFailureFromProbeWhenExecutableMissing() { assertFalse(executor.probe()); } + @Test + void shouldUseProbeTimeoutWithoutChangingNormalCommandTimeout() throws Exception { + Path script = Files.createTempFile("slow-codex-", ".sh"); + Files.write(script, Arrays.asList( + "#!/bin/sh", + "sleep 2", + "echo codex-test" + ), StandardCharsets.UTF_8); + assertTrue(script.toFile().setExecutable(true)); + + CodexClientConfig config = configFor(script.toAbsolutePath().toString()); + config.setLocalProbeTimeoutSeconds(1); + config.setLocalTimeoutSeconds(5); + CodexCliExecutor executor = new CodexCliExecutor(config); + + long probeStarted = System.nanoTime(); + assertFalse(executor.probe()); + long probeElapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - probeStarted); + assertTrue(probeElapsedMs < 3_500, + "probe must use localProbeTimeoutSeconds instead of localTimeoutSeconds"); + + long executeStarted = System.nanoTime(); + CodexCliResult normal = executor.execute("--version"); + long executeElapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - executeStarted); + assertTrue(normal.isSuccess(), "normal command must still use localTimeoutSeconds"); + assertTrue(executeElapsedMs >= 1_500, + "normal command should be allowed to outlive the probe timeout"); + + Files.deleteIfExists(script); + } + @Test void shouldTimeoutOnHangingProcess() { // Use a short timeout and a command that sleeps for a long time. From 41a2fe32f3d1ea50a03950cb481d931e59bc6908 Mon Sep 17 00:00:00 2001 From: Loong Wan Date: Sun, 20 Sep 2026 18:54:46 +0800 Subject: [PATCH 2/3] fix(cli): honor probe timeout independently --- .../github/easy4j/codex/cli/CodexCliExecutor.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/github/easy4j/codex/cli/CodexCliExecutor.java b/src/main/java/io/github/easy4j/codex/cli/CodexCliExecutor.java index ae985a4..d75fcbd 100644 --- a/src/main/java/io/github/easy4j/codex/cli/CodexCliExecutor.java +++ b/src/main/java/io/github/easy4j/codex/cli/CodexCliExecutor.java @@ -93,7 +93,7 @@ public CodexCliExecutor(CodexClientConfig config) { * @return a {@link CodexCliResult} describing the outcome; never {@code null}. */ public CodexCliResult execute(String... args) { - return runProcess(null, args); + return runProcess(null, config.getLocalTimeoutSeconds() * 1000L, args); } /** @@ -111,10 +111,14 @@ public CodexCliResult execute(String... args) { * @return a {@link CodexCliResult} describing the outcome; never {@code null}. */ public CodexCliResult executeWithStdin(String stdin, String... args) { - return runProcess(stdin, args); + return runProcess(stdin, config.getLocalTimeoutSeconds() * 1000L, args); } - private CodexCliResult runProcess(String stdin, String... args) { + private CodexCliResult executeWithTimeoutSeconds(int timeoutSeconds, String... args) { + return runProcess(null, timeoutSeconds * 1000L, args); + } + + private CodexCliResult runProcess(String stdin, long timeoutMs, String... args) { CommandLine cmd = CommandLine.parse(config.getLocalExecutable()); for (String arg : args) { if (arg != null) { @@ -136,7 +140,6 @@ private CodexCliResult runProcess(String stdin, String... args) { executor.setStreamHandler(new org.apache.commons.exec.PumpStreamHandler(stdout, stderr, new ByteArrayInputStream(stdinBytes))); - long timeoutMs = config.getLocalTimeoutSeconds() * 1000L; ExecuteWatchdog watchdog = new ExecuteWatchdog(timeoutMs); executor.setWatchdog(watchdog); @@ -186,7 +189,7 @@ private CodexCliResult runProcess(String stdin, String... args) { */ public boolean probe() { try { - CodexCliResult result = execute("--version"); + CodexCliResult result = executeWithTimeoutSeconds(config.getLocalProbeTimeoutSeconds(), "--version"); return result.isSuccess(); } catch (Exception e) { return false; From 64d5f947e480eff948bac237ff895917cc6484f8 Mon Sep 17 00:00:00 2001 From: Loong Wan Date: Sun, 20 Sep 2026 18:56:56 +0800 Subject: [PATCH 3/3] test(client): expose config propagation regressions --- .../github/easy4j/codex/CodexClientTest.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/test/java/io/github/easy4j/codex/CodexClientTest.java b/src/test/java/io/github/easy4j/codex/CodexClientTest.java index a0db0f0..d116d0f 100644 --- a/src/test/java/io/github/easy4j/codex/CodexClientTest.java +++ b/src/test/java/io/github/easy4j/codex/CodexClientTest.java @@ -64,6 +64,34 @@ private static CodexClient echoClient() { return new CodexClient(echoConfig()); } + private static final class CapturingExecutor extends CodexCliExecutor { + private final String stdout; + private String[] lastArgs; + + private CapturingExecutor(CodexClientConfig config, String stdout) { + super(config); + this.stdout = stdout; + } + + @Override + public CodexCliResult execute(String... args) { + this.lastArgs = args; + return new CodexCliResult(0, stdout, ""); + } + + private boolean hasArg(String expected) { + if (lastArgs == null) { + return false; + } + for (String arg : lastArgs) { + if (expected.equals(arg)) { + return true; + } + } + return false; + } + } + // ---------------------------------------------------------------- // Constructor // ---------------------------------------------------------------- @@ -148,6 +176,17 @@ void shouldDelegateExecWithPrompt() { assertTrue(out.contains("hello")); } + @Test + void shouldHonorJsonOutputFalseForNormalExec() { + CodexClientConfig config = echoConfig(); + config.setJsonOutput(false); + + CodexCliResult result = new CodexClient(config).exec("hello"); + + assertFalse(result.getStdout().contains("--json"), + "normal exec must honor CodexClientConfig.jsonOutput=false"); + } + @Test void shouldDelegateExecWithPromptAndModel() { CodexCliResult result = echoClient().exec("hello", "gpt-5"); @@ -251,6 +290,22 @@ void shouldReturnEmptyListForBlankOutput() { // (failed parse lines are skipped) } + @Test + void shouldForceJsonForExecAndParseEvenWhenDefaultJsonOutputIsFalse() { + CodexClientConfig config = echoConfig(); + config.setJsonOutput(false); + CapturingExecutor executor = new CapturingExecutor( + config, "{\"type\":\"message\",\"message\":\"hi\"}"); + CodexClient client = new CodexClient(config, new CodexCli(executor)); + + List events = client.execAndParse("hello"); + + assertTrue(executor.hasArg("--json"), + "execAndParse must force --json independently of the normal exec default"); + assertEquals(1, events.size()); + assertEquals("message", events.get(0).getType()); + } + @Test void shouldParseValidJsonlOutput() throws Exception { // Create a custom client that returns valid JSON-Lines @@ -278,6 +333,17 @@ void shouldDelegateStartSessionWithPrompt() { assertTrue(result.getStdout().contains("hello")); } + @Test + void shouldPropagateNoAltScreenToDefaultInteractiveSession() { + CodexClientConfig config = echoConfig(); + config.setNoAltScreen(true); + + CodexCliResult result = new CodexClient(config).startSession("hello"); + + assertTrue(result.getStdout().contains("--no-alt-screen"), + "default interactive session must honor CodexClientConfig.noAltScreen=true"); + } + @Test void shouldDelegateStartSessionWithOpts() { CodexCli.GlobalOptions opts = new CodexCli.GlobalOptions().model("gpt-5");