Skip to content

Commit b5856b9

Browse files
committed
test(appserver): expose websocket send ordering and failure
1 parent 1a92e21 commit b5856b9

1 file changed

Lines changed: 92 additions & 0 deletions

File tree

src/test/java/io/github/easy4j/codex/appserver/CodexAppServerTurnTest.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@
2121
import static org.junit.jupiter.api.Assertions.assertThrows;
2222
import static org.junit.jupiter.api.Assertions.assertTrue;
2323

24+
import java.lang.reflect.Proxy;
25+
import java.net.http.WebSocket;
26+
import java.util.ArrayDeque;
2427
import java.util.ArrayList;
2528
import java.util.List;
29+
import java.util.Queue;
30+
import java.util.concurrent.CompletableFuture;
2631
import java.util.concurrent.CompletionException;
2732
import java.util.concurrent.TimeUnit;
2833

@@ -67,6 +72,44 @@ private JsonNode frameAt(List<String> sent, int index) {
6772
}
6873
}
6974

75+
private String methodOf(String frame) {
76+
try {
77+
return mapper.readTree(frame).path("method").asText("");
78+
} catch (Exception ex) {
79+
throw new IllegalStateException("Invalid frame", ex);
80+
}
81+
}
82+
83+
private WebSocket recordingSocket(List<String> writes,
84+
Queue<CompletableFuture<WebSocket>> completions) {
85+
return (WebSocket) Proxy.newProxyInstance(
86+
WebSocket.class.getClassLoader(),
87+
new Class<?>[]{WebSocket.class},
88+
(proxy, method, args) -> {
89+
if ("sendText".equals(method.getName())) {
90+
writes.add((String) args[0]);
91+
CompletableFuture<WebSocket> completion = completions.poll();
92+
return completion == null
93+
? CompletableFuture.completedFuture((WebSocket) proxy)
94+
: completion;
95+
}
96+
if ("sendClose".equals(method.getName())
97+
|| "sendPing".equals(method.getName())
98+
|| "sendPong".equals(method.getName())
99+
|| "sendBinary".equals(method.getName())) {
100+
return CompletableFuture.completedFuture((WebSocket) proxy);
101+
}
102+
if ("getSubprotocol".equals(method.getName())) {
103+
return "";
104+
}
105+
if ("isInputClosed".equals(method.getName())
106+
|| "isOutputClosed".equals(method.getName())) {
107+
return false;
108+
}
109+
return null;
110+
});
111+
}
112+
70113
/**
71114
* Drives {@code begin()} through the mandatory initialize handshake:
72115
* initialize (id=1) → notifications/initialized → thread/start|resume (id=2).
@@ -77,6 +120,55 @@ private void handshake(CodexAppServerTurn turn) {
77120
"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"serverInfo\":{\"name\":\"codex\"}}}");
78121
}
79122

123+
@Test
124+
void shouldWaitForInitializedSendBeforeThreadStart() {
125+
CodexAppServerTurn turn = newTurn(
126+
AppServerTurnRequest.builder().prompt("hi").build(),
127+
new ThreadMappingCache(10));
128+
129+
List<String> socketWrites = new ArrayList<>();
130+
Queue<CompletableFuture<WebSocket>> completions = new ArrayDeque<>();
131+
CompletableFuture<WebSocket> initializeWrite = new CompletableFuture<>();
132+
CompletableFuture<WebSocket> initializedWrite = new CompletableFuture<>();
133+
CompletableFuture<WebSocket> threadStartWrite = new CompletableFuture<>();
134+
completions.add(initializeWrite);
135+
completions.add(initializedWrite);
136+
completions.add(threadStartWrite);
137+
WebSocket socket = recordingSocket(socketWrites, completions);
138+
initializeWrite.complete(socket);
139+
140+
turn.onOpen(socket);
141+
turn.handleFrame("{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{}}");
142+
143+
assertEquals(2, socketWrites.size(),
144+
"thread/start must wait until notifications/initialized finishes sending");
145+
assertEquals(CodexAppServerProtocol.INITIALIZED, methodOf(socketWrites.get(1)));
146+
147+
initializedWrite.complete(socket);
148+
assertEquals(3, socketWrites.size());
149+
assertEquals(CodexAppServerProtocol.THREAD_START, methodOf(socketWrites.get(2)));
150+
threadStartWrite.complete(socket);
151+
}
152+
153+
@Test
154+
void shouldFailImmediatelyWhenWebSocketSendFails() {
155+
CodexAppServerTurn turn = newTurn(
156+
AppServerTurnRequest.builder().prompt("hi").build(),
157+
new ThreadMappingCache(10));
158+
159+
List<String> socketWrites = new ArrayList<>();
160+
Queue<CompletableFuture<WebSocket>> completions = new ArrayDeque<>();
161+
CompletableFuture<WebSocket> initializeWrite = new CompletableFuture<>();
162+
completions.add(initializeWrite);
163+
WebSocket socket = recordingSocket(socketWrites, completions);
164+
165+
turn.onOpen(socket);
166+
initializeWrite.completeExceptionally(new RuntimeException("write failed"));
167+
168+
assertTrue(turn.future().isCompletedExceptionally(),
169+
"write failure must fail the turn immediately instead of waiting for read timeout");
170+
}
171+
80172
@Test
81173
void shouldMapWebSocketUrls() {
82174
assertEquals("ws://host:8081", CodexAppServerTurn.toWebSocketUrl("ws://host:8081"));

0 commit comments

Comments
 (0)