feat: refine mobile master agent sync and chat rendering
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user