diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/filesystem/local/LocalFilesystemWithShellTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/filesystem/local/LocalFilesystemWithShellTest.java index 3f9ff553b9..d58e205e7d 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/filesystem/local/LocalFilesystemWithShellTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/filesystem/local/LocalFilesystemWithShellTest.java @@ -17,16 +17,24 @@ 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.agentscope.harness.agent.filesystem.model.ExecuteResponse; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.util.Locale; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class LocalFilesystemWithShellTest { + private static final int OUTPUT_LINES = 8192; + private static final String STDOUT_LINE = "out-" + "0123456789abcdef".repeat(8); + private static final String STDERR_LINE = "err-" + "fedcba9876543210".repeat(8); + @Test void outputCharset_usesNativeEncodingOnWindows() { assertEquals( @@ -46,28 +54,59 @@ void outputCharset_fallsBackToDefaultWhenWindowsNativeEncodingIsUnavailable() { LocalFilesystemWithShell.outputCharset("Windows 10", null)); } + @ParameterizedTest + @ValueSource(strings = {"stdout", "stderr", "both"}) + void execute_outputLargerThanOsPipeBufferCompletesWithoutDeadlock( + String streams, @TempDir Path tempDir) { + // More than 1 MiB per stream, with room to capture both streams without truncation. + // Alternating writes to both pipes also catches sequential stdout/stderr readers. + LocalFilesystemWithShell fs = + new LocalFilesystemWithShell(tempDir, false, 10, 4 * 1024 * 1024, null, false); + ExecuteResponse resp = fs.execute(null, largeOutputCommand(streams), 10); + + assertEquals(0, resp.exitCode()); + assertFalse(resp.truncated()); + String stdout = (STDOUT_LINE + "\n").repeat(OUTPUT_LINES); + String stderr = ("[stderr] " + STDERR_LINE + "\n").repeat(OUTPUT_LINES).stripTrailing(); + String expected = + switch (streams) { + case "stdout" -> stdout; + case "stderr" -> stderr; + default -> stdout + "\n" + stderr; + }; + assertEquals(expected, resp.output().replace("\r\n", "\n")); + } + @Test - void execute_outputLargerThanOsPipeBufferCompletesWithoutDeadlock(@TempDir Path tempDir) { - // ~68-72 KB of stdout: beyond the OS pipe buffer (~4 KB on Windows, 64 KB on Linux), - // below the default maxOutputBytes cap. Before stdout/stderr were drained concurrently - // with waitFor, this deadlocked and was misreported as a timeout (exit 124). - int lines = 4000; - String payload = "0123456789abcdef"; // 16 chars per line - boolean windows = System.getProperty("os.name").toLowerCase().contains("win"); - String command = - windows - ? "for /l %i in (1,1," + lines + ") do @echo " + payload - : "i=0; while [ \"$i\" -lt " - + lines - + " ]; do echo " - + payload - + "; i=$((i+1)); done"; + void execute_outputBeyondCaptureLimitStillDrainsBothPipes(@TempDir Path tempDir) { + LocalFilesystemWithShell fs = + new LocalFilesystemWithShell(tempDir, false, 10, 128, null, false); + ExecuteResponse resp = fs.execute(null, largeOutputCommand("both"), 10); - LocalFilesystemWithShell fs = new LocalFilesystemWithShell(tempDir); - ExecuteResponse resp = fs.execute(null, command, 60); + assertEquals(0, resp.exitCode()); + assertTrue(resp.truncated()); + assertEquals( + STDOUT_LINE.substring(0, 128) + "\n\n... Output truncated at 128 bytes.", + resp.output()); + } - assertEquals(0, resp.exitCode(), "unexpected exit code, output: " + resp.output()); - assertFalse(resp.truncated()); - assertEquals(lines, resp.output().split(payload, -1).length - 1); + private static String largeOutputCommand(String streams) { + boolean windows = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win"); + String stdout = "echo " + STDOUT_LINE; + String stderr = windows ? "1>&2 echo " + STDERR_LINE : "echo " + STDERR_LINE + " >&2"; + String body = + switch (streams) { + case "stdout" -> stdout; + case "stderr" -> stderr; + default -> stdout + (windows ? "&" : "; ") + stderr; + }; + // Shell builtins keep the reproducer independent of Python and external child processes. + return windows + ? "for /l %i in (1,1," + OUTPUT_LINES + ") do @(" + body + ")" + : "i=0; while [ \"$i\" -lt " + + OUTPUT_LINES + + " ]; do " + + body + + "; i=$((i+1)); done"; } }