965 lines
41 KiB
Java
965 lines
41 KiB
Java
package com.hyzq.boss;
|
||
|
||
import static org.junit.Assert.assertEquals;
|
||
import static org.junit.Assert.assertFalse;
|
||
import static org.junit.Assert.assertNotNull;
|
||
import static org.junit.Assert.assertTrue;
|
||
|
||
import android.content.Intent;
|
||
import android.content.SharedPreferences;
|
||
import android.os.Looper;
|
||
import android.view.View;
|
||
import android.view.ViewGroup;
|
||
import android.widget.EditText;
|
||
import android.widget.Spinner;
|
||
import android.widget.TextView;
|
||
|
||
import androidx.appcompat.app.AlertDialog;
|
||
|
||
import org.json.JSONObject;
|
||
import org.junit.Test;
|
||
import org.junit.runner.RunWith;
|
||
import org.robolectric.Robolectric;
|
||
import org.robolectric.RobolectricTestRunner;
|
||
import org.robolectric.annotation.Config;
|
||
import org.robolectric.Shadows;
|
||
import org.robolectric.shadows.ShadowActivity;
|
||
import org.robolectric.shadows.ShadowDialog;
|
||
import org.robolectric.shadows.ShadowToast;
|
||
import org.robolectric.util.ReflectionHelpers;
|
||
|
||
import java.io.ByteArrayInputStream;
|
||
import java.io.ByteArrayOutputStream;
|
||
import java.io.InputStream;
|
||
import java.io.OutputStream;
|
||
import java.net.HttpURLConnection;
|
||
import java.net.ProtocolException;
|
||
import java.net.URL;
|
||
import java.nio.charset.StandardCharsets;
|
||
import java.util.ArrayList;
|
||
import java.util.Collections;
|
||
import java.util.HashMap;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import java.util.Set;
|
||
import java.util.concurrent.AbstractExecutorService;
|
||
import java.util.concurrent.TimeUnit;
|
||
|
||
@RunWith(RobolectricTestRunner.class)
|
||
@Config(sdk = 34)
|
||
public class AiAccountsActivityTest {
|
||
@Test
|
||
public void activeIdentityCardOffersMainAgentTestEntry() throws Exception {
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
|
||
JSONObject activeIdentity = new JSONObject()
|
||
.put("accountId", "acc-1")
|
||
.put("label", "主Agent")
|
||
.put("displayName", "ChatGPT OAuth 主链路账号")
|
||
.put("roleLabel", "主链路")
|
||
.put("providerLabel", "ChatGPT登录")
|
||
.put("statusLabel", "ready")
|
||
.put("note", "当前账号可直接生成主 Agent 回复。")
|
||
.put("canGenerate", true);
|
||
|
||
View card = ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"buildActiveIdentityCard",
|
||
ReflectionHelpers.ClassParameter.from(JSONObject.class, activeIdentity)
|
||
);
|
||
|
||
View testButton = findClickableViewContainingText(card, "测试主 Agent 对话");
|
||
assertNotNull(testButton);
|
||
testButton.performClick();
|
||
|
||
ShadowActivity shadowActivity = Shadows.shadowOf(activity);
|
||
Intent nextIntent = shadowActivity.getNextStartedActivity();
|
||
assertNotNull(nextIntent);
|
||
assertEquals(ProjectDetailActivity.class.getName(), nextIntent.getComponent().getClassName());
|
||
assertEquals("master-agent", nextIntent.getStringExtra(ProjectDetailActivity.EXTRA_PROJECT_ID));
|
||
}
|
||
|
||
@Test
|
||
public void renderAccountsShowsStructuredSectionsAndExpandedEntries() throws Exception {
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
|
||
JSONObject payload = new JSONObject()
|
||
.put("activeIdentity", new JSONObject()
|
||
.put("accountId", "chatgpt-primary")
|
||
.put("label", "主Agent")
|
||
.put("displayName", "ChatGPT OAuth 主链路账号")
|
||
.put("roleLabel", "主链路")
|
||
.put("providerLabel", "ChatGPT登录")
|
||
.put("statusLabel", "ready")
|
||
.put("canGenerate", true))
|
||
.put("accounts", new org.json.JSONArray()
|
||
.put(new JSONObject()
|
||
.put("accountId", "chatgpt-primary")
|
||
.put("label", "主Agent")
|
||
.put("displayName", "ChatGPT OAuth 主链路账号")
|
||
.put("roleLabel", "主链路")
|
||
.put("providerLabel", "ChatGPT登录")
|
||
.put("provider", "chatgpt_oauth")
|
||
.put("role", "primary")
|
||
.put("statusLabel", "ready")
|
||
.put("enabled", true)
|
||
.put("isActive", true))
|
||
.put(new JSONObject()
|
||
.put("accountId", "hyzq-backup")
|
||
.put("label", "备用API")
|
||
.put("displayName", "环宇智擎 备用账号")
|
||
.put("roleLabel", "备用链路")
|
||
.put("providerLabel", "环宇智擎")
|
||
.put("provider", "hyzq_api")
|
||
.put("role", "backup")
|
||
.put("statusLabel", "ready")
|
||
.put("enabled", true)
|
||
.put("isActive", false)
|
||
.put("apiKeyConfigured", true)
|
||
.put("apiBaseUrl", "https://api.hyzq2046.com/v1"))
|
||
.put(new JSONObject()
|
||
.put("accountId", "master-node")
|
||
.put("label", "主Agent")
|
||
.put("displayName", "绑定电脑上的 Codex 节点")
|
||
.put("roleLabel", "主链路")
|
||
.put("providerLabel", "主Agent 节点")
|
||
.put("provider", "master_codex_node")
|
||
.put("role", "primary")
|
||
.put("statusLabel", "ready")
|
||
.put("enabled", true)
|
||
.put("isActive", false)));
|
||
|
||
ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"renderAccounts",
|
||
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
|
||
);
|
||
|
||
View root = activity.findViewById(R.id.screen_content);
|
||
assertNotNull(root);
|
||
assertTrue(viewTreeContainsText(root, "主要API配置"));
|
||
assertTrue(viewTreeContainsText(root, "备用API配置"));
|
||
assertFalse(viewTreeContainsText(root, "OAuth 登录"));
|
||
assertFalse(viewTreeContainsText(root, "API 接入"));
|
||
assertFalse(viewTreeContainsText(root, "谷歌登录"));
|
||
assertFalse(viewTreeContainsText(root, "ChatGPT登录"));
|
||
assertFalse(viewTreeContainsText(root, "阿里"));
|
||
assertFalse(viewTreeContainsText(root, "Minimax"));
|
||
assertFalse(viewTreeContainsText(root, "GLM"));
|
||
assertFalse(viewTreeContainsText(root, "环宇智擎"));
|
||
assertFalse(viewTreeContainsText(root, "自定义"));
|
||
assertFalse(viewTreeContainsText(root, "绑定设备节点"));
|
||
}
|
||
|
||
@Test
|
||
public void tappingPrimaryConfigEntryOpensPrimaryDetailPage() throws Exception {
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
|
||
|
||
ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"renderAccounts",
|
||
ReflectionHelpers.ClassParameter.from(JSONObject.class, new JSONObject().put("accounts", new org.json.JSONArray()))
|
||
);
|
||
|
||
View root = activity.findViewById(R.id.screen_content);
|
||
View entry = findClickableViewContainingText(root, "主要API配置");
|
||
assertNotNull(entry);
|
||
entry.performClick();
|
||
|
||
ShadowActivity shadowActivity = Shadows.shadowOf(activity);
|
||
Intent nextIntent = shadowActivity.getNextStartedActivity();
|
||
assertNotNull(nextIntent);
|
||
assertEquals(AiAccountsActivity.class.getName(), nextIntent.getComponent().getClassName());
|
||
assertEquals("primary", nextIntent.getStringExtra("ai_accounts_role"));
|
||
}
|
||
|
||
@Test
|
||
public void detailPageShowsOnlySelectedRoleConfiguration() throws Exception {
|
||
Intent intent = new Intent(
|
||
org.robolectric.RuntimeEnvironment.getApplication(),
|
||
TestAiAccountsActivity.class
|
||
);
|
||
intent.putExtra("ai_accounts_role", "primary");
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class, intent).setup().get();
|
||
JSONObject payload = new JSONObject()
|
||
.put("accounts", new org.json.JSONArray()
|
||
.put(new JSONObject()
|
||
.put("accountId", "chatgpt-primary")
|
||
.put("label", "主要API")
|
||
.put("displayName", "ChatGPT OAuth 主链路账号")
|
||
.put("roleLabel", "主链路")
|
||
.put("providerLabel", "ChatGPT登录")
|
||
.put("provider", "chatgpt_oauth")
|
||
.put("role", "primary")
|
||
.put("model", "gpt-5.4-mini")
|
||
.put("statusLabel", "ready")
|
||
.put("enabled", true)
|
||
.put("isActive", true))
|
||
.put(new JSONObject()
|
||
.put("accountId", "hyzq-primary")
|
||
.put("label", "主要API")
|
||
.put("displayName", "环宇智擎 主链路账号")
|
||
.put("roleLabel", "主链路")
|
||
.put("providerLabel", "环宇智擎")
|
||
.put("provider", "hyzq_api")
|
||
.put("role", "primary")
|
||
.put("model", "gpt-5.4")
|
||
.put("statusLabel", "ready")
|
||
.put("enabled", true)
|
||
.put("isActive", false)
|
||
.put("apiKeyConfigured", true)
|
||
.put("apiBaseUrl", "https://api.hyzq2046.com/v1")));
|
||
|
||
ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"renderAccounts",
|
||
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
|
||
);
|
||
|
||
View root = activity.findViewById(R.id.screen_content);
|
||
assertNotNull(root);
|
||
assertTrue(viewTreeContainsText(root, "当前使用方式"));
|
||
assertTrue(viewTreeContainsText(root, "主Agent模式"));
|
||
assertTrue(viewTreeContainsText(root, "快速反应模型"));
|
||
assertTrue(viewTreeContainsText(root, "深度思考模型"));
|
||
assertTrue(viewTreeContainsText(root, "ChatGPT登录"));
|
||
assertTrue(viewTreeContainsText(root, "OAuth 登录"));
|
||
assertTrue(viewTreeContainsText(root, "当前模型:gpt-5.4-mini"));
|
||
assertTrue(viewTreeContainsText(root, "当前:沿用默认"));
|
||
assertTrue(viewTreeContainsText(root, "当前:gpt-5.4-mini"));
|
||
assertTrue(viewTreeContainsText(root, "当前:gpt-5.4"));
|
||
assertTrue(viewTreeContainsText(root, "API 接入"));
|
||
assertTrue(viewTreeContainsText(root, "已配置:ChatGPT登录"));
|
||
assertTrue(viewTreeContainsText(root, "已配置:环宇智擎"));
|
||
assertFalse(viewTreeContainsText(root, "谷歌登录"));
|
||
assertFalse(viewTreeContainsText(root, "阿里"));
|
||
assertFalse(viewTreeContainsText(root, "Minimax"));
|
||
assertFalse(viewTreeContainsText(root, "GLM"));
|
||
assertFalse(viewTreeContainsText(root, "自定义"));
|
||
assertFalse(viewTreeContainsText(root, "可编辑配置"));
|
||
assertFalse(viewTreeContainsText(root, "当前已保存"));
|
||
assertFalse(viewTreeContainsText(root, "只读状态"));
|
||
assertFalse(viewTreeContainsText(root, "备用API配置"));
|
||
}
|
||
|
||
@Test
|
||
public void currentMethodEntryOpensCurrentAccountEditor() throws Exception {
|
||
Intent intent = new Intent(
|
||
org.robolectric.RuntimeEnvironment.getApplication(),
|
||
TestAiAccountsActivity.class
|
||
);
|
||
intent.putExtra("ai_accounts_role", "primary");
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class, intent).setup().get();
|
||
ReflectionHelpers.setField(activity, "currentMasterAgentModelOverride", "gpt-5.4-mini");
|
||
ReflectionHelpers.setField(activity, "currentMasterAgentReasoningEffortOverride", "low");
|
||
ReflectionHelpers.setField(activity, "currentFastModelOverride", "gpt-5.4-mini");
|
||
ReflectionHelpers.setField(activity, "currentDeepModelOverride", "gpt-5.4");
|
||
JSONObject payload = new JSONObject()
|
||
.put("accounts", new org.json.JSONArray()
|
||
.put(new JSONObject()
|
||
.put("accountId", "chatgpt-primary")
|
||
.put("label", "主要API")
|
||
.put("displayName", "ChatGPT OAuth 主链路账号")
|
||
.put("roleLabel", "主链路")
|
||
.put("providerLabel", "ChatGPT登录")
|
||
.put("provider", "chatgpt_oauth")
|
||
.put("role", "primary")
|
||
.put("model", "gpt-5.4-mini")
|
||
.put("statusLabel", "ready")
|
||
.put("enabled", true)
|
||
.put("isActive", true)));
|
||
|
||
ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"renderAccounts",
|
||
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
|
||
);
|
||
|
||
View root = activity.findViewById(R.id.screen_content);
|
||
View entry = findClickableViewContainingText(root, "当前使用方式");
|
||
assertNotNull(entry);
|
||
entry.performClick();
|
||
Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||
|
||
AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();
|
||
assertNotNull(dialog);
|
||
View dialogRoot = dialog.getWindow().getDecorView();
|
||
assertTrue(viewTreeContainsText(dialogRoot, "账号快捷登录"));
|
||
assertTrue(viewTreeContainsText(dialogRoot, "选择模型"));
|
||
}
|
||
|
||
@Test
|
||
public void fastModeEntryOpensDedicatedModelPicker() throws Exception {
|
||
Intent intent = new Intent(
|
||
org.robolectric.RuntimeEnvironment.getApplication(),
|
||
TestAiAccountsActivity.class
|
||
);
|
||
intent.putExtra("ai_accounts_role", "primary");
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class, intent).setup().get();
|
||
ReflectionHelpers.setField(activity, "currentFastModelOverride", "gpt-4.1");
|
||
ReflectionHelpers.setField(activity, "currentDeepModelOverride", "gpt-5.4");
|
||
|
||
ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"renderAccounts",
|
||
ReflectionHelpers.ClassParameter.from(JSONObject.class, new JSONObject()
|
||
.put("accounts", new org.json.JSONArray()
|
||
.put(new JSONObject()
|
||
.put("accountId", "chatgpt-primary")
|
||
.put("label", "主要API")
|
||
.put("displayName", "ChatGPT OAuth 主链路账号")
|
||
.put("roleLabel", "主链路")
|
||
.put("providerLabel", "ChatGPT登录")
|
||
.put("provider", "chatgpt_oauth")
|
||
.put("role", "primary")
|
||
.put("model", "gpt-5.4-mini")
|
||
.put("statusLabel", "ready")
|
||
.put("enabled", true)
|
||
.put("isActive", true))))
|
||
);
|
||
|
||
View root = activity.findViewById(R.id.screen_content);
|
||
View entry = findClickableViewContainingText(root, "快速反应模型");
|
||
assertNotNull(entry);
|
||
entry.performClick();
|
||
Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||
|
||
AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();
|
||
assertNotNull(dialog);
|
||
View dialogRoot = dialog.getWindow().getDecorView();
|
||
assertTrue(viewTreeContainsText(dialogRoot, "快速反应模型"));
|
||
assertTrue(viewTreeContainsText(dialogRoot, "gpt-4.1"));
|
||
}
|
||
|
||
@Test
|
||
public void tappingOauthEntryShowsOauthProviderChooser() throws Exception {
|
||
Intent intent = new Intent(
|
||
org.robolectric.RuntimeEnvironment.getApplication(),
|
||
TestAiAccountsActivity.class
|
||
);
|
||
intent.putExtra("ai_accounts_role", "primary");
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class, intent).setup().get();
|
||
|
||
ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"renderAccounts",
|
||
ReflectionHelpers.ClassParameter.from(JSONObject.class, new JSONObject().put("accounts", new org.json.JSONArray()))
|
||
);
|
||
|
||
View root = activity.findViewById(R.id.screen_content);
|
||
View entry = findClickableViewContainingText(root, "OAuth 登录");
|
||
assertNotNull(entry);
|
||
entry.performClick();
|
||
Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||
|
||
AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();
|
||
assertNotNull(dialog);
|
||
View dialogRoot = dialog.getWindow().getDecorView();
|
||
assertTrue(viewTreeContainsText(dialogRoot, "谷歌登录"));
|
||
assertTrue(viewTreeContainsText(dialogRoot, "ChatGPT登录"));
|
||
}
|
||
|
||
@Test
|
||
public void tappingApiEntryShowsApiProviderChooser() throws Exception {
|
||
Intent intent = new Intent(
|
||
org.robolectric.RuntimeEnvironment.getApplication(),
|
||
TestAiAccountsActivity.class
|
||
);
|
||
intent.putExtra("ai_accounts_role", "primary");
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class, intent).setup().get();
|
||
|
||
ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"renderAccounts",
|
||
ReflectionHelpers.ClassParameter.from(JSONObject.class, new JSONObject().put("accounts", new org.json.JSONArray()))
|
||
);
|
||
|
||
View root = activity.findViewById(R.id.screen_content);
|
||
View entry = findClickableViewContainingText(root, "API 接入");
|
||
assertNotNull(entry);
|
||
entry.performClick();
|
||
Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||
|
||
AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();
|
||
assertNotNull(dialog);
|
||
View dialogRoot = dialog.getWindow().getDecorView();
|
||
assertTrue(viewTreeContainsText(dialogRoot, "阿里"));
|
||
assertTrue(viewTreeContainsText(dialogRoot, "Minimax"));
|
||
assertTrue(viewTreeContainsText(dialogRoot, "GLM"));
|
||
assertTrue(viewTreeContainsText(dialogRoot, "环宇智擎"));
|
||
assertTrue(viewTreeContainsText(dialogRoot, "自定义"));
|
||
}
|
||
|
||
@Test
|
||
public void defaultApiBaseUrlForProviderSupportsExpandedApiProviders() {
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
|
||
|
||
String openai = ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"defaultApiBaseUrlForProvider",
|
||
ReflectionHelpers.ClassParameter.from(String.class, "openai_api")
|
||
);
|
||
String aliyun = ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"defaultApiBaseUrlForProvider",
|
||
ReflectionHelpers.ClassParameter.from(String.class, "aliyun_qwen_api")
|
||
);
|
||
String minimax = ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"defaultApiBaseUrlForProvider",
|
||
ReflectionHelpers.ClassParameter.from(String.class, "minimax_api")
|
||
);
|
||
String glm = ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"defaultApiBaseUrlForProvider",
|
||
ReflectionHelpers.ClassParameter.from(String.class, "glm_api")
|
||
);
|
||
String hyzq = ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"defaultApiBaseUrlForProvider",
|
||
ReflectionHelpers.ClassParameter.from(String.class, "hyzq_api")
|
||
);
|
||
String custom = ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"defaultApiBaseUrlForProvider",
|
||
ReflectionHelpers.ClassParameter.from(String.class, "custom_api")
|
||
);
|
||
|
||
assertEquals("https://api.openai.com/v1", openai);
|
||
assertEquals("https://dashscope.aliyuncs.com/compatible-mode/v1", aliyun);
|
||
assertEquals("https://api.minimaxi.com/v1", minimax);
|
||
assertEquals("https://open.bigmodel.cn/api/paas/v4", glm);
|
||
assertEquals("https://api.hyzq2046.com/v1", hyzq);
|
||
assertEquals("", custom);
|
||
}
|
||
|
||
@Test
|
||
public void openOauthAccountDialogShowsLoginAction() throws Exception {
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
|
||
|
||
ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"openOauthAccountDialog",
|
||
ReflectionHelpers.ClassParameter.from(String.class, "primary"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "google_oauth"),
|
||
ReflectionHelpers.ClassParameter.from(JSONObject.class, null)
|
||
);
|
||
Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||
|
||
AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();
|
||
assertNotNull(dialog);
|
||
View root = dialog.getWindow().getDecorView();
|
||
assertTrue(viewTreeContainsText(root, "账号快捷登录"));
|
||
assertTrue(viewTreeContainsText(root, "谷歌登录"));
|
||
Spinner modelSpinner = findSpinner(root);
|
||
assertNotNull(modelSpinner);
|
||
assertFalse(modelSpinner.isEnabled());
|
||
assertFalse(modelSpinner.isClickable());
|
||
}
|
||
|
||
@Test
|
||
public void openOauthAccountDialogEnablesModelSelectionWhenAccountIsReady() throws Exception {
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
|
||
JSONObject existing = new JSONObject()
|
||
.put("label", "主要API")
|
||
.put("displayName", "ChatGPT OAuth 主链路账号")
|
||
.put("accountIdentifier", "kris@example.com")
|
||
.put("model", "gpt-5.4")
|
||
.put("loginStatusNote", "已登录")
|
||
.put("enabled", true)
|
||
.put("isActive", true)
|
||
.put("status", "ready")
|
||
.put("statusLabel", "ready");
|
||
|
||
ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"openOauthAccountDialog",
|
||
ReflectionHelpers.ClassParameter.from(String.class, "primary"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "chatgpt_oauth"),
|
||
ReflectionHelpers.ClassParameter.from(JSONObject.class, existing)
|
||
);
|
||
Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||
|
||
AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();
|
||
assertNotNull(dialog);
|
||
View root = dialog.getWindow().getDecorView();
|
||
Spinner modelSpinner = findSpinner(root);
|
||
assertNotNull(modelSpinner);
|
||
assertTrue(modelSpinner.isEnabled());
|
||
assertTrue(modelSpinner.isClickable());
|
||
}
|
||
|
||
@Test
|
||
public void openApiAccountDialogLocksModelSelectionBeforeValidation() throws Exception {
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
|
||
|
||
ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"openApiAccountDialog",
|
||
ReflectionHelpers.ClassParameter.from(String.class, "backup"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "hyzq_api"),
|
||
ReflectionHelpers.ClassParameter.from(JSONObject.class, null),
|
||
ReflectionHelpers.ClassParameter.from(String.class, null)
|
||
);
|
||
Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||
|
||
AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();
|
||
assertNotNull(dialog);
|
||
View root = dialog.getWindow().getDecorView();
|
||
assertNotNull(findEditTextWithHint(root, "账号标识 / 备注"));
|
||
assertNotNull(findEditTextWithHint(root, "API Key"));
|
||
Spinner modelSpinner = findSpinner(root);
|
||
assertNotNull(modelSpinner);
|
||
assertFalse(modelSpinner.isEnabled());
|
||
assertEquals(0, ((android.widget.ArrayAdapter<?>) modelSpinner.getAdapter()).getCount());
|
||
}
|
||
|
||
@Test
|
||
public void applyDraftValidatedModelsEnablesModelSelection() throws Exception {
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
|
||
Spinner spinner = new Spinner(activity);
|
||
android.widget.ArrayAdapter<String> adapter = new android.widget.ArrayAdapter<>(
|
||
activity,
|
||
android.R.layout.simple_spinner_dropdown_item,
|
||
new ArrayList<>()
|
||
);
|
||
spinner.setAdapter(adapter);
|
||
spinner.setEnabled(false);
|
||
org.json.JSONArray models = new org.json.JSONArray().put("gpt-5.4-mini").put("gpt-5.4");
|
||
|
||
ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"applyValidatedApiModels",
|
||
ReflectionHelpers.ClassParameter.from(Spinner.class, spinner),
|
||
ReflectionHelpers.ClassParameter.from(android.widget.ArrayAdapter.class, adapter),
|
||
ReflectionHelpers.ClassParameter.from(org.json.JSONArray.class, models),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "gpt-5.4")
|
||
);
|
||
|
||
assertTrue(spinner.isEnabled());
|
||
assertEquals(2, adapter.getCount());
|
||
assertEquals("gpt-5.4", spinner.getSelectedItem());
|
||
}
|
||
|
||
@Test
|
||
public void saveExpandedApiProviderUsesGenericCreateFlowAndAutoFillsBaseUrl() throws Exception {
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
|
||
RecordingConnection createConnection = new RecordingConnection(
|
||
new URL("https://boss.hyzq.net/api/v1/accounts"),
|
||
200,
|
||
"{\"ok\":true,\"accountId\":\"acc-1\"}",
|
||
"{\"ok\":false,\"message\":\"SAVE_FAILED\"}"
|
||
);
|
||
ReflectionHelpers.setField(activity, "apiClient", new ScriptedBossApiClient(createConnection));
|
||
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
|
||
int initialReloadCount = activity.reloadCount;
|
||
|
||
ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"saveAccount",
|
||
ReflectionHelpers.ClassParameter.from(JSONObject.class, null),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "备用API"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "环宇智擎备用账号"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "fallback@example.com"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, ""),
|
||
ReflectionHelpers.ClassParameter.from(String.class, ""),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "gpt-5.4"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, ""),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "hyzq-secret"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "待校验"),
|
||
ReflectionHelpers.ClassParameter.from(boolean.class, true),
|
||
ReflectionHelpers.ClassParameter.from(boolean.class, false),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "backup"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "hyzq_api")
|
||
);
|
||
org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||
|
||
assertEquals("AI 账号已新增", ShadowToast.getTextOfLatestToast());
|
||
assertEquals(initialReloadCount + 1, activity.reloadCount);
|
||
|
||
JSONObject requestJson = new JSONObject(createConnection.getCapturedRequestBody());
|
||
assertEquals("hyzq_api", requestJson.getString("provider"));
|
||
assertEquals("backup", requestJson.getString("role"));
|
||
assertEquals("https://api.hyzq2046.com/v1", requestJson.getString("apiBaseUrl"));
|
||
assertEquals("hyzq-secret", requestJson.getString("apiKey"));
|
||
}
|
||
|
||
@Test
|
||
public void saveOauthAccountUsesGenericCreateFlow() throws Exception {
|
||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
|
||
RecordingConnection createConnection = new RecordingConnection(
|
||
new URL("https://boss.hyzq.net/api/v1/accounts"),
|
||
200,
|
||
"{\"ok\":true,\"accountId\":\"acc-2\"}",
|
||
"{\"ok\":false,\"message\":\"SAVE_FAILED\"}"
|
||
);
|
||
ReflectionHelpers.setField(activity, "apiClient", new ScriptedBossApiClient(createConnection));
|
||
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
|
||
|
||
ReflectionHelpers.callInstanceMethod(
|
||
activity,
|
||
"saveAccount",
|
||
ReflectionHelpers.ClassParameter.from(JSONObject.class, null),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "主Agent"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "ChatGPT OAuth 主链路账号"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "kris@example.com"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, ""),
|
||
ReflectionHelpers.ClassParameter.from(String.class, ""),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "gpt-5.4"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, ""),
|
||
ReflectionHelpers.ClassParameter.from(String.class, ""),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "待网页登录"),
|
||
ReflectionHelpers.ClassParameter.from(boolean.class, true),
|
||
ReflectionHelpers.ClassParameter.from(boolean.class, true),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "primary"),
|
||
ReflectionHelpers.ClassParameter.from(String.class, "chatgpt_oauth")
|
||
);
|
||
org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||
|
||
assertEquals("AI 账号已新增", ShadowToast.getTextOfLatestToast());
|
||
JSONObject requestJson = new JSONObject(createConnection.getCapturedRequestBody());
|
||
assertEquals("chatgpt_oauth", requestJson.getString("provider"));
|
||
assertEquals("primary", requestJson.getString("role"));
|
||
assertEquals("待网页登录", requestJson.getString("loginStatusNote"));
|
||
assertEquals("", requestJson.getString("apiBaseUrl"));
|
||
}
|
||
|
||
private static final class TestAiAccountsActivity extends AiAccountsActivity {
|
||
private int reloadCount = 0;
|
||
|
||
@Override
|
||
protected void reload() {
|
||
reloadCount += 1;
|
||
}
|
||
}
|
||
|
||
private static final class DirectExecutorService extends AbstractExecutorService {
|
||
private boolean shutdown;
|
||
|
||
@Override
|
||
public void shutdown() {
|
||
shutdown = true;
|
||
}
|
||
|
||
@Override
|
||
public List<Runnable> shutdownNow() {
|
||
shutdown = true;
|
||
return Collections.emptyList();
|
||
}
|
||
|
||
@Override
|
||
public boolean isShutdown() {
|
||
return shutdown;
|
||
}
|
||
|
||
@Override
|
||
public boolean isTerminated() {
|
||
return shutdown;
|
||
}
|
||
|
||
@Override
|
||
public boolean awaitTermination(long timeout, TimeUnit unit) {
|
||
return true;
|
||
}
|
||
|
||
@Override
|
||
public void execute(Runnable command) {
|
||
command.run();
|
||
}
|
||
}
|
||
|
||
private static final class ScriptedBossApiClient extends BossApiClient {
|
||
private final Map<String, RecordingConnection> connections;
|
||
|
||
ScriptedBossApiClient(RecordingConnection... connections) {
|
||
super(new InMemorySharedPreferences(), "https://boss.hyzq.net");
|
||
this.connections = new HashMap<>();
|
||
for (RecordingConnection connection : connections) {
|
||
this.connections.put(connection.getURL().getPath(), connection);
|
||
}
|
||
}
|
||
|
||
@Override
|
||
HttpURLConnection openConnection(String 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) {
|
||
// JVM 单测不需要落 Android 侧身份缓存。
|
||
}
|
||
}
|
||
|
||
private static final class RecordingConnection extends HttpURLConnection {
|
||
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream();
|
||
private final Map<String, String> requestHeaders = new HashMap<>();
|
||
private final int responseCodeValue;
|
||
private final String responseBody;
|
||
private final String errorBody;
|
||
|
||
RecordingConnection(URL url, int responseCodeValue, String responseBody, String errorBody) {
|
||
super(url);
|
||
this.responseCodeValue = responseCodeValue;
|
||
this.responseBody = responseBody;
|
||
this.errorBody = errorBody;
|
||
}
|
||
|
||
@Override
|
||
public void disconnect() {}
|
||
|
||
@Override
|
||
public boolean usingProxy() {
|
||
return false;
|
||
}
|
||
|
||
@Override
|
||
public void connect() {}
|
||
|
||
@Override
|
||
public void setRequestMethod(String method) throws ProtocolException {}
|
||
|
||
@Override
|
||
public void setRequestProperty(String key, String value) {
|
||
requestHeaders.put(key, value);
|
||
}
|
||
|
||
@Override
|
||
public OutputStream getOutputStream() {
|
||
return requestBody;
|
||
}
|
||
|
||
@Override
|
||
public int getResponseCode() {
|
||
return responseCodeValue;
|
||
}
|
||
|
||
@Override
|
||
public InputStream getInputStream() {
|
||
return new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8));
|
||
}
|
||
|
||
@Override
|
||
public InputStream getErrorStream() {
|
||
return new ByteArrayInputStream(errorBody.getBytes(StandardCharsets.UTF_8));
|
||
}
|
||
|
||
@Override
|
||
public Map<String, List<String>> getHeaderFields() {
|
||
return Collections.emptyMap();
|
||
}
|
||
|
||
String getCapturedRequestBody() {
|
||
return requestBody.toString(StandardCharsets.UTF_8);
|
||
}
|
||
}
|
||
|
||
private static final class InMemorySharedPreferences implements SharedPreferences {
|
||
private final Map<String, String> values = new HashMap<>();
|
||
|
||
@Override
|
||
public Map<String, ?> getAll() {
|
||
return Collections.unmodifiableMap(values);
|
||
}
|
||
|
||
@Override
|
||
public String getString(String key, String defValue) {
|
||
return values.getOrDefault(key, defValue);
|
||
}
|
||
|
||
@Override
|
||
public Set<String> getStringSet(String key, Set<String> defValues) {
|
||
throw new UnsupportedOperationException();
|
||
}
|
||
|
||
@Override
|
||
public int getInt(String key, int defValue) {
|
||
throw new UnsupportedOperationException();
|
||
}
|
||
|
||
@Override
|
||
public long getLong(String key, long defValue) {
|
||
throw new UnsupportedOperationException();
|
||
}
|
||
|
||
@Override
|
||
public float getFloat(String key, float defValue) {
|
||
throw new UnsupportedOperationException();
|
||
}
|
||
|
||
@Override
|
||
public boolean getBoolean(String key, boolean defValue) {
|
||
throw new UnsupportedOperationException();
|
||
}
|
||
|
||
@Override
|
||
public boolean contains(String key) {
|
||
return values.containsKey(key);
|
||
}
|
||
|
||
@Override
|
||
public Editor edit() {
|
||
return new Editor() {
|
||
@Override
|
||
public Editor putString(String key, String value) {
|
||
values.put(key, value);
|
||
return this;
|
||
}
|
||
|
||
@Override
|
||
public Editor remove(String key) {
|
||
values.remove(key);
|
||
return this;
|
||
}
|
||
|
||
@Override
|
||
public Editor clear() {
|
||
values.clear();
|
||
return this;
|
||
}
|
||
|
||
@Override
|
||
public Editor putStringSet(String key, Set<String> values) {
|
||
throw new UnsupportedOperationException();
|
||
}
|
||
|
||
@Override
|
||
public Editor putInt(String key, int value) {
|
||
throw new UnsupportedOperationException();
|
||
}
|
||
|
||
@Override
|
||
public Editor putLong(String key, long value) {
|
||
throw new UnsupportedOperationException();
|
||
}
|
||
|
||
@Override
|
||
public Editor putFloat(String key, float value) {
|
||
throw new UnsupportedOperationException();
|
||
}
|
||
|
||
@Override
|
||
public Editor putBoolean(String key, boolean value) {
|
||
throw new UnsupportedOperationException();
|
||
}
|
||
|
||
@Override
|
||
public boolean commit() {
|
||
return true;
|
||
}
|
||
|
||
@Override
|
||
public void apply() {}
|
||
};
|
||
}
|
||
|
||
@Override
|
||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||
|
||
@Override
|
||
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||
}
|
||
|
||
private static View findClickableViewContainingText(View root, String expectedText) {
|
||
if (root == null) {
|
||
return null;
|
||
}
|
||
if (root.isClickable() && viewTreeContainsText(root, expectedText)) {
|
||
return root;
|
||
}
|
||
if (!(root instanceof ViewGroup)) {
|
||
return null;
|
||
}
|
||
ViewGroup group = (ViewGroup) root;
|
||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||
View match = findClickableViewContainingText(group.getChildAt(index), expectedText);
|
||
if (match != null) {
|
||
return match;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static boolean viewTreeContainsText(View root, String expectedText) {
|
||
if (root instanceof TextView) {
|
||
CharSequence text = ((TextView) root).getText();
|
||
if (text != null && text.toString().contains(expectedText)) {
|
||
return true;
|
||
}
|
||
}
|
||
if (!(root instanceof ViewGroup)) {
|
||
return false;
|
||
}
|
||
ViewGroup group = (ViewGroup) root;
|
||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||
if (viewTreeContainsText(group.getChildAt(index), expectedText)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private static EditText findEditTextWithHint(View root, String expectedText) {
|
||
if (root instanceof EditText) {
|
||
CharSequence hint = ((EditText) root).getHint();
|
||
if (hint != null && hint.toString().contains(expectedText)) {
|
||
return (EditText) root;
|
||
}
|
||
}
|
||
if (!(root instanceof ViewGroup)) {
|
||
return null;
|
||
}
|
||
ViewGroup group = (ViewGroup) root;
|
||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||
EditText match = findEditTextWithHint(group.getChildAt(index), expectedText);
|
||
if (match != null) {
|
||
return match;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static EditText findEditTextWithText(View root, String expectedText) {
|
||
if (root instanceof EditText) {
|
||
CharSequence text = ((EditText) root).getText();
|
||
if (text != null && text.toString().contains(expectedText)) {
|
||
return (EditText) root;
|
||
}
|
||
}
|
||
if (!(root instanceof ViewGroup)) {
|
||
return null;
|
||
}
|
||
ViewGroup group = (ViewGroup) root;
|
||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||
EditText match = findEditTextWithText(group.getChildAt(index), expectedText);
|
||
if (match != null) {
|
||
return match;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static Spinner findSpinner(View root) {
|
||
if (root instanceof Spinner) {
|
||
return (Spinner) root;
|
||
}
|
||
if (!(root instanceof ViewGroup)) {
|
||
return null;
|
||
}
|
||
ViewGroup group = (ViewGroup) root;
|
||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||
Spinner match = findSpinner(group.getChildAt(index));
|
||
if (match != null) {
|
||
return match;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
}
|