1515import com .agentclientprotocol .sdk .client .AcpClient ;
1616import com .agentclientprotocol .sdk .spec .AcpClientTransport ;
1717import com .agentclientprotocol .sdk .spec .AcpSchema ;
18+ import org .jspecify .annotations .Nullable ;
1819import reactor .core .publisher .Mono ;
1920
2021/**
@@ -69,12 +70,36 @@ public class MockAcpClient {
6970
7071 private volatile CountDownLatch updateLatch = new CountDownLatch (0 );
7172
72- private volatile String currentSessionId ;
73-
74- private MockAcpClient (AcpAsyncClient delegate , AcpClientTransport transport , Duration timeout ) {
75- this .delegate = delegate ;
76- this .transport = transport ;
77- this .timeout = timeout ;
73+ private volatile @ Nullable String currentSessionId ;
74+
75+ private MockAcpClient (Builder builder ) {
76+ this .transport = builder .transport ;
77+ this .timeout = builder .requestTimeout ;
78+ Function <AcpSchema .RequestPermissionRequest , AcpSchema .RequestPermissionResponse > permissionHandler = builder .permissionHandler ;
79+ Function <AcpSchema .ReadTextFileRequest , AcpSchema .ReadTextFileResponse > readFileHandler = builder .readFileHandler ;
80+ Function <AcpSchema .WriteTextFileRequest , AcpSchema .WriteTextFileResponse > writeFileHandler = builder .writeFileHandler ;
81+ // The handlers record into this mock's fields, which are initialized before this body runs
82+ this .delegate = AcpClient .async (builder .transport )
83+ .requestTimeout (builder .requestTimeout )
84+ .sessionUpdateConsumer (notification -> {
85+ receivedUpdates .add (notification );
86+ updateLatch .countDown ();
87+ return Mono .empty ();
88+ })
89+ // Using typed handlers (no manual unmarshalling needed)
90+ .requestPermissionHandler ((AcpSchema .RequestPermissionRequest request ) -> {
91+ receivedPermissionRequests .add (request );
92+ return Mono .just (permissionHandler .apply (request ));
93+ })
94+ .readTextFileHandler ((AcpSchema .ReadTextFileRequest request ) -> {
95+ receivedFileReadRequests .add (request );
96+ return Mono .just (readFileHandler .apply (request ));
97+ })
98+ .writeTextFileHandler ((AcpSchema .WriteTextFileRequest request ) -> {
99+ receivedFileWriteRequests .add (request );
100+ return Mono .just (writeFileHandler .apply (request ));
101+ })
102+ .build ();
78103 }
79104
80105 /**
@@ -104,7 +129,7 @@ public AcpSchema.InitializeResponse initialize() {
104129 // Mock client advertises all capabilities by default
105130 AcpSchema .FileSystemCapability fs = new AcpSchema .FileSystemCapability (true , true );
106131 AcpSchema .ClientCapabilities caps = new AcpSchema .ClientCapabilities (fs , true );
107- return delegate .initialize (new AcpSchema .InitializeRequest (1 , caps )). block ( timeout );
132+ return await ( delegate .initialize (new AcpSchema .InitializeRequest (1 , caps )));
108133 }
109134
110135 /**
@@ -113,11 +138,9 @@ public AcpSchema.InitializeResponse initialize() {
113138 * @return The new session response
114139 */
115140 public AcpSchema .NewSessionResponse newSession (String cwd ) {
116- AcpSchema .NewSessionResponse response = delegate .newSession (new AcpSchema .NewSessionRequest (cwd , List .of ()))
117- .block (timeout );
118- if (response != null ) {
119- this .currentSessionId = response .sessionId ();
120- }
141+ AcpSchema .NewSessionResponse response = await (
142+ delegate .newSession (new AcpSchema .NewSessionRequest (cwd , List .of ())));
143+ this .currentSessionId = response .sessionId ();
121144 return response ;
122145 }
123146
@@ -127,12 +150,7 @@ public AcpSchema.NewSessionResponse newSession(String cwd) {
127150 * @return The prompt response
128151 */
129152 public AcpSchema .PromptResponse prompt (String text ) {
130- if (currentSessionId == null ) {
131- throw new IllegalStateException ("No session created. Call newSession() first." );
132- }
133- return delegate
134- .prompt (new AcpSchema .PromptRequest (currentSessionId , List .of (new AcpSchema .TextContent (text ))))
135- .block (timeout );
153+ return prompt (requireSessionId (), text );
136154 }
137155
138156 /**
@@ -142,18 +160,31 @@ public AcpSchema.PromptResponse prompt(String text) {
142160 * @return The prompt response
143161 */
144162 public AcpSchema .PromptResponse prompt (String sessionId , String text ) {
145- return delegate .prompt (new AcpSchema .PromptRequest (sessionId , List .of (new AcpSchema .TextContent (text ))))
146- .block (timeout );
163+ return await (delegate .prompt (new AcpSchema .PromptRequest (sessionId , List .of (new AcpSchema .TextContent (text )))));
147164 }
148165
149166 /**
150167 * Cancels operations for the current session.
151168 */
152169 public void cancel () {
153- if (currentSessionId == null ) {
170+ delegate .cancel (new AcpSchema .CancelNotification (requireSessionId ())).block (timeout );
171+ }
172+
173+ private String requireSessionId () {
174+ String sessionId = this .currentSessionId ;
175+ if (sessionId == null ) {
154176 throw new IllegalStateException ("No session created. Call newSession() first." );
155177 }
156- delegate .cancel (new AcpSchema .CancelNotification (currentSessionId )).block (timeout );
178+ return sessionId ;
179+ }
180+
181+ /** Blocks for a response; a request's Mono emits its response or fails, never completes empty. */
182+ private <T > T await (Mono <T > response ) {
183+ T value = response .block (timeout );
184+ if (value == null ) {
185+ throw new IllegalStateException ("ACP request completed without a response" );
186+ }
187+ return value ;
157188 }
158189
159190 /**
@@ -210,7 +241,7 @@ public List<AcpSchema.WriteTextFileRequest> getReceivedFileWriteRequests() {
210241 * Gets the current session ID.
211242 * @return The session ID, or null if no session created
212243 */
213- public String getCurrentSessionId () {
244+ public @ Nullable String getCurrentSessionId () {
214245 return currentSessionId ;
215246 }
216247
@@ -325,42 +356,7 @@ public Builder requestTimeout(Duration timeout) {
325356 * @return The configured mock client
326357 */
327358 public MockAcpClient build () {
328- // We need to create the mock first to capture updates
329- MockAcpClient mockClient = new MockAcpClient (null , transport , requestTimeout );
330-
331- AcpAsyncClient delegate = AcpClient .async (transport )
332- .requestTimeout (requestTimeout )
333- .sessionUpdateConsumer (notification -> {
334- mockClient .receivedUpdates .add (notification );
335- mockClient .updateLatch .countDown ();
336- return Mono .empty ();
337- })
338- // Using typed handlers (no manual unmarshalling needed)
339- .requestPermissionHandler ((AcpSchema .RequestPermissionRequest request ) -> {
340- mockClient .receivedPermissionRequests .add (request );
341- return Mono .just (permissionHandler .apply (request ));
342- })
343- .readTextFileHandler ((AcpSchema .ReadTextFileRequest request ) -> {
344- mockClient .receivedFileReadRequests .add (request );
345- return Mono .just (readFileHandler .apply (request ));
346- })
347- .writeTextFileHandler ((AcpSchema .WriteTextFileRequest request ) -> {
348- mockClient .receivedFileWriteRequests .add (request );
349- return Mono .just (writeFileHandler .apply (request ));
350- })
351- .build ();
352-
353- // Replace the delegate using reflection
354- try {
355- var field = MockAcpClient .class .getDeclaredField ("delegate" );
356- field .setAccessible (true );
357- field .set (mockClient , delegate );
358- }
359- catch (Exception e ) {
360- throw new RuntimeException ("Failed to set delegate" , e );
361- }
362-
363- return mockClient ;
359+ return new MockAcpClient (this );
364360 }
365361
366362 }
0 commit comments