Skip to content

Commit ac6b111

Browse files
committed
test(appserver): port canonical 2.x contracts to 3.x
1 parent e269f1f commit ac6b111

8 files changed

Lines changed: 713 additions & 37 deletions

‎src/test/java/io/github/easy4j/codex/appserver/CodexAppServerClientE2ETest.java‎

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

‎src/test/java/io/github/easy4j/codex/appserver/CodexAppServerClientTest.java‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ void shouldExposeSecureDefaults() {
4646
assertEquals(1000, config.getMaxSessionMappings());
4747
}
4848

49+
50+
@Test
51+
void shouldAcceptInjectedThreadMappingStore() {
52+
ThreadMappingStore store = new ThreadMappingStore() {
53+
@Override
54+
public String get(String sessionKey) {
55+
return null;
56+
}
57+
58+
@Override
59+
public void put(String sessionKey, String threadId) {
60+
}
61+
62+
@Override
63+
public void remove(String sessionKey) {
64+
}
65+
};
66+
67+
try (CodexAppServerClient client =
68+
new CodexAppServerClient(configWithUrl("ws://localhost:8081"), store)) {
69+
assertNotNull(client);
70+
}
71+
}
72+
4973
@Test
5074
void shouldRejectNullRequestAndBlankPrompt() {
5175
try (CodexAppServerClient client = new CodexAppServerClient(configWithUrl("ws://localhost:8081"))) {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright (c) 2018-present, easy-4-java (https://github.com/easy-4-java).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.easy4j.codex.appserver;
17+
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
20+
21+
import static org.junit.jupiter.api.Assertions.assertFalse;
22+
import static org.junit.jupiter.api.Assertions.assertNotNull;
23+
import static org.junit.jupiter.api.Assertions.assertTrue;
24+
25+
/**
26+
* 显式真实集成测试:默认不执行,仅当同时传入
27+
* {@code -Dcodex.real.base-url=http://host:port}(token 可选
28+
* {@code -Dcodex.real.token=...})时对真实 codex app-server 执行。
29+
*
30+
* <p>协议层验收点(2026-09-17 对 codex 0.154.0 实测提炼):</p>
31+
* <ol>
32+
* <li>initialize → notifications/initialized 握手必须先行,否则 -32600 Not initialized;</li>
33+
* <li>thread id 嵌套在 {@code result.thread.id},顶层 {@code threadId} 为空;</li>
34+
* <li>turn 阶段失败必须来自模型/凭证层,而不是 {@code no threadId}
35+
* 这类协议层错误。</li>
36+
* </ol>
37+
*
38+
* @since 2.0.x
39+
*/
40+
@EnabledIfSystemProperty(named = "codex.real.base-url", matches = "https?://.+")
41+
class CodexAppServerRealIntegrationTest {
42+
43+
@Test
44+
void listThreadsShouldPassProtocolHandshake() {
45+
CodexAppServerClient client = client();
46+
try {
47+
// 走通用 RPC 通道(tolerant initialize);能返回列表即证明握手与帧解析可用
48+
client.listThreads(5);
49+
} finally {
50+
client.close();
51+
}
52+
}
53+
54+
@Test
55+
void runTurnMustGetPastThreadStartEvenWithoutModelCredentials() {
56+
CodexAppServerClient client = client();
57+
try {
58+
try {
59+
client.runTurn(AppServerTurnRequest.builder()
60+
.prompt("只回复两个字母:OK")
61+
.sessionKey("real-wire-smoke")
62+
.build());
63+
// 有凭证时直接完成,也算通过
64+
} catch (CodexAppServerException ex) {
65+
String message = String.valueOf(ex.getMessage());
66+
assertFalse(message.contains("no threadId"),
67+
"threadId 解析回退到了旧协议: " + message);
68+
assertFalse(message.contains("Not initialized"),
69+
"initialize 握手缺失: " + message);
70+
// 无模型凭证的真实环境在此收到 turn/failed 类错误——协议层已通过
71+
}
72+
} finally {
73+
client.close();
74+
}
75+
}
76+
77+
private CodexAppServerClient client() {
78+
CodexAppServerConfig config = new CodexAppServerConfig();
79+
config.setBaseUrl(System.getProperty("codex.real.base-url").trim());
80+
String token = System.getProperty("codex.real.token");
81+
if (token != null && !token.isBlank()) {
82+
config.setToken(token.trim());
83+
}
84+
assertNotNull(config.getBaseUrl());
85+
assertTrue(config.getConnectTimeoutMillis() > 0);
86+
return new CodexAppServerClient(config);
87+
}
88+
}

0 commit comments

Comments
 (0)