feat: complete chat routing and openai onboarding
This commit is contained in:
@@ -49,6 +49,8 @@ public class BossApiClientDispatchPlansTest {
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/p1/dispatch-plans/plan-1/confirm", apiClient.lastPath);
|
||||
assertEquals("POST", connection.requestMethodValue);
|
||||
assertEquals(12000, connection.connectTimeoutValue);
|
||||
assertEquals(65000, connection.readTimeoutValue);
|
||||
assertEquals("{\"approvedTargetProjectIds\":[\"target-1\",\"target-2\"]}", connection.requestBody());
|
||||
}
|
||||
|
||||
@@ -66,6 +68,71 @@ public class BossApiClientDispatchPlansTest {
|
||||
assertEquals(65000, connection.readTimeoutValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendProjectMessageUsesExtendedReadTimeoutForNormalThread() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/thread-1/messages"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
BossApiClient.ApiResponse response = apiClient.sendProjectMessage("thread-1", "你好", "text");
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/thread-1/messages", apiClient.lastPath);
|
||||
assertEquals("POST", connection.requestMethodValue);
|
||||
assertEquals(12000, connection.connectTimeoutValue);
|
||||
assertEquals(65000, connection.readTimeoutValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onboardOpenAiApiAccountUsesDedicatedRouteAndSetsActive() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/accounts/onboard/openai-api"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("label", "主 GPT")
|
||||
.put("displayName", "OpenAI 平台账号")
|
||||
.put("accountIdentifier", "sk-test")
|
||||
.put("model", "gpt-5.4")
|
||||
.put("apiKey", "sk-test-key");
|
||||
|
||||
BossApiClient.ApiResponse response = apiClient.onboardOpenAiApiAccount(payload);
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/accounts/onboard/openai-api", apiClient.lastPath);
|
||||
assertEquals("POST", connection.requestMethodValue);
|
||||
assertEquals(
|
||||
"{\"label\":\"主 GPT\",\"displayName\":\"OpenAI 平台账号\",\"accountIdentifier\":\"sk-test\",\"model\":\"gpt-5.4\",\"apiKey\":\"sk-test-key\",\"setActive\":true}",
|
||||
connection.requestBody()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onboardMasterNodeFallsBackToGenericAccountCreationWhenDedicatedRouteMissing() throws Exception {
|
||||
RecordingConnection dedicated = new RecordingConnection(
|
||||
new URL("https://boss.hyzq.net/api/v1/accounts/onboard/master-node"),
|
||||
404,
|
||||
"{\"ok\":false,\"message\":\"NOT_FOUND\"}",
|
||||
"{\"ok\":false,\"message\":\"NOT_FOUND\"}"
|
||||
);
|
||||
RecordingConnection fallback = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/accounts"));
|
||||
ScriptedBossApiClient apiClient = new ScriptedBossApiClient(dedicated, fallback);
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("label", "主 GPT")
|
||||
.put("displayName", "Mac Studio")
|
||||
.put("accountIdentifier", "mac-studio")
|
||||
.put("nodeId", "mac-studio")
|
||||
.put("nodeLabel", "Mac Studio")
|
||||
.put("model", "gpt-5.4");
|
||||
|
||||
BossApiClient.ApiResponse response = apiClient.onboardMasterNodeAccount(payload);
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/accounts", apiClient.lastPath);
|
||||
assertEquals("POST", fallback.requestMethodValue);
|
||||
assertEquals(
|
||||
"{\"label\":\"主 GPT\",\"displayName\":\"Mac Studio\",\"accountIdentifier\":\"mac-studio\",\"nodeId\":\"mac-studio\",\"nodeLabel\":\"Mac Studio\",\"model\":\"gpt-5.4\",\"setActive\":true}",
|
||||
fallback.requestBody()
|
||||
);
|
||||
}
|
||||
|
||||
private static final class RecordingBossApiClient extends BossApiClient {
|
||||
private final RecordingConnection connection;
|
||||
private String lastPath = "";
|
||||
@@ -92,15 +159,58 @@ public class BossApiClientDispatchPlansTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ScriptedBossApiClient extends BossApiClient {
|
||||
private final Map<String, RecordingConnection> connections;
|
||||
private String lastPath = "";
|
||||
|
||||
ScriptedBossApiClient(RecordingConnection... connections) {
|
||||
super(new InMemorySharedPreferences(), "https://boss.hyzq.net");
|
||||
this.connections = new HashMap<>();
|
||||
for (RecordingConnection connection : connections) {
|
||||
this.connections.put(connection.url().getPath(), connection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
HttpURLConnection openConnection(String path) {
|
||||
lastPath = path;
|
||||
RecordingConnection connection = connections.get(path);
|
||||
if (connection == null) {
|
||||
throw new IllegalStateException("Missing scripted connection for " + path);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
String encode(String value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
void rememberIdentity(JSONObject json) {
|
||||
// no-op for JVM unit test
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingConnection extends HttpURLConnection {
|
||||
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream();
|
||||
private final Map<String, String> requestHeaders = new HashMap<>();
|
||||
private String requestMethodValue = "GET";
|
||||
private int connectTimeoutValue = 0;
|
||||
private int readTimeoutValue = 0;
|
||||
private final int responseCodeValue;
|
||||
private final String responseBody;
|
||||
private final String errorBody;
|
||||
|
||||
RecordingConnection(URL url) {
|
||||
this(url, 200, "{\"ok\":true}", "{\"ok\":false}");
|
||||
}
|
||||
|
||||
RecordingConnection(URL url, int responseCodeValue, String responseBody, String errorBody) {
|
||||
super(url);
|
||||
this.responseCodeValue = responseCodeValue;
|
||||
this.responseBody = responseBody;
|
||||
this.errorBody = errorBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -146,17 +256,29 @@ public class BossApiClientDispatchPlansTest {
|
||||
|
||||
@Override
|
||||
public int getResponseCode() {
|
||||
return 200;
|
||||
return responseCodeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return new ByteArrayInputStream("{\"ok\":true}".getBytes(StandardCharsets.UTF_8));
|
||||
return new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getErrorStream() {
|
||||
if (responseCodeValue < 400) {
|
||||
return null;
|
||||
}
|
||||
return new ByteArrayInputStream(errorBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
String requestBody() {
|
||||
return requestBody.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
URL url() {
|
||||
return getURL();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InMemorySharedPreferences implements SharedPreferences {
|
||||
|
||||
@@ -194,4 +194,59 @@ public class ProjectChatUiStateTest {
|
||||
|
||||
assertEquals(List.of("p2", "p1"), approvedTargetIds);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queuedReplyTaskStartsReplyWaitFromRequestMessageId() throws Exception {
|
||||
JSONObject response = new JSONObject()
|
||||
.put("message", new JSONObject().put("id", "msg-user-1"))
|
||||
.put("task", new JSONObject()
|
||||
.put("taskId", "task-1")
|
||||
.put("taskType", "conversation_reply")
|
||||
.put("status", "queued"));
|
||||
|
||||
ProjectChatUiState.ReplyWaitSpec waitSpec = ProjectChatUiState.resolveReplyWaitAfterSend(response);
|
||||
|
||||
assertTrue(waitSpec.shouldWait);
|
||||
assertEquals("msg-user-1", waitSpec.baselineMessageId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completedReplyTaskDoesNotStartReplyWait() throws Exception {
|
||||
JSONObject response = new JSONObject()
|
||||
.put("message", new JSONObject().put("id", "msg-user-1"))
|
||||
.put("task", new JSONObject()
|
||||
.put("taskId", "task-1")
|
||||
.put("taskType", "conversation_reply")
|
||||
.put("status", "completed"));
|
||||
|
||||
ProjectChatUiState.ReplyWaitSpec waitSpec = ProjectChatUiState.resolveReplyWaitAfterSend(response);
|
||||
|
||||
assertFalse(waitSpec.shouldWait);
|
||||
assertEquals("", waitSpec.baselineMessageId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dispatchConfirmWaitsFromNoticeMessageId() throws Exception {
|
||||
JSONObject response = new JSONObject()
|
||||
.put("notice", new JSONObject().put("id", "msg-notice-1"))
|
||||
.put("executions", new JSONArray()
|
||||
.put(new JSONObject().put("executionId", "exec-1")));
|
||||
|
||||
ProjectChatUiState.ReplyWaitSpec waitSpec = ProjectChatUiState.resolveReplyWaitAfterDispatchConfirm(response);
|
||||
|
||||
assertTrue(waitSpec.shouldWait);
|
||||
assertEquals("msg-notice-1", waitSpec.baselineMessageId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replyWaitSatisfiedOnlyAfterLatestMessageMovesPastBaseline() throws Exception {
|
||||
JSONObject project = new JSONObject()
|
||||
.put("messages", new JSONArray()
|
||||
.put(new JSONObject().put("id", "msg-user-1"))
|
||||
.put(new JSONObject().put("id", "msg-thread-1")));
|
||||
|
||||
assertTrue(ProjectChatUiState.hasReplyBeyondBaseline(project, "msg-user-1"));
|
||||
assertFalse(ProjectChatUiState.hasReplyBeyondBaseline(project, "msg-thread-1"));
|
||||
assertFalse(ProjectChatUiState.hasReplyBeyondBaseline(project, ""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,4 +54,13 @@ public class ProjectDetailActivityChromeBindingsTest {
|
||||
assertEquals("北区试产线回归", bindings.title);
|
||||
assertEquals("归档确认", bindings.subtitle);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void describeDispatchPlanApprovalStateUsesUserFacingLabels() {
|
||||
assertEquals("待确认", ProjectDetailActivity.describeDispatchPlanApprovalState("pending_user"));
|
||||
assertEquals("等待主 Agent 处理", ProjectDetailActivity.describeDispatchPlanApprovalState("pending_agent"));
|
||||
assertEquals("已确认,等待线程回流", ProjectDetailActivity.describeDispatchPlanApprovalState("approved"));
|
||||
assertEquals("已拒绝", ProjectDetailActivity.describeDispatchPlanApprovalState("rejected"));
|
||||
assertEquals("无需确认", ProjectDetailActivity.describeDispatchPlanApprovalState("not_required"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user