|
24 | 24 | import java.util.concurrent.CompletableFuture; |
25 | 25 | import java.util.concurrent.CompletionStage; |
26 | 26 | import java.util.concurrent.CountDownLatch; |
| 27 | +import java.util.concurrent.BlockingQueue; |
| 28 | +import java.util.concurrent.LinkedBlockingQueue; |
27 | 29 | import java.util.concurrent.TimeUnit; |
28 | 30 | import java.util.concurrent.atomic.AtomicInteger; |
29 | 31 | import java.util.concurrent.atomic.AtomicReference; |
@@ -285,6 +287,44 @@ void rejectsNonInitializeFirstMessage() throws Exception { |
285 | 287 | } |
286 | 288 | } |
287 | 289 |
|
| 290 | + @Test |
| 291 | + void rejectsDuplicateInitializeWithoutForwardingItToTheAgent() throws Exception { |
| 292 | + AtomicInteger initializeCalls = new AtomicInteger(); |
| 293 | + AcpAgentFactory agentFactory = AcpAgentFactory.async(transport -> AcpAgent.async(transport) |
| 294 | + .initializeHandler(request -> { |
| 295 | + initializeCalls.incrementAndGet(); |
| 296 | + return Mono.just(new AcpSchema.InitializeResponse(AcpSchema.LATEST_PROTOCOL_VERSION, |
| 297 | + new AcpSchema.AgentCapabilities(true, null, null), List.of())); |
| 298 | + }) |
| 299 | + .build()); |
| 300 | + |
| 301 | + try (FixtureServer server = FixtureServer.start(agentFactory)) { |
| 302 | + MessageRecordingListener listener = new MessageRecordingListener(); |
| 303 | + WebSocket webSocket = HttpClient.newHttpClient() |
| 304 | + .newWebSocketBuilder() |
| 305 | + .connectTimeout(TIMEOUT) |
| 306 | + .buildAsync(server.endpoint(), listener) |
| 307 | + .get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); |
| 308 | + |
| 309 | + assertThat(listener.openLatch.await(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)).isTrue(); |
| 310 | + webSocket.sendText(""" |
| 311 | + {"jsonrpc":"2.0","id":"init-1","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{}}} |
| 312 | + """, true).get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); |
| 313 | + assertThat(readJsonRpcMessage(listener)).isInstanceOf(AcpSchema.JSONRPCResponse.class); |
| 314 | + |
| 315 | + webSocket.sendText(""" |
| 316 | + {"jsonrpc":"2.0","id":"init-2","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{}}} |
| 317 | + """, true).get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); |
| 318 | + |
| 319 | + AcpSchema.JSONRPCResponse response = (AcpSchema.JSONRPCResponse) readJsonRpcMessage(listener); |
| 320 | + assertThat(response.id()).isEqualTo("init-2"); |
| 321 | + assertThat(response.error()).isNotNull(); |
| 322 | + assertThat(response.error().code()).isEqualTo(-32600); |
| 323 | + assertThat(response.error().message()).isEqualTo("Initialize not allowed on existing connection"); |
| 324 | + assertThat(initializeCalls).hasValue(1); |
| 325 | + } |
| 326 | + } |
| 327 | + |
288 | 328 | private static AcpAgentFactory simpleAgentFactory() { |
289 | 329 | AtomicInteger sessionCounter = new AtomicInteger(); |
290 | 330 | return AcpAgentFactory.async(transport -> AcpAgent.async(transport) |
@@ -318,6 +358,12 @@ private static void assertEventuallyNoConnections(StreamableHttpAcpAgentTranspor |
318 | 358 | assertThat(transport.activeConnectionCount()).isEqualTo(0); |
319 | 359 | } |
320 | 360 |
|
| 361 | + private static AcpSchema.JSONRPCMessage readJsonRpcMessage(MessageRecordingListener listener) throws Exception { |
| 362 | + String message = listener.messages.poll(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); |
| 363 | + assertThat(message).isNotNull(); |
| 364 | + return AcpSchema.deserializeJsonRpcMessage(AcpJsonMapper.createDefault(), message); |
| 365 | + } |
| 366 | + |
321 | 367 | private static int freePort() { |
322 | 368 | try (ServerSocket socket = new ServerSocket(0)) { |
323 | 369 | return socket.getLocalPort(); |
@@ -395,4 +441,25 @@ public void onError(WebSocket webSocket, Throwable error) { |
395 | 441 |
|
396 | 442 | } |
397 | 443 |
|
| 444 | + private static final class MessageRecordingListener implements WebSocket.Listener { |
| 445 | + |
| 446 | + private final CountDownLatch openLatch = new CountDownLatch(1); |
| 447 | + |
| 448 | + private final BlockingQueue<String> messages = new LinkedBlockingQueue<>(); |
| 449 | + |
| 450 | + @Override |
| 451 | + public void onOpen(WebSocket webSocket) { |
| 452 | + openLatch.countDown(); |
| 453 | + webSocket.request(1); |
| 454 | + } |
| 455 | + |
| 456 | + @Override |
| 457 | + public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) { |
| 458 | + messages.add(data.toString()); |
| 459 | + webSocket.request(1); |
| 460 | + return CompletableFuture.completedFuture(null); |
| 461 | + } |
| 462 | + |
| 463 | + } |
| 464 | + |
398 | 465 | } |
0 commit comments