feat: harden agent onboarding and device import flows

This commit is contained in:
kris
2026-03-31 05:18:58 +08:00
parent 4aed93e90c
commit f417fe1955
18 changed files with 975 additions and 132 deletions

View File

@@ -1,9 +1,14 @@
package com.hyzq.boss;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.json.JSONObject;
import org.junit.Test;
@@ -11,6 +16,8 @@ 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.ShadowToast;
import org.robolectric.util.ReflectionHelpers;
@@ -96,6 +103,36 @@ public class AiAccountsActivityTest {
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("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));
}
private static final class TestAiAccountsActivity extends AiAccountsActivity {
private int reloadCount = 0;
@@ -342,4 +379,43 @@ public class AiAccountsActivityTest {
@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;
}
}

View File

@@ -0,0 +1,175 @@
package com.hyzq.boss;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.Intent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.json.JSONArray;
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.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class DeviceImportDraftActivityTest {
@Test
public void renderCurrentStateShowsSelectionAndRecommendationCopy() throws Exception {
TestDeviceImportDraftActivity activity = Robolectric
.buildActivity(
TestDeviceImportDraftActivity.class,
new Intent()
.putExtra(DeviceImportDraftActivity.EXTRA_DEVICE_ID, "device-1")
.putExtra(DeviceImportDraftActivity.EXTRA_DEVICE_NAME, "Mac Studio")
)
.setup()
.get();
ReflectionHelpers.callInstanceMethod(
activity,
"applyPayload",
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildPendingDraft()),
ReflectionHelpers.ClassParameter.from(JSONObject.class, null)
);
View content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "等待勾选"));
assertTrue(viewTreeContainsText(content, "推荐导入"));
assertTrue(viewTreeContainsText(content, "生成导入建议"));
assertFalse(viewTreeContainsText(content, "应用结果"));
}
@Test
public void renderCurrentStateShowsAppliedResultAndImportedNames() throws Exception {
TestDeviceImportDraftActivity activity = Robolectric
.buildActivity(
TestDeviceImportDraftActivity.class,
new Intent()
.putExtra(DeviceImportDraftActivity.EXTRA_DEVICE_ID, "device-1")
.putExtra(DeviceImportDraftActivity.EXTRA_DEVICE_NAME, "Mac Studio")
)
.setup()
.get();
ReflectionHelpers.callInstanceMethod(
activity,
"applyPayload",
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildAppliedDraft()),
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildAppliedResolution())
);
View content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "已导入"));
assertTrue(viewTreeContainsText(content, "应用结果"));
assertTrue(viewTreeContainsText(content, "北区试产线回归"));
assertTrue(viewTreeContainsText(content, "北区试产线审计"));
assertTrue(viewTreeContainsText(content, "已导入"));
}
private static JSONObject buildPendingDraft() throws Exception {
return new JSONObject()
.put("draftId", "draft-1")
.put("deviceId", "device-1")
.put("status", "pending_selection")
.put("selectedCandidateIds", new JSONArray().put("candidate-1"))
.put("appliedProjectNames", new JSONArray())
.put("candidates", new JSONArray()
.put(new JSONObject()
.put("candidateId", "candidate-1")
.put("deviceId", "device-1")
.put("folderName", "北区试产线")
.put("threadId", "thread-1")
.put("threadDisplayName", "北区试产线回归")
.put("lastActiveAt", "2026-03-30T10:18:00+08:00")
.put("suggestedImport", true))
.put(new JSONObject()
.put("candidateId", "candidate-2")
.put("deviceId", "device-1")
.put("folderName", "北区试产线")
.put("threadId", "thread-2")
.put("threadDisplayName", "北区试产线审计")
.put("lastActiveAt", "2026-03-30T10:20:00+08:00")
.put("suggestedImport", false)));
}
private static JSONObject buildAppliedDraft() throws Exception {
return new JSONObject()
.put("draftId", "draft-1")
.put("deviceId", "device-1")
.put("status", "applied")
.put("selectedCandidateIds", new JSONArray().put("candidate-1").put("candidate-2"))
.put("appliedProjectNames", new JSONArray().put("北区试产线回归").put("北区试产线审计"))
.put("candidates", new JSONArray()
.put(new JSONObject()
.put("candidateId", "candidate-1")
.put("deviceId", "device-1")
.put("folderName", "北区试产线")
.put("threadId", "thread-1")
.put("threadDisplayName", "北区试产线回归")
.put("lastActiveAt", "2026-03-30T10:18:00+08:00")
.put("suggestedImport", true))
.put(new JSONObject()
.put("candidateId", "candidate-2")
.put("deviceId", "device-1")
.put("folderName", "北区试产线")
.put("threadId", "thread-2")
.put("threadDisplayName", "北区试产线审计")
.put("lastActiveAt", "2026-03-30T10:20:00+08:00")
.put("suggestedImport", true)));
}
private static JSONObject buildAppliedResolution() throws Exception {
return new JSONObject()
.put("resolutionId", "resolution-1")
.put("draftId", "draft-1")
.put("deviceId", "device-1")
.put("status", "applied")
.put("summary", "Mac Studio 导入建议:新建 2 个会话。")
.put("items", new JSONArray()
.put(new JSONObject()
.put("candidateId", "candidate-1")
.put("action", "create_thread_conversation")
.put("threadDisplayName", "北区试产线回归")
.put("folderName", "北区试产线")
.put("reason", "作为独立聊天窗口导入。"))
.put(new JSONObject()
.put("candidateId", "candidate-2")
.put("action", "create_thread_conversation")
.put("threadDisplayName", "北区试产线审计")
.put("folderName", "北区试产线")
.put("reason", "作为独立聊天窗口导入。")));
}
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;
}
public static class TestDeviceImportDraftActivity extends DeviceImportDraftActivity {
@Override
protected void reload() {
// Tests render synthetic payloads directly.
}
}
}

View File

@@ -2,6 +2,7 @@ package com.hyzq.boss;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.content.Intent;
import android.net.Uri;
@@ -12,6 +13,7 @@ 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;
@@ -63,16 +65,32 @@ public class OpenAiOnboardingActivityTest {
}
@Test
public void successActionsDialogCanOpenMasterAgentConversation() {
public void successActionsDialogCanOpenMasterAgentConversation() throws Exception {
OpenAiOnboardingActivity activity = Robolectric
.buildActivity(OpenAiOnboardingActivity.class)
.setup()
.get();
ReflectionHelpers.callInstanceMethod(activity, "showPostLoginActions");
JSONObject payload = new JSONObject();
JSONObject activeIdentity = new JSONObject();
activeIdentity.put("label", "主 GPT");
activeIdentity.put("displayName", "OpenAI 平台账号");
activeIdentity.put("statusLabel", "ready");
activeIdentity.put("note", "当前账号可直接生成主 Agent 回复。");
payload.put("activeIdentity", activeIdentity);
ReflectionHelpers.callInstanceMethod(
activity,
"showPostLoginActions",
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
);
AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();
assertNotNull(dialog);
TextView messageView = dialog.findViewById(android.R.id.message);
assertNotNull(messageView);
assertTrue(messageView.getText().toString().contains("当前主控:主 GPT · OpenAI 平台账号"));
assertTrue(messageView.getText().toString().contains("你现在可以直接测试主 Agent 对话"));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick();
Shadows.shadowOf(Looper.getMainLooper()).idle();