feat: refine mobile master agent sync and chat rendering

This commit is contained in:
kris
2026-04-18 04:51:50 +08:00
parent e0c0ea1814
commit 449f84fcbc
61 changed files with 7051 additions and 1075 deletions

View File

@@ -1,7 +1,9 @@
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;
@@ -10,7 +12,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.SpinnerAdapter;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
@@ -35,6 +36,7 @@ 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;
@@ -46,78 +48,15 @@ import java.util.concurrent.TimeUnit;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class AiAccountsActivityTest {
@Test
public void submitOpenAiOnboarding_reportsExplicitPrimaryControllerSuccessAndRefreshesSummary() throws Exception {
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
ReflectionHelpers.setField(activity, "apiClient", new ScriptedBossApiClient(
new RecordingConnection(
new URL("https://boss.hyzq.net/api/v1/accounts/onboard/openai-api"),
200,
"{\"ok\":true,\"accountId\":\"acc-1\"}",
"{\"ok\":false,\"message\":\"ONBOARD_FAILED\"}"
),
new RecordingConnection(
new URL("https://boss.hyzq.net/api/v1/accounts/acc-1/validate"),
200,
"{\"ok\":true,\"message\":\"校验通过\"}",
"{\"ok\":false,\"message\":\"VALIDATION_FAILED\"}"
)
));
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
int initialReloadCount = activity.reloadCount;
ReflectionHelpers.callInstanceMethod(
activity,
"submitOpenAiOnboarding",
ReflectionHelpers.ClassParameter.from(String.class, "主 GPT"),
ReflectionHelpers.ClassParameter.from(String.class, "OpenAI 平台账号"),
ReflectionHelpers.ClassParameter.from(String.class, "sk-test"),
ReflectionHelpers.ClassParameter.from(String.class, "gpt-5.4"),
ReflectionHelpers.ClassParameter.from(String.class, "sk-test-key")
);
org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle();
assertEquals("OpenAI 平台账号已登录,并设为当前主控。", ShadowToast.getTextOfLatestToast());
assertEquals(initialReloadCount + 1, activity.reloadCount);
}
@Test
public void submitOpenAiOnboarding_showsClearChineseFailurePrefix() throws Exception {
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
ReflectionHelpers.setField(activity, "apiClient", new ScriptedBossApiClient(
new RecordingConnection(
new URL("https://boss.hyzq.net/api/v1/accounts/onboard/openai-api"),
403,
"{\"ok\":false,\"message\":\"API Key 无效\"}",
"{\"ok\":false,\"message\":\"API Key 无效\"}"
)
));
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
ReflectionHelpers.callInstanceMethod(
activity,
"submitOpenAiOnboarding",
ReflectionHelpers.ClassParameter.from(String.class, "主 GPT"),
ReflectionHelpers.ClassParameter.from(String.class, "OpenAI 平台账号"),
ReflectionHelpers.ClassParameter.from(String.class, "sk-test"),
ReflectionHelpers.ClassParameter.from(String.class, "gpt-5.4"),
ReflectionHelpers.ClassParameter.from(String.class, "bad-key")
);
org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle();
assertEquals("OpenAI 平台账号登录失败API Key 无效", ShadowToast.getTextOfLatestToast());
assertEquals(1, activity.reloadCount);
}
@Test
public void activeIdentityCardOffersMainAgentTestEntry() throws Exception {
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
JSONObject activeIdentity = new JSONObject()
.put("accountId", "acc-1")
.put("label", " GPT")
.put("displayName", "OpenAI 平台账号")
.put("roleLabel", " GPT")
.put("providerLabel", "OpenAI API")
.put("label", "Agent")
.put("displayName", "ChatGPT OAuth 主链路账号")
.put("roleLabel", "链路")
.put("providerLabel", "ChatGPT登录")
.put("statusLabel", "ready")
.put("note", "当前账号可直接生成主 Agent 回复。")
.put("canGenerate", true);
@@ -140,62 +79,547 @@ public class AiAccountsActivityTest {
}
@Test
public void openAliyunQwenOnboardingDialogUsesPresetModelsWithCustomFallback() throws Exception {
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, "openAliyunQwenOnboardingDialog");
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 root = dialog.getWindow().getDecorView();
Spinner modelSpinner = findSpinnerContainingItem(root, "qwen3.5-plus");
assertNotNull(modelSpinner);
SpinnerAdapter adapter = modelSpinner.getAdapter();
assertNotNull(adapter);
assertEquals(3, adapter.getCount());
assertEquals("qwen3.5-plus", adapter.getItem(0).toString());
assertEquals("qwen3.5-flash", adapter.getItem(1).toString());
assertEquals("自定义模型", adapter.getItem(2).toString());
assertEquals("qwen3.5-plus", modelSpinner.getSelectedItem().toString());
EditText customModelInput = findEditTextWithHint(root, "自定义模型");
assertNotNull(customModelInput);
View dialogRoot = dialog.getWindow().getDecorView();
assertTrue(viewTreeContainsText(dialogRoot, "账号快捷登录"));
assertTrue(viewTreeContainsText(dialogRoot, "选择模型"));
}
@Test
public void openAccountEditorShowsCustomFallbackForNonPresetAliyunModel() throws Exception {
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
JSONObject existing = new JSONObject()
.put("accountId", "acc-1")
.put("label", "备用 GPT")
.put("displayName", "阿里百炼备用账号")
.put("provider", "aliyun_qwen_api")
.put("model", "qwen-custom-x");
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,
"openAccountEditor",
ReflectionHelpers.ClassParameter.from(JSONObject.class, existing),
"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();
Spinner modelSpinner = findSpinnerContainingItem(root, "自定义模型");
assertNotNull(findEditTextWithHint(root, "账号标识 / 备注"));
assertNotNull(findEditTextWithHint(root, "API Key"));
Spinner modelSpinner = findSpinner(root);
assertNotNull(modelSpinner);
SpinnerAdapter adapter = modelSpinner.getAdapter();
assertNotNull(adapter);
assertEquals(3, adapter.getCount());
assertEquals("自定义模型", modelSpinner.getSelectedItem().toString());
assertFalse(modelSpinner.isEnabled());
assertEquals(0, ((android.widget.ArrayAdapter<?>) modelSpinner.getAdapter()).getCount());
}
EditText customModelInput = findEditTextWithHint(root, "自定义模型");
assertNotNull(customModelInput);
assertEquals("qwen-custom-x", customModelInput.getText().toString());
@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 {
@@ -279,8 +703,6 @@ public class AiAccountsActivityTest {
private final int responseCodeValue;
private final String responseBody;
private final String errorBody;
private String requestMethodValue = "GET";
private String contentTypeValue = "";
RecordingConnection(URL url, int responseCodeValue, String responseBody, String errorBody) {
super(url);
@@ -301,16 +723,11 @@ public class AiAccountsActivityTest {
public void connect() {}
@Override
public void setRequestMethod(String method) throws ProtocolException {
requestMethodValue = method;
}
public void setRequestMethod(String method) throws ProtocolException {}
@Override
public void setRequestProperty(String key, String value) {
requestHeaders.put(key, value);
if ("Content-Type".equalsIgnoreCase(key)) {
contentTypeValue = value;
}
}
@Override
@@ -337,6 +754,10 @@ public class AiAccountsActivityTest {
public Map<String, List<String>> getHeaderFields() {
return Collections.emptyMap();
}
String getCapturedRequestBody() {
return requestBody.toString(StandardCharsets.UTF_8);
}
}
private static final class InMemorySharedPreferences implements SharedPreferences {
@@ -484,32 +905,6 @@ public class AiAccountsActivityTest {
return false;
}
private static Spinner findSpinnerContainingItem(View root, String expectedText) {
if (root instanceof Spinner) {
Spinner spinner = (Spinner) root;
SpinnerAdapter adapter = spinner.getAdapter();
if (adapter != null) {
for (int index = 0; index < adapter.getCount(); index += 1) {
Object item = adapter.getItem(index);
if (item != null && item.toString().contains(expectedText)) {
return spinner;
}
}
}
}
if (!(root instanceof ViewGroup)) {
return null;
}
ViewGroup group = (ViewGroup) root;
for (int index = 0; index < group.getChildCount(); index += 1) {
Spinner match = findSpinnerContainingItem(group.getChildAt(index), expectedText);
if (match != null) {
return match;
}
}
return null;
}
private static EditText findEditTextWithHint(View root, String expectedText) {
if (root instanceof EditText) {
CharSequence hint = ((EditText) root).getHint();
@@ -529,4 +924,41 @@ public class AiAccountsActivityTest {
}
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;
}
}