Skip to content

Commit 063ee5a

Browse files
committed
test(runtime): lock concurrency environment and capture bounds
1 parent 05cdd77 commit 063ee5a

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package io.github.easy4j.opencode.cli;
2+
3+
import io.github.easy4j.opencode.OpenCodeCliConfig;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.lang.reflect.Method;
7+
import java.util.concurrent.CompletableFuture;
8+
import java.util.concurrent.TimeUnit;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
class OpenCodeCliExecutorRuntimeContractTest {
13+
14+
@Test
15+
void maxConcurrentExecutionsMustActuallyLimitChildProcesses() throws Exception {
16+
OpenCodeCliConfig config = new OpenCodeCliConfig();
17+
config.setExecutable("sh");
18+
config.setTimeout(10);
19+
config.setMaxConcurrentExecutions(1);
20+
OpenCodeCliExecutor executor = new OpenCodeCliExecutor(config);
21+
22+
long started = System.nanoTime();
23+
CompletableFuture<OpenCodeCliResult> first = CompletableFuture.supplyAsync(
24+
() -> executor.execute("-c", "sleep 1.5; printf first"));
25+
CompletableFuture<OpenCodeCliResult> second = CompletableFuture.supplyAsync(
26+
() -> executor.execute("-c", "sleep 1.5; printf second"));
27+
28+
assertTrue(first.get(8, TimeUnit.SECONDS).isSuccess());
29+
assertTrue(second.get(8, TimeUnit.SECONDS).isSuccess());
30+
31+
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started);
32+
assertTrue(elapsedMs >= 2500,
33+
"with maxConcurrentExecutions=1 the two 1.5s processes must run serially, elapsed=" + elapsedMs);
34+
}
35+
36+
@Test
37+
void perExecutionEnvironmentMustNotLeakToNextExecution() throws Exception {
38+
Class<?> contextType = assertDoesNotThrow(
39+
() -> Class.forName("io.github.easy4j.opencode.cli.OpenCodeCliExecutionContext"));
40+
Object context = assertDoesNotThrow(() -> contextType.getConstructor().newInstance());
41+
42+
Method environment = assertDoesNotThrow(
43+
() -> contextType.getMethod("environment", String.class, String.class));
44+
environment.invoke(context, "OPENCODE_SDK_TEST_ENV", "scoped-value");
45+
46+
OpenCodeCliConfig config = new OpenCodeCliConfig();
47+
config.setExecutable("sh");
48+
config.setTimeout(5);
49+
OpenCodeCliExecutor executor = new OpenCodeCliExecutor(config);
50+
51+
Method execute = assertDoesNotThrow(
52+
() -> OpenCodeCliExecutor.class.getMethod("execute", contextType, String[].class));
53+
54+
OpenCodeCliResult scoped = (OpenCodeCliResult) execute.invoke(
55+
executor, context, new String[]{"-c", "printf %s \"$OPENCODE_SDK_TEST_ENV\""});
56+
assertEquals("scoped-value", scoped.getStdout());
57+
58+
OpenCodeCliResult next = executor.execute(
59+
"-c", "printf %s \"\${OPENCODE_SDK_TEST_ENV-unset}\"");
60+
assertEquals("unset", next.getStdout(),
61+
"per-execution environment overrides must not mutate later executions");
62+
}
63+
64+
@Test
65+
void stdoutCaptureMustBeBoundedAndMarkedTruncated() throws Exception {
66+
OpenCodeCliConfig config = new OpenCodeCliConfig();
67+
config.setExecutable("sh");
68+
config.setTimeout(5);
69+
70+
Method setter = assertDoesNotThrow(
71+
() -> OpenCodeCliConfig.class.getMethod("setMaxStdoutBytes", int.class));
72+
setter.invoke(config, 128);
73+
74+
OpenCodeCliExecutor executor = new OpenCodeCliExecutor(config);
75+
OpenCodeCliResult result = executor.execute(
76+
"-c", "head -c 1024 /dev/zero | tr '\\000' x");
77+
78+
assertTrue(result.isSuccess());
79+
assertTrue(result.getStdout().length() <= 128,
80+
"retained stdout must respect maxStdoutBytes");
81+
82+
Method truncated = assertDoesNotThrow(
83+
() -> OpenCodeCliResult.class.getMethod("isStdoutTruncated"));
84+
assertEquals(Boolean.TRUE, truncated.invoke(result));
85+
}
86+
}

0 commit comments

Comments
 (0)