From 6ef3a8babb810a66d2a484601e27a9410e5feea2 Mon Sep 17 00:00:00 2001 From: Loong Wan Date: Sun, 20 Sep 2026 18:34:09 +0800 Subject: [PATCH 1/4] 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 4ae401e..e3c0380 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 823424b63898e10cf5ca614c0fc85bbe578da119 Mon Sep 17 00:00:00 2001 From: Loong Wan Date: Sun, 20 Sep 2026 18:37:09 +0800 Subject: [PATCH 2/4] 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 1a57569..72f5a6a 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); @@ -201,7 +204,7 @@ private static String utf8(ByteArrayOutputStream buffer) { */ public boolean probe() { try { - CodexCliResult result = execute("--version"); + CodexCliResult result = executeWithTimeoutSeconds(config.getLocalProbeTimeoutSeconds(), "--version"); return result.isSuccess(); } catch (Exception e) { return false; From 867de53abf61f5d5bbb9ccf767da0e4f25a82697 Mon Sep 17 00:00:00 2001 From: Loong Wan Date: Sun, 20 Sep 2026 18:40:07 +0800 Subject: [PATCH 3/4] 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 525be47..c7833c9 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"); From 5d9021e3ffec1871b3954411b1b1b1f1ae595581 Mon Sep 17 00:00:00 2001 From: Loong Wan Date: Sun, 20 Sep 2026 18:42:17 +0800 Subject: [PATCH 4/4] fix(client): honor CLI output and interactive defaults --- .../io/github/easy4j/codex/CodexClient.java | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/github/easy4j/codex/CodexClient.java b/src/main/java/io/github/easy4j/codex/CodexClient.java index 3638013..fc2618c 100644 --- a/src/main/java/io/github/easy4j/codex/CodexClient.java +++ b/src/main/java/io/github/easy4j/codex/CodexClient.java @@ -161,7 +161,8 @@ public CodexCliResult exec(CodexCli.ExecOptions opts) { * empty or unparseable. */ public List execAndParse(String prompt) { - CodexCliResult result = exec(prompt); + CodexCli.ExecOptions opts = defaultOptions(prompt).json(true); + CodexCliResult result = cli.exec(opts); return parseJsonlOutput(result.getStdout()); } @@ -296,7 +297,7 @@ public CodexCliResult execWithDisable(String prompt, String... features) { * @return the raw CLI invocation result; never {@code null}. */ public CodexCliResult startSession() { - return cli.startInteractive(); + return cli.startInteractive(defaultGlobalOptions(), null); } /** @@ -306,7 +307,7 @@ public CodexCliResult startSession() { * @return the raw CLI invocation result; never {@code null}. */ public CodexCliResult startSession(String prompt) { - return cli.startInteractive(prompt); + return cli.startInteractive(defaultGlobalOptions(), prompt); } /** @@ -1122,7 +1123,7 @@ public CodexCliResult execute(String... args) { * @return a fresh options instance; never {@code null}. */ private CodexCli.ExecOptions defaultOptions(String prompt) { - CodexCli.ExecOptions opts = new CodexCli.ExecOptions(prompt).json(true); + CodexCli.ExecOptions opts = new CodexCli.ExecOptions(prompt).json(config.isJsonOutput()); if (config.getDefaultModel() != null) opts.model(config.getDefaultModel()); if (config.getDefaultSandbox() != null) opts.sandbox(config.getDefaultSandbox()); if (config.getDefaultApprovalPolicy() != null) opts.approvalPolicy(config.getDefaultApprovalPolicy()); @@ -1146,6 +1147,33 @@ private CodexCli.ExecOptions defaultOptions(String prompt) { return opts; } + /** + * Builds global options for the default interactive-session entry points. + * + * @return a fresh global-options instance populated from client defaults. + */ + private CodexCli.GlobalOptions defaultGlobalOptions() { + CodexCli.GlobalOptions opts = new CodexCli.GlobalOptions(); + if (config.getDefaultModel() != null) opts.model(config.getDefaultModel()); + if (config.getDefaultSandbox() != null) opts.sandbox(config.getDefaultSandbox()); + if (config.getDefaultApprovalPolicy() != null) opts.approvalPolicy(config.getDefaultApprovalPolicy()); + if (config.getDefaultProfile() != null) opts.profile(config.getDefaultProfile()); + if (config.getWorkingDir() != null) opts.workingDir(config.getWorkingDir()); + if (config.getAddDir() != null) opts.addDir(config.getAddDir()); + if (config.isOssProvider()) opts.oss(true); + if (config.getLocalProvider() != null) opts.localProvider(config.getLocalProvider()); + if (config.isSearch()) opts.search(true); + if (config.getImage() != null) opts.image(config.getImage()); + if (config.getConfigOverrides() != null) opts.configOverrides(config.getConfigOverrides()); + if (config.isDangerouslyBypassApprovalsAndSandbox()) opts.dangerouslyBypassApprovalsAndSandbox(true); + if (config.isDangerouslyBypassHookTrust()) opts.dangerouslyBypassHookTrust(true); + if (config.isStrictConfig()) opts.strictConfig(true); + if (config.getEnable() != null) opts.enable(config.getEnable()); + if (config.getDisable() != null) opts.disable(config.getDisable()); + if (config.isNoAltScreen()) opts.noAltScreen(true); + return opts; + } + /** * Decodes a JSON-Lines blob into a list of {@link CodexEvent}s. *