3030import java .util .List ;
3131import java .util .Locale ;
3232import java .util .concurrent .CopyOnWriteArrayList ;
33+ import java .util .concurrent .CountDownLatch ;
34+ import java .util .concurrent .TimeUnit ;
35+ import java .util .concurrent .atomic .AtomicInteger ;
3336
3437import com .fasterxml .jackson .databind .JsonNode ;
3538import com .fasterxml .jackson .databind .json .JsonMapper ;
@@ -52,7 +55,9 @@ final class FakeCodexAppServer implements AutoCloseable {
5255 private final ServerSocket serverSocket ;
5356 private final List <String > receivedFrames = new CopyOnWriteArrayList <>();
5457 private final Thread acceptLoop ;
58+ private final AtomicInteger turnStarts = new AtomicInteger ();
5559
60+ private volatile CountDownLatch turnCompletionGate = new CountDownLatch (0 );
5661 private volatile boolean authorizationSeen ;
5762 private volatile boolean closed ;
5863
@@ -78,19 +83,48 @@ boolean authorizationSeen() {
7883 return authorizationSeen ;
7984 }
8085
86+ void holdTurnCompletions () {
87+ turnCompletionGate = new CountDownLatch (1 );
88+ }
89+
90+ void releaseTurnCompletions () {
91+ turnCompletionGate .countDown ();
92+ }
93+
94+ int turnStartCount () {
95+ return turnStarts .get ();
96+ }
97+
98+ boolean awaitTurnStarts (int expected , long timeoutMillis ) throws InterruptedException {
99+ long deadline = System .nanoTime () + TimeUnit .MILLISECONDS .toNanos (timeoutMillis );
100+ while (System .nanoTime () < deadline ) {
101+ if (turnStarts .get () >= expected ) {
102+ return true ;
103+ }
104+ Thread .sleep (10L );
105+ }
106+ return turnStarts .get () >= expected ;
107+ }
108+
81109 private void acceptLoop () {
82110 while (!closed ) {
83- try (Socket socket = serverSocket .accept ()) {
84- handleConnection (socket );
111+ try {
112+ Socket socket = serverSocket .accept ();
113+ Thread connection = new Thread (() -> {
114+ try (Socket closeable = socket ) {
115+ handleConnection (closeable );
116+ } catch (Exception ex ) {
117+ if (!closed ) {
118+ ex .printStackTrace ();
119+ }
120+ }
121+ }, "fake-codex-app-server-connection" );
122+ connection .setDaemon (true );
123+ connection .start ();
85124 } catch (IOException ex ) {
86125 if (!closed ) {
87- // Accept failures on a listening socket mean it was closed; stop quietly.
88126 return ;
89127 }
90- } catch (Exception ex ) {
91- // A runtime failure mid-connection must be visible — a silent
92- // thread death here looks exactly like a client-side timeout.
93- ex .printStackTrace ();
94128 }
95129 }
96130 }
@@ -163,6 +197,7 @@ private void respondTo(OutputStream out, String frame) throws IOException {
163197 return ;
164198 }
165199 if ("turn/start" .equals (method )) {
200+ turnStarts .incrementAndGet ();
166201 sendText (out , "{\" jsonrpc\" :\" 2.0\" ,\" id\" :" + id + ",\" result\" :{}}" );
167202 sendText (out , "{\" method\" :\" turn/started\" ,\" params\" :{\" threadId\" :\" th_e2e\" ,\" turnId\" :\" turn_1\" }}" );
168203 sendText (out , "{\" method\" :\" thread/tokenUsage/updated\" ,\" params\" :{\" tokens\" :1}}" );
@@ -172,6 +207,14 @@ private void respondTo(OutputStream out, String frame) throws IOException {
172207 + "{\" item\" :{\" type\" :\" agentMessage\" ,\" text\" :\" 你好\" }}}" );
173208 sendText (out , "{\" method\" :\" item/completed\" ,\" params\" :"
174209 + "{\" item\" :{\" itemType\" :\" agent_message\" ,\" content\" :\" 世界\" }}}" );
210+ try {
211+ if (!turnCompletionGate .await (5 , TimeUnit .SECONDS )) {
212+ throw new IOException ("Timed out waiting to release fake turn completion" );
213+ }
214+ } catch (InterruptedException ex ) {
215+ Thread .currentThread ().interrupt ();
216+ throw new IOException ("Interrupted waiting to release fake turn completion" , ex );
217+ }
175218 sendText (out , "{\" method\" :\" turn/completed\" ,\" params\" :"
176219 + "{\" threadId\" :\" th_e2e\" ,\" message\" :\" fallback-unused\" }}" );
177220 return ;
0 commit comments