fix: recover master-agent takeover session on android

This commit is contained in:
kris
2026-04-05 11:56:20 +08:00
parent 35913f9d1d
commit e00f7a55ea
2 changed files with 221 additions and 2 deletions

View File

@@ -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);

View File

@@ -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<Runnable> 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();
}
}
}