feat: harden agent onboarding and device import flows
This commit is contained in:
@@ -36,8 +36,8 @@ android {
|
||||
applicationId "com.hyzq.boss"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 18
|
||||
versionName "2.5.5"
|
||||
versionCode 19
|
||||
versionName "2.5.6"
|
||||
buildConfigField "String", "BOSS_API_BASE_URL", "\"https://boss.hyzq.net\""
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -78,28 +78,61 @@ public class AiAccountsActivity extends BossScreenActivity {
|
||||
|
||||
private LinearLayout buildActiveIdentityCard(@Nullable JSONObject activeIdentity) {
|
||||
if (activeIdentity == null) {
|
||||
return BossUi.buildWechatMenuRow(
|
||||
LinearLayout empty = new LinearLayout(this);
|
||||
empty.setOrientation(LinearLayout.VERTICAL);
|
||||
empty.addView(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"当前主控身份",
|
||||
"当前没有可用账号。",
|
||||
"请先新增或启用一个账号。",
|
||||
null,
|
||||
null
|
||||
);
|
||||
));
|
||||
return empty;
|
||||
}
|
||||
String subtitle = activeIdentity.optString("label", "AI 账号")
|
||||
+ " · " + activeIdentity.optString("displayName", "-");
|
||||
String meta = activeIdentity.optString("roleLabel", "-")
|
||||
+ " · " + activeIdentity.optString("providerLabel", "-")
|
||||
+ " · " + activeIdentity.optString("statusLabel", "-");
|
||||
return BossUi.buildWechatMenuRow(
|
||||
String note = activeIdentity.optString("note", "");
|
||||
String activeAccountId = activeIdentity.optString("accountId", "");
|
||||
boolean canGenerate = activeIdentity.optBoolean("canGenerate", false);
|
||||
|
||||
LinearLayout card = new LinearLayout(this);
|
||||
card.setOrientation(LinearLayout.VERTICAL);
|
||||
card.addView(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"当前主控身份",
|
||||
subtitle,
|
||||
meta,
|
||||
null,
|
||||
activeIdentity.optBoolean("isEnvironmentFallback") ? "环境" : "当前",
|
||||
null
|
||||
);
|
||||
));
|
||||
|
||||
if (!note.isEmpty()) {
|
||||
card.addView(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"主控状态",
|
||||
note,
|
||||
activeIdentity.optString("switchReason", ""),
|
||||
null,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
if (!activeAccountId.isEmpty()) {
|
||||
Button validate = BossUi.buildMiniActionButton(this, "校验主控", false);
|
||||
validate.setOnClickListener(v -> validateAccount(activeAccountId));
|
||||
|
||||
Button testMasterAgent = BossUi.buildMiniActionButton(this, "测试主 Agent 对话", canGenerate);
|
||||
testMasterAgent.setEnabled(canGenerate);
|
||||
testMasterAgent.setOnClickListener(v -> openMasterAgentConversation());
|
||||
|
||||
card.addView(BossUi.buildInlineActionRow(this, validate, testMasterAgent));
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
private LinearLayout buildAccountsSection(@Nullable JSONArray accounts) {
|
||||
@@ -564,10 +597,18 @@ public class AiAccountsActivity extends BossScreenActivity {
|
||||
}
|
||||
|
||||
private void validateAccount(JSONObject account) {
|
||||
validateAccount(account.optString("accountId"));
|
||||
}
|
||||
|
||||
private void validateAccount(String accountId) {
|
||||
if (accountId == null || accountId.trim().isEmpty()) {
|
||||
showMessage("当前账号没有可用的账号 ID,暂时无法校验。");
|
||||
return;
|
||||
}
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = apiClient.validateAccount(account.optString("accountId"));
|
||||
BossApiClient.ApiResponse response = apiClient.validateAccount(accountId.trim());
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
runOnUiThread(() -> {
|
||||
showMessage(response.message());
|
||||
@@ -582,6 +623,13 @@ public class AiAccountsActivity extends BossScreenActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void openMasterAgentConversation() {
|
||||
Intent intent = new Intent(this, ProjectDetailActivity.class);
|
||||
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "master-agent");
|
||||
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "主 Agent");
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void confirmDeleteAccount(JSONObject account) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("删除 AI 账号")
|
||||
|
||||
@@ -81,7 +81,7 @@ public class DeviceEnrollmentActivity extends BossScreenActivity {
|
||||
runOnUiThread(() -> {
|
||||
JSONObject enrollment = response.json.optJSONObject("enrollment");
|
||||
JSONObject device = response.json.optJSONObject("device");
|
||||
android.widget.Button importButton = BossUi.buildSecondaryButton(this, "继续导入项目");
|
||||
android.widget.Button importButton = BossUi.buildSecondaryButton(this, "继续导入线程");
|
||||
importButton.setOnClickListener(v -> openImportDraft(device));
|
||||
replaceContent(
|
||||
BossUi.buildSoftPanel(
|
||||
@@ -90,8 +90,9 @@ public class DeviceEnrollmentActivity extends BossScreenActivity {
|
||||
"设备 " + (device == null ? "-" : device.optString("name", "-"))
|
||||
+ "\npairingCode " + (enrollment == null ? "-" : enrollment.optString("pairingCode", "-"))
|
||||
+ "\ntoken " + (enrollment == null ? "-" : enrollment.optString("token", "-")),
|
||||
enrollment == null ? "ready" : enrollment.optString("status", "ready")
|
||||
+ " · 到期 " + enrollment.optString("expiresAt", "-")
|
||||
(enrollment == null ? "ready" : enrollment.optString("status", "ready"))
|
||||
+ " · 到期 " + (enrollment == null ? "-" : enrollment.optString("expiresAt", "-"))
|
||||
+ "\n下一步:打开导入草稿,勾选线程后生成导入建议。"
|
||||
),
|
||||
importButton
|
||||
);
|
||||
|
||||
@@ -9,8 +9,10 @@ import androidx.annotation.Nullable;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
@@ -79,10 +81,10 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
appendContent(BossUi.buildSoftPanel(
|
||||
this,
|
||||
"导入 Codex 项目",
|
||||
(deviceName == null ? "当前设备" : deviceName) + "\n勾选要暴露到会话首页的项目和线程。",
|
||||
(deviceName == null ? "当前设备" : deviceName) + "\n先勾选线程,再生成导入建议,最后应用导入。",
|
||||
draft == null
|
||||
? "等待设备完成首次 heartbeat"
|
||||
: "候选 " + (draft.optJSONArray("candidates") == null ? 0 : draft.optJSONArray("candidates").length()) + " · 状态 " + draft.optString("status", "-")
|
||||
: "状态 " + resolveStatusTitle(draft)
|
||||
));
|
||||
|
||||
if (draft == null) {
|
||||
@@ -98,6 +100,22 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
int recommendedCount = 0;
|
||||
for (int i = 0; i < candidates.length(); i++) {
|
||||
JSONObject candidate = candidates.optJSONObject(i);
|
||||
if (candidate != null && candidate.optBoolean("suggestedImport", false)) {
|
||||
recommendedCount += 1;
|
||||
}
|
||||
}
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
resolveStatusTitle(draft),
|
||||
resolveStatusBody(draft, resolution),
|
||||
"候选 " + candidates.length()
|
||||
+ " · 已选 " + selectedCandidateIds.size()
|
||||
+ " · 推荐 " + recommendedCount
|
||||
));
|
||||
|
||||
Map<String, JSONArray> grouped = new LinkedHashMap<>();
|
||||
for (int i = 0; i < candidates.length(); i++) {
|
||||
JSONObject candidate = candidates.optJSONObject(i);
|
||||
@@ -133,7 +151,9 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
candidate.optString("threadDisplayName", "未命名线程"),
|
||||
"最近活跃:" + candidate.optString("lastActiveAt", "-"),
|
||||
null,
|
||||
selectedState ? "已选" : (candidate.optBoolean("suggestedImport", false) ? "推荐" : null),
|
||||
selectedState
|
||||
? (candidate.optBoolean("suggestedImport", false) ? "已选 · 推荐导入" : "已选")
|
||||
: (candidate.optBoolean("suggestedImport", false) ? "推荐导入" : null),
|
||||
v -> toggleSelection(candidateId)
|
||||
));
|
||||
}
|
||||
@@ -163,6 +183,16 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray appliedProjectNames = draft.optJSONArray("appliedProjectNames");
|
||||
if (appliedProjectNames != null && appliedProjectNames.length() > 0) {
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
"应用结果",
|
||||
"已导入 " + appliedProjectNames.length() + " 个线程:" + joinNames(appliedProjectNames) + "。",
|
||||
"这些线程现在会出现在会话首页。"
|
||||
));
|
||||
}
|
||||
|
||||
Button reviewButton = BossUi.buildMiniActionButton(this, "生成导入建议", true);
|
||||
reviewButton.setEnabled(!selectedCandidateIds.isEmpty());
|
||||
reviewButton.setOnClickListener(v -> reviewSelection());
|
||||
@@ -177,6 +207,67 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
private String resolveStatusTitle(@Nullable JSONObject draft) {
|
||||
if (draft == null) {
|
||||
return "等待导入草稿";
|
||||
}
|
||||
String status = draft.optString("status", "");
|
||||
if ("pending_candidates".equals(status)) {
|
||||
return "等待候选线程";
|
||||
}
|
||||
if ("pending_selection".equals(status)) {
|
||||
return "等待勾选";
|
||||
}
|
||||
if ("pending_resolution".equals(status)) {
|
||||
return "建议生成中";
|
||||
}
|
||||
if ("resolved".equals(status)) {
|
||||
return "建议已生成";
|
||||
}
|
||||
if ("applied".equals(status)) {
|
||||
return "已导入";
|
||||
}
|
||||
return "导入草稿";
|
||||
}
|
||||
|
||||
private String resolveStatusBody(@Nullable JSONObject draft, @Nullable JSONObject resolution) {
|
||||
if (draft == null) {
|
||||
return "先让设备完成首次 heartbeat 并上报候选线程,导入草稿就会出现在这里。";
|
||||
}
|
||||
String status = draft.optString("status", "");
|
||||
if ("pending_candidates".equals(status)) {
|
||||
return "设备已经就绪,等 heartbeat 带回线程候选后,就可以开始勾选。";
|
||||
}
|
||||
if ("pending_selection".equals(status)) {
|
||||
return "先勾选想导入的线程,再生成导入建议。";
|
||||
}
|
||||
if ("pending_resolution".equals(status)) {
|
||||
return "勾选已保存,接下来会生成导入建议。";
|
||||
}
|
||||
if ("resolved".equals(status)) {
|
||||
return resolution == null ? "可以先看建议,再点应用导入。" : resolution.optString("summary", "可以先看建议,再点应用导入。");
|
||||
}
|
||||
if ("applied".equals(status)) {
|
||||
JSONArray appliedProjectNames = draft.optJSONArray("appliedProjectNames");
|
||||
if (appliedProjectNames != null && appliedProjectNames.length() > 0) {
|
||||
return "已导入 " + appliedProjectNames.length() + " 个线程:" + joinNames(appliedProjectNames) + "。";
|
||||
}
|
||||
return "导入已完成,线程已经落到会话首页。";
|
||||
}
|
||||
return "先勾选线程,再生成导入建议,最后应用导入。";
|
||||
}
|
||||
|
||||
private String joinNames(JSONArray values) {
|
||||
List<String> names = new ArrayList<>();
|
||||
for (int i = 0; i < values.length(); i++) {
|
||||
String value = values.optString(i, "");
|
||||
if (!value.isEmpty()) {
|
||||
names.add(value);
|
||||
}
|
||||
}
|
||||
return String.join("、", names);
|
||||
}
|
||||
|
||||
private void toggleSelection(String candidateId) {
|
||||
if (candidateId == null || candidateId.isEmpty()) {
|
||||
return;
|
||||
|
||||
@@ -180,7 +180,8 @@ public class OpenAiOnboardingActivity extends BossScreenActivity {
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
runOnUiThread(() -> {
|
||||
setResult(RESULT_OK);
|
||||
showPostLoginActions();
|
||||
setRefreshing(false);
|
||||
showPostLoginActions(response.json);
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
@@ -194,12 +195,34 @@ public class OpenAiOnboardingActivity extends BossScreenActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void showPostLoginActions() {
|
||||
private void showPostLoginActions(JSONObject responseJson) {
|
||||
JSONObject activeIdentity = responseJson == null ? null : responseJson.optJSONObject("activeIdentity");
|
||||
StringBuilder message = new StringBuilder();
|
||||
if (activeIdentity != null) {
|
||||
String statusLabel = activeIdentity.optString("statusLabel", "");
|
||||
String note = activeIdentity.optString("note", "");
|
||||
message.append("当前主控:")
|
||||
.append(activeIdentity.optString("label", "OpenAI 平台账号"))
|
||||
.append(" · ")
|
||||
.append(activeIdentity.optString("displayName", ""))
|
||||
.append('\n')
|
||||
.append("状态:")
|
||||
.append(statusLabel.isEmpty() ? "可用" : statusLabel);
|
||||
if (!note.isEmpty()) {
|
||||
message.append('\n').append(note);
|
||||
}
|
||||
} else {
|
||||
message.append("OpenAI 平台账号已登录,并设为当前主控。");
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("OpenAI 平台账号已登录")
|
||||
.setMessage("已经设为当前主控。现在就可以直接测试主 Agent 对话。")
|
||||
.setPositiveButton("测试主 Agent 对话", (dialog, which) -> openMasterAgentConversation())
|
||||
.setNegativeButton("稍后再说", (dialog, which) -> finish())
|
||||
.setMessage(message.toString() + "\n\n你现在可以直接测试主 Agent 对话,确认当前主控链路是否可用。")
|
||||
.setPositiveButton("测试主 Agent 对话", (dialog, which) -> {
|
||||
openMasterAgentConversation();
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton("返回账号页", (dialog, which) -> finish())
|
||||
.setOnDismissListener(dialog -> {
|
||||
if (!isFinishing()) {
|
||||
finish();
|
||||
@@ -213,6 +236,5 @@ public class OpenAiOnboardingActivity extends BossScreenActivity {
|
||||
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "master-agent");
|
||||
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "主 Agent");
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user