feat: narrow thread sync context and dedupe realtime refresh

This commit is contained in:
kris
2026-04-05 03:23:11 +08:00
parent da78e82a90
commit 5a53b60f13
9 changed files with 678 additions and 83 deletions

View File

@@ -27,4 +27,14 @@ public class BossRealtimeClientTest {
public void parseEventBlockReturnsNullForKeepaliveComment() {
assertNull(BossRealtimeClient.parseEventBlock(": keepalive\n\n"));
}
@Test
public void parseEventBlockIgnoresHeartbeatControlEvents() {
assertNull(BossRealtimeClient.parseEventBlock("event: heartbeat\n\n"));
}
@Test
public void parseEventBlockReturnsNullForEmptyEventPayloads() {
assertNull(BossRealtimeClient.parseEventBlock("event: conversation.updated\n\n"));
}
}

View File

@@ -7,6 +7,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
@@ -25,6 +26,7 @@ public class MainActivityRealtimeTest {
new BossRealtimeEvent("conversation.updated", new JSONObject().put("projectId", "project-1"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(1, activity.refreshCount);
}
@@ -41,10 +43,79 @@ public class MainActivityRealtimeTest {
new BossRealtimeEvent("devices.updated", new JSONObject().put("deviceId", "mac-studio"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(0, activity.refreshCount);
}
@Test
public void blankProjectIdConversationEventDoesNotRefreshVisibleConversationTab() throws Exception {
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent("conversation.updated", new JSONObject())
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(0, activity.refreshCount);
}
@Test
public void contextIndicatorEventRefreshesVisibleConversationTab() throws Exception {
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent(
"conversation.context_indicator.updated",
new JSONObject().put("projectId", "project-1")
)
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(1, activity.refreshCount);
}
@Test
public void distinctConversationEventsBackToBackBothRefreshVisibleConversationTab() throws Exception {
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent(
"conversation.updated",
new JSONObject().put("projectId", "project-1").put("at", "2026-04-05T10:00:00.000Z")
)
)
);
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent(
"project.messages.updated",
new JSONObject().put("projectId", "project-1").put("at", "2026-04-05T10:00:00.500Z")
)
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(2, activity.refreshCount);
}
public static class TestMainActivity extends MainActivity {
int refreshCount;

View File

@@ -9,6 +9,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
@@ -34,6 +35,7 @@ public class ProjectDetailActivityRealtimeTest {
new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-1"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(1, activity.reloadCount);
}
@@ -57,10 +59,111 @@ public class ProjectDetailActivityRealtimeTest {
new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-2"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(0, activity.reloadCount);
}
@Test
public void masterAgentTaskEventDoesNotRefreshForDifferentProjectId() throws Exception {
Intent intent = new Intent()
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "master-agent")
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "主 Agent");
TestRealtimeProjectDetailActivity activity = Robolectric
.buildActivity(TestRealtimeProjectDetailActivity.class, intent)
.setup()
.resume()
.get();
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent("master_agent.task.updated", new JSONObject().put("projectId", "project-2"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(0, activity.reloadCount);
}
@Test
public void distinctRealtimeEventsBackToBackStillReloadMatchingProject() throws Exception {
Intent intent = new Intent()
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-1")
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "北区试产线");
TestRealtimeProjectDetailActivity activity = Robolectric
.buildActivity(TestRealtimeProjectDetailActivity.class, intent)
.setup()
.resume()
.get();
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent(
"conversation.updated",
new JSONObject().put("projectId", "project-1").put("at", "2026-04-05T10:00:00.000Z")
)
)
);
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent(
"project.messages.updated",
new JSONObject().put("projectId", "project-1").put("at", "2026-04-05T10:00:00.500Z")
)
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(2, activity.reloadCount);
}
@Test
public void duplicateRealtimeEventsWithDifferentAtAreDeduped() throws Exception {
Intent intent = new Intent()
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-1")
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "北区试产线");
TestRealtimeProjectDetailActivity activity = Robolectric
.buildActivity(TestRealtimeProjectDetailActivity.class, intent)
.setup()
.resume()
.get();
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent(
"project.messages.updated",
new JSONObject().put("projectId", "project-1").put("at", "2026-04-05T10:00:00.000Z")
)
)
);
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent(
"project.messages.updated",
new JSONObject().put("projectId", "project-1").put("at", "2026-04-05T10:00:00.500Z")
)
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(1, activity.reloadCount);
}
public static class TestRealtimeProjectDetailActivity extends ProjectDetailActivity {
int reloadCount;