diff --git a/android/app/src/main/java/com/hyzq/boss/MasterAgentTakeoverActivity.java b/android/app/src/main/java/com/hyzq/boss/MasterAgentTakeoverActivity.java index 20c64ba..bae3e97 100644 --- a/android/app/src/main/java/com/hyzq/boss/MasterAgentTakeoverActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/MasterAgentTakeoverActivity.java @@ -40,7 +40,7 @@ public class MasterAgentTakeoverActivity extends BossScreenActivity { setRefreshing(true); executor.execute(() -> { try { - BossApiClient.ApiResponse response = apiClient.getProjectAgentControls(projectId); + BossApiClient.ApiResponse response = loadTakeoverControls(); if (!response.ok()) { throw new IllegalStateException(response.message()); } @@ -98,7 +98,7 @@ public class MasterAgentTakeoverActivity extends BossScreenActivity { setRefreshing(true); executor.execute(() -> { try { - BossApiClient.ApiResponse response = apiClient.updateProjectTakeoverSettings( + BossApiClient.ApiResponse response = saveTakeoverControls( projectId, null, enabled @@ -120,6 +120,44 @@ public class MasterAgentTakeoverActivity extends BossScreenActivity { }); } + private BossApiClient.ApiResponse loadTakeoverControls() throws Exception { + BossApiClient.ApiResponse response = apiClient.getProjectAgentControls(projectId); + if (response.ok() || !isUnauthorized(response)) { + return response; + } + BossApiClient.ApiResponse loginResponse = apiClient.autoLogin(); + if (!loginResponse.ok()) { + return response; + } + return apiClient.getProjectAgentControls(projectId); + } + + private BossApiClient.ApiResponse saveTakeoverControls( + String projectId, + @Nullable Boolean takeoverEnabled, + @Nullable Boolean globalTakeoverEnabled + ) throws Exception { + BossApiClient.ApiResponse response = apiClient.updateProjectTakeoverSettings( + projectId, + takeoverEnabled, + globalTakeoverEnabled + ); + if (response.ok() || !isUnauthorized(response)) { + return response; + } + BossApiClient.ApiResponse loginResponse = apiClient.autoLogin(); + if (!loginResponse.ok()) { + return response; + } + return apiClient.updateProjectTakeoverSettings(projectId, takeoverEnabled, globalTakeoverEnabled); + } + + private boolean isUnauthorized(BossApiClient.ApiResponse response) { + return response != null + && response.statusCode == 401 + && "UNAUTHORIZED".equals(response.message()); + } + private void updateSaveAvailability() { if (headerActionButton != null) { headerActionButton.setEnabled(contentLoaded); diff --git a/android/app/src/test/java/com/hyzq/boss/MasterAgentTakeoverActivityTest.java b/android/app/src/test/java/com/hyzq/boss/MasterAgentTakeoverActivityTest.java index 92aaa07..a694fc5 100644 --- a/android/app/src/test/java/com/hyzq/boss/MasterAgentTakeoverActivityTest.java +++ b/android/app/src/test/java/com/hyzq/boss/MasterAgentTakeoverActivityTest.java @@ -1,10 +1,13 @@ package com.hyzq.boss; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import android.content.Context; import android.content.Intent; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; import android.widget.TextView; import org.json.JSONObject; @@ -15,6 +18,11 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.util.ReflectionHelpers; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.TimeUnit; + @RunWith(RobolectricTestRunner.class) @Config(sdk = 34) public class MasterAgentTakeoverActivityTest { @@ -44,6 +52,61 @@ public class MasterAgentTakeoverActivityTest { assertTrue(viewTreeContainsText(content, "线程会话默认跟随全局协同推进")); } + @Test + public void reloadAutoRecoversUnauthorizedAndRendersTakeoverSettings() { + Intent intent = new Intent() + .putExtra(MasterAgentTakeoverActivity.EXTRA_PROJECT_ID, "master-agent") + .putExtra(MasterAgentTakeoverActivity.EXTRA_PROJECT_NAME, "主 Agent"); + TestMasterAgentTakeoverActivity activity = Robolectric + .buildActivity(TestMasterAgentTakeoverActivity.class, intent) + .setup() + .get(); + + RecordingBossApiClient apiClient = new RecordingBossApiClient( + activity.getSharedPreferences("master-agent-takeover-test", Context.MODE_PRIVATE), + "https://boss.hyzq.net" + ); + ReflectionHelpers.setField(activity, "apiClient", apiClient); + ReflectionHelpers.setField(activity, "reloadEnabled", true); + ReflectionHelpers.setField(activity, "executor", new DirectExecutorService()); + + activity.reload(); + + LinearLayout content = activity.findViewById(R.id.screen_content); + assertTrue(viewTreeContainsText(content, "全局主 Agent 协同接管")); + assertEquals(2, apiClient.getProjectAgentControlsCalls); + assertEquals(1, apiClient.autoLoginCalls); + } + + @Test + public void saveAutoRecoversUnauthorizedAndPersistsTakeoverSettings() throws Exception { + Intent intent = new Intent() + .putExtra(MasterAgentTakeoverActivity.EXTRA_PROJECT_ID, "master-agent") + .putExtra(MasterAgentTakeoverActivity.EXTRA_PROJECT_NAME, "主 Agent"); + TestMasterAgentTakeoverActivity activity = Robolectric + .buildActivity(TestMasterAgentTakeoverActivity.class, intent) + .setup() + .get(); + + RecordingBossApiClient apiClient = new RecordingBossApiClient( + activity.getSharedPreferences("master-agent-takeover-save-test", Context.MODE_PRIVATE), + "https://boss.hyzq.net" + ); + apiClient.failFirstLoad = false; + apiClient.failFirstSave = true; + ReflectionHelpers.setField(activity, "apiClient", apiClient); + ReflectionHelpers.setField(activity, "reloadEnabled", true); + ReflectionHelpers.setField(activity, "executor", new DirectExecutorService()); + + activity.reload(); + + ReflectionHelpers.callInstanceMethod(activity, "saveTakeoverSettings"); + + assertEquals(1, apiClient.updateTakeoverCalls); + assertEquals(1, apiClient.retryUpdateTakeoverCalls); + assertEquals(1, apiClient.autoLoginCalls); + } + private static boolean viewTreeContainsText(View root, String expectedText) { if (root instanceof TextView) { CharSequence text = ((TextView) root).getText(); @@ -62,4 +125,122 @@ public class MasterAgentTakeoverActivityTest { } return false; } + + private static final class TestMasterAgentTakeoverActivity extends MasterAgentTakeoverActivity { + private boolean reloadEnabled; + + @Override + protected void reload() { + if (!reloadEnabled) { + return; + } + super.reload(); + } + } + + private static final class RecordingBossApiClient extends BossApiClient { + private int getProjectAgentControlsCalls; + private int autoLoginCalls; + private int updateTakeoverCalls; + private int retryUpdateTakeoverCalls; + private boolean failFirstLoad = true; + private boolean failFirstSave; + + RecordingBossApiClient(android.content.SharedPreferences prefs, String baseUrl) { + super(prefs, baseUrl); + } + + @Override + public ApiResponse getProjectAgentControls(String projectId) throws java.io.IOException, org.json.JSONException { + getProjectAgentControlsCalls += 1; + if (failFirstLoad && getProjectAgentControlsCalls == 1) { + return ApiResponse.error( + 401, + new JSONObject() + .put("ok", false) + .put("message", "UNAUTHORIZED") + ); + } + return new ApiResponse( + 200, + new JSONObject() + .put("ok", true) + .put("controls", new JSONObject().put("globalTakeoverEnabled", true)) + ); + } + + @Override + public ApiResponse autoLogin() throws java.io.IOException, org.json.JSONException { + autoLoginCalls += 1; + return new ApiResponse( + 200, + new JSONObject() + .put("ok", true) + .put("session", new JSONObject().put("account", "17600003315")) + ); + } + + @Override + public ApiResponse updateProjectTakeoverSettings( + String projectId, + Boolean takeoverEnabled, + Boolean globalTakeoverEnabled + ) throws java.io.IOException, org.json.JSONException { + if (failFirstSave && updateTakeoverCalls == 0) { + updateTakeoverCalls += 1; + return ApiResponse.error( + 401, + new JSONObject() + .put("ok", false) + .put("message", "UNAUTHORIZED") + ); + } + if (failFirstSave) { + retryUpdateTakeoverCalls += 1; + } else { + updateTakeoverCalls += 1; + } + return new ApiResponse( + 200, + new JSONObject() + .put("ok", true) + .put("controls", new JSONObject().put("globalTakeoverEnabled", true)) + ); + } + } + + private static final class DirectExecutorService extends AbstractExecutorService { + private boolean shutdown; + + @Override + public void shutdown() { + shutdown = true; + } + + @Override + public List shutdownNow() { + shutdown = true; + return Collections.emptyList(); + } + + @Override + public boolean isShutdown() { + return shutdown; + } + + @Override + public boolean isTerminated() { + return shutdown; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) { + return true; + } + + @Override + public void execute(Runnable command) { + command.run(); + } + } }