@@ -63,10 +63,69 @@ void shouldRunFullTurnWithDeltasAndHandshakeAuth() throws Exception {
6363
6464 assertEquals ("th_e2e" , result .getThreadId ());
6565 assertEquals ("你好世界" , result .getContent ());
66- assertEquals ("stop " , result .getFinishReason ());
66+ assertEquals ("completed " , result .getFinishReason ());
6767 assertEquals (List .of ("你好" , "世界" ), deltas );
6868 assertTrue (server .authorizationSeen (), "WebSocket handshake must carry the bearer token" );
69- assertEquals ("thread/start" , methodOf (server .receivedFrames ().get (0 )));
69+ // 真实协议(codex ≥0.14x):turn 前必须先 initialize 握手
70+ assertEquals ("initialize" , methodOf (server .receivedFrames ().get (0 )));
71+ assertTrue (methodsOf (server ).contains ("thread/start" ),
72+ "turn must issue thread/start after the initialize handshake" );
73+ }
74+ }
75+
76+
77+ @ Test
78+ void shouldSerializeConcurrentTurnsForSameSessionKey () throws Exception {
79+ try (FakeCodexAppServer server = new FakeCodexAppServer ();
80+ CodexAppServerClient client = new CodexAppServerClient (configFor (server ))) {
81+ server .holdTurnCompletions ();
82+
83+ java .util .concurrent .CompletableFuture <AppServerTurnResult > first =
84+ client .runTurnAsync (AppServerTurnRequest .builder ()
85+ .prompt ("first" )
86+ .sessionKey ("chat-serial" )
87+ .build ());
88+ assertTrue (server .awaitTurnStarts (1 , 2_000 ), "first turn must reach server" );
89+
90+ java .util .concurrent .CompletableFuture <AppServerTurnResult > second =
91+ client .runTurnAsync (AppServerTurnRequest .builder ()
92+ .prompt ("second" )
93+ .sessionKey ("chat-serial" )
94+ .build ());
95+
96+ assertFalse (server .awaitTurnStarts (2 , 750 ),
97+ "second same-session turn must not start while first is active" );
98+
99+ server .releaseTurnCompletions ();
100+ first .get (5 , TimeUnit .SECONDS );
101+ second .get (5 , TimeUnit .SECONDS );
102+ assertEquals (2 , server .turnStartCount ());
103+ }
104+ }
105+
106+ @ Test
107+ void shouldAllowConcurrentTurnsForDifferentSessionKeys () throws Exception {
108+ try (FakeCodexAppServer server = new FakeCodexAppServer ();
109+ CodexAppServerClient client = new CodexAppServerClient (configFor (server ))) {
110+ server .holdTurnCompletions ();
111+
112+ java .util .concurrent .CompletableFuture <AppServerTurnResult > first =
113+ client .runTurnAsync (AppServerTurnRequest .builder ()
114+ .prompt ("first" )
115+ .sessionKey ("chat-a" )
116+ .build ());
117+ java .util .concurrent .CompletableFuture <AppServerTurnResult > second =
118+ client .runTurnAsync (AppServerTurnRequest .builder ()
119+ .prompt ("second" )
120+ .sessionKey ("chat-b" )
121+ .build ());
122+
123+ assertTrue (server .awaitTurnStarts (2 , 2_000 ),
124+ "different sessions must be able to run concurrently" );
125+
126+ server .releaseTurnCompletions ();
127+ first .get (5 , TimeUnit .SECONDS );
128+ second .get (5 , TimeUnit .SECONDS );
70129 }
71130 }
72131
@@ -168,8 +227,12 @@ void shouldSendInitializeHandshake() throws Exception {
168227 List <String > methods = server .receivedFrames ().stream ()
169228 .map (this ::methodOf )
170229 .toList ();
171- assertTrue (methods .contains ("initialize" ), "generic RPC calls must open with the initialize handshake" );
172- assertTrue (methods .contains ("initialized" ), "initialize must be followed by the initialized notification" );
230+ assertTrue (methods .size () >= 3 , "initialize handshake and business request must all be present" );
231+ assertEquals ("initialize" , methods .get (0 ));
232+ assertEquals ("notifications/initialized" , methods .get (1 ),
233+ "initialize response must be acknowledged with the official notification name" );
234+ assertEquals ("thread/list" , methods .get (2 ),
235+ "business RPC must follow the initialized acknowledgement" );
173236 }
174237 }
175238
0 commit comments