Refresh goals and versions on precise goal updates
This commit is contained in:
@@ -12,12 +12,19 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ProjectGoalsActivity extends BossScreenActivity {
|
||||
public static final String EXTRA_PROJECT_ID = "project_id";
|
||||
public static final String EXTRA_PROJECT_NAME = "project_name";
|
||||
private static final String GOAL_REFRESH_NOTE = "project_goals.updated";
|
||||
private static final long REALTIME_RELOAD_THROTTLE_MS = 900L;
|
||||
|
||||
private String projectId;
|
||||
private String projectName;
|
||||
private @Nullable BossRealtimeClient realtimeClient;
|
||||
private final Map<String, Long> recentRealtimeEventTimestamps = new LinkedHashMap<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
@@ -26,11 +33,35 @@ public class ProjectGoalsActivity extends BossScreenActivity {
|
||||
projectName = getIntent().getStringExtra(EXTRA_PROJECT_NAME);
|
||||
configureScreen("项目目标", projectName == null ? "原生目标清单" : projectName);
|
||||
setHeaderAction("编辑目标", v -> openGoalEditor(null, ""));
|
||||
realtimeClient = new BossRealtimeClient(apiClient, this::handleRealtimeEvent);
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
updateRealtimeSubscription();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
stopRealtimeUpdates();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
stopRealtimeUpdates();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
if (projectId == null || projectId.isEmpty()) {
|
||||
replaceContent(BossUi.buildEmptyCard(this, "缺少 projectId。"));
|
||||
setRefreshing(false);
|
||||
return;
|
||||
}
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
@@ -46,6 +77,70 @@ public class ProjectGoalsActivity extends BossScreenActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void updateRealtimeSubscription() {
|
||||
if (apiClient != null && apiClient.hasSessionHints() && realtimeClient != null) {
|
||||
realtimeClient.start();
|
||||
return;
|
||||
}
|
||||
stopRealtimeUpdates();
|
||||
}
|
||||
|
||||
private void stopRealtimeUpdates() {
|
||||
if (realtimeClient != null) {
|
||||
realtimeClient.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void handleRealtimeEvent(BossRealtimeEvent event) {
|
||||
if (event == null || event.eventName.isEmpty() || projectId == null || projectId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!shouldReloadForRealtimeEvent(event)) {
|
||||
return;
|
||||
}
|
||||
String eventFingerprint = BossRealtimeClient.buildEventFingerprint(event);
|
||||
if (eventFingerprint.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
|
||||
return;
|
||||
}
|
||||
runOnUiThread(this::reload);
|
||||
}
|
||||
|
||||
private boolean shouldReloadForRealtimeEvent(BossRealtimeEvent event) {
|
||||
if (!"conversation.updated".equals(event.eventName)) {
|
||||
return false;
|
||||
}
|
||||
String payloadProjectId = event.payload.optString("projectId", "").trim();
|
||||
if (payloadProjectId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String payloadNote = event.payload.optString("note", "").trim();
|
||||
return payloadProjectId.equals(projectId) && GOAL_REFRESH_NOTE.equals(payloadNote);
|
||||
}
|
||||
|
||||
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
|
||||
pruneRecentRealtimeEvents(now);
|
||||
Long previousEventAt = recentRealtimeEventTimestamps.get(eventFingerprint);
|
||||
if (previousEventAt != null && now - previousEventAt < REALTIME_RELOAD_THROTTLE_MS) {
|
||||
return true;
|
||||
}
|
||||
recentRealtimeEventTimestamps.put(eventFingerprint, now);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void pruneRecentRealtimeEvents(long now) {
|
||||
java.util.Iterator<Map.Entry<String, Long>> iterator = recentRealtimeEventTimestamps.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, Long> entry = iterator.next();
|
||||
if (now - entry.getValue() >= REALTIME_RELOAD_THROTTLE_MS) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void renderGoals(@Nullable JSONObject project) {
|
||||
replaceContent();
|
||||
if (project == null) {
|
||||
|
||||
@@ -7,11 +7,18 @@ import androidx.annotation.Nullable;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ProjectVersionsActivity extends BossScreenActivity {
|
||||
public static final String EXTRA_PROJECT_ID = "project_id";
|
||||
public static final String EXTRA_PROJECT_NAME = "project_name";
|
||||
private static final String GOAL_REFRESH_NOTE = "project_goals.updated";
|
||||
private static final long REALTIME_RELOAD_THROTTLE_MS = 900L;
|
||||
|
||||
private String projectId;
|
||||
private @Nullable BossRealtimeClient realtimeClient;
|
||||
private final Map<String, Long> recentRealtimeEventTimestamps = new LinkedHashMap<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
@@ -19,11 +26,35 @@ public class ProjectVersionsActivity extends BossScreenActivity {
|
||||
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
|
||||
configureScreen("版本迭代记录", getIntent().getStringExtra(EXTRA_PROJECT_NAME));
|
||||
setHeaderAction("只读", v -> showMessage("版本记录只读"));
|
||||
realtimeClient = new BossRealtimeClient(apiClient, this::handleRealtimeEvent);
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
updateRealtimeSubscription();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
stopRealtimeUpdates();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
stopRealtimeUpdates();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
if (projectId == null || projectId.isEmpty()) {
|
||||
replaceContent(BossUi.buildEmptyCard(this, "缺少 projectId。"));
|
||||
setRefreshing(false);
|
||||
return;
|
||||
}
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
@@ -39,6 +70,70 @@ public class ProjectVersionsActivity extends BossScreenActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void updateRealtimeSubscription() {
|
||||
if (apiClient != null && apiClient.hasSessionHints() && realtimeClient != null) {
|
||||
realtimeClient.start();
|
||||
return;
|
||||
}
|
||||
stopRealtimeUpdates();
|
||||
}
|
||||
|
||||
private void stopRealtimeUpdates() {
|
||||
if (realtimeClient != null) {
|
||||
realtimeClient.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void handleRealtimeEvent(BossRealtimeEvent event) {
|
||||
if (event == null || event.eventName.isEmpty() || projectId == null || projectId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!shouldReloadForRealtimeEvent(event)) {
|
||||
return;
|
||||
}
|
||||
String eventFingerprint = BossRealtimeClient.buildEventFingerprint(event);
|
||||
if (eventFingerprint.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
|
||||
return;
|
||||
}
|
||||
runOnUiThread(this::reload);
|
||||
}
|
||||
|
||||
private boolean shouldReloadForRealtimeEvent(BossRealtimeEvent event) {
|
||||
if (!"conversation.updated".equals(event.eventName)) {
|
||||
return false;
|
||||
}
|
||||
String payloadProjectId = event.payload.optString("projectId", "").trim();
|
||||
if (payloadProjectId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String payloadNote = event.payload.optString("note", "").trim();
|
||||
return payloadProjectId.equals(projectId) && GOAL_REFRESH_NOTE.equals(payloadNote);
|
||||
}
|
||||
|
||||
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
|
||||
pruneRecentRealtimeEvents(now);
|
||||
Long previousEventAt = recentRealtimeEventTimestamps.get(eventFingerprint);
|
||||
if (previousEventAt != null && now - previousEventAt < REALTIME_RELOAD_THROTTLE_MS) {
|
||||
return true;
|
||||
}
|
||||
recentRealtimeEventTimestamps.put(eventFingerprint, now);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void pruneRecentRealtimeEvents(long now) {
|
||||
java.util.Iterator<Map.Entry<String, Long>> iterator = recentRealtimeEventTimestamps.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, Long> entry = iterator.next();
|
||||
if (now - entry.getValue() >= REALTIME_RELOAD_THROTTLE_MS) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void renderVersions(@Nullable JSONObject project) {
|
||||
replaceContent(BossUi.buildCard(
|
||||
this,
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import org.json.JSONObject;
|
||||
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 java.lang.reflect.Method;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class ProjectGoalsActivityTest {
|
||||
@Test
|
||||
public void matchingConversationUpdatedEventTriggersReload() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectGoalsActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ProjectGoalsActivity.EXTRA_PROJECT_NAME, "树莓派二代接入");
|
||||
TestProjectGoalsActivity activity = Robolectric
|
||||
.buildActivity(TestProjectGoalsActivity.class, intent)
|
||||
.setup()
|
||||
.resume()
|
||||
.get();
|
||||
activity.reloadEnabled = true;
|
||||
activity.reloadCount = 0;
|
||||
|
||||
Method handleRealtimeEvent = findHandleRealtimeEvent();
|
||||
assertNotNull(handleRealtimeEvent);
|
||||
|
||||
handleRealtimeEvent.invoke(
|
||||
activity,
|
||||
new BossRealtimeEvent(
|
||||
"conversation.updated",
|
||||
new JSONObject()
|
||||
.put("projectId", "project-1")
|
||||
.put("note", "project_goals.updated")
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(1, activity.reloadCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameProjectNonGoalEventDoesNotTriggerReload() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectGoalsActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ProjectGoalsActivity.EXTRA_PROJECT_NAME, "树莓派二代接入");
|
||||
TestProjectGoalsActivity activity = Robolectric
|
||||
.buildActivity(TestProjectGoalsActivity.class, intent)
|
||||
.setup()
|
||||
.resume()
|
||||
.get();
|
||||
activity.reloadEnabled = true;
|
||||
activity.reloadCount = 0;
|
||||
|
||||
Method handleRealtimeEvent = findHandleRealtimeEvent();
|
||||
assertNotNull(handleRealtimeEvent);
|
||||
|
||||
handleRealtimeEvent.invoke(
|
||||
activity,
|
||||
new BossRealtimeEvent(
|
||||
"conversation.updated",
|
||||
new JSONObject()
|
||||
.put("projectId", "project-1")
|
||||
.put("note", "dispatch_execution.updated")
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(0, activity.reloadCount);
|
||||
}
|
||||
|
||||
private static Method findHandleRealtimeEvent() {
|
||||
for (Method method : ProjectGoalsActivity.class.getDeclaredMethods()) {
|
||||
if ("handleRealtimeEvent".equals(method.getName())
|
||||
&& method.getParameterTypes().length == 1
|
||||
&& method.getParameterTypes()[0] == BossRealtimeEvent.class) {
|
||||
method.setAccessible(true);
|
||||
return method;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class TestProjectGoalsActivity extends ProjectGoalsActivity {
|
||||
private boolean reloadEnabled;
|
||||
private int reloadCount;
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
if (!reloadEnabled) {
|
||||
return;
|
||||
}
|
||||
reloadCount += 1;
|
||||
setRefreshing(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import org.json.JSONObject;
|
||||
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 java.lang.reflect.Method;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class ProjectVersionsActivityTest {
|
||||
@Test
|
||||
public void matchingGoalRefreshMarkerTriggersReload() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectVersionsActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ProjectVersionsActivity.EXTRA_PROJECT_NAME, "树莓派二代接入");
|
||||
TestProjectVersionsActivity activity = Robolectric
|
||||
.buildActivity(TestProjectVersionsActivity.class, intent)
|
||||
.setup()
|
||||
.resume()
|
||||
.get();
|
||||
activity.reloadEnabled = true;
|
||||
activity.reloadCount = 0;
|
||||
|
||||
Method handleRealtimeEvent = findHandleRealtimeEvent();
|
||||
assertNotNull(handleRealtimeEvent);
|
||||
|
||||
handleRealtimeEvent.invoke(
|
||||
activity,
|
||||
new BossRealtimeEvent(
|
||||
"conversation.updated",
|
||||
new JSONObject()
|
||||
.put("projectId", "project-1")
|
||||
.put("note", "project_goals.updated")
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(1, activity.reloadCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameProjectNonGoalEventDoesNotTriggerReload() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectVersionsActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ProjectVersionsActivity.EXTRA_PROJECT_NAME, "树莓派二代接入");
|
||||
TestProjectVersionsActivity activity = Robolectric
|
||||
.buildActivity(TestProjectVersionsActivity.class, intent)
|
||||
.setup()
|
||||
.resume()
|
||||
.get();
|
||||
activity.reloadEnabled = true;
|
||||
activity.reloadCount = 0;
|
||||
|
||||
Method handleRealtimeEvent = findHandleRealtimeEvent();
|
||||
assertNotNull(handleRealtimeEvent);
|
||||
|
||||
handleRealtimeEvent.invoke(
|
||||
activity,
|
||||
new BossRealtimeEvent(
|
||||
"conversation.updated",
|
||||
new JSONObject()
|
||||
.put("projectId", "project-1")
|
||||
.put("note", "dispatch_execution.updated")
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(0, activity.reloadCount);
|
||||
}
|
||||
|
||||
private static Method findHandleRealtimeEvent() {
|
||||
for (Method method : ProjectVersionsActivity.class.getDeclaredMethods()) {
|
||||
if ("handleRealtimeEvent".equals(method.getName())
|
||||
&& method.getParameterTypes().length == 1
|
||||
&& method.getParameterTypes()[0] == BossRealtimeEvent.class) {
|
||||
method.setAccessible(true);
|
||||
return method;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class TestProjectVersionsActivity extends ProjectVersionsActivity {
|
||||
private boolean reloadEnabled;
|
||||
private int reloadCount;
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
if (!reloadEnabled) {
|
||||
return;
|
||||
}
|
||||
reloadCount += 1;
|
||||
setRefreshing(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"fileName": "boss-android-v2.5.11-release.apk",
|
||||
"urlPath": "/api/v1/user/ota/package",
|
||||
"sizeBytes": 3355587,
|
||||
"updatedAt": "2026-04-07T08:13:31Z",
|
||||
"sha256": "0a94ce924344c02914f78414ad6bb5276126124e8eae123508a52e2c33aa86d7",
|
||||
"sizeBytes": 3356442,
|
||||
"updatedAt": "2026-04-07T08:39:26Z",
|
||||
"sha256": "292abbd4f6e7953b3e8d8f195ee1c463d16e973a2d81e7a9760a71951167d47a",
|
||||
"versionName": "2.5.11",
|
||||
"versionCode": 24,
|
||||
"buildFlavor": "release"
|
||||
|
||||
Binary file not shown.
@@ -5040,7 +5040,7 @@ export async function getPreferredDeviceIdForAccount(
|
||||
}
|
||||
|
||||
export async function toggleGoal(projectId: string, goalId: string) {
|
||||
return mutateState((state) => {
|
||||
const goal = await mutateState((state) => {
|
||||
const project = state.projects.find((item) => item.id === projectId);
|
||||
if (!project) throw new Error("PROJECT_NOT_FOUND");
|
||||
|
||||
@@ -5062,10 +5062,12 @@ export async function toggleGoal(projectId: string, goalId: string) {
|
||||
project.lastMessageAt = nowIso();
|
||||
return goal;
|
||||
});
|
||||
publishBossEvent("conversation.updated", { projectId, note: "project_goals.updated" });
|
||||
return goal;
|
||||
}
|
||||
|
||||
export async function updateGoalText(projectId: string, goalId: string, text: string) {
|
||||
return mutateState((state) => {
|
||||
const goal = await mutateState((state) => {
|
||||
const project = state.projects.find((item) => item.id === projectId);
|
||||
if (!project) throw new Error("PROJECT_NOT_FOUND");
|
||||
|
||||
@@ -5078,10 +5080,12 @@ export async function updateGoalText(projectId: string, goalId: string, text: st
|
||||
project.lastMessageAt = nowIso();
|
||||
return goal;
|
||||
});
|
||||
publishBossEvent("conversation.updated", { projectId, note: "project_goals.updated" });
|
||||
return goal;
|
||||
}
|
||||
|
||||
export async function createGoal(projectId: string, text: string) {
|
||||
return mutateState((state) => {
|
||||
const goal = await mutateState((state) => {
|
||||
const project = state.projects.find((item) => item.id === projectId);
|
||||
if (!project) throw new Error("PROJECT_NOT_FOUND");
|
||||
if (!text.trim()) throw new Error("GOAL_TEXT_REQUIRED");
|
||||
@@ -5097,6 +5101,8 @@ export async function createGoal(projectId: string, text: string) {
|
||||
project.lastMessageAt = nowIso();
|
||||
return goal;
|
||||
});
|
||||
publishBossEvent("conversation.updated", { projectId, note: "project_goals.updated" });
|
||||
return goal;
|
||||
}
|
||||
|
||||
export async function issueVerificationCode(
|
||||
|
||||
113
tests/project-goal-events.test.ts
Normal file
113
tests/project-goal-events.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
|
||||
let runtimeRoot = "";
|
||||
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
||||
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
|
||||
let toggleGoal: (typeof import("../src/lib/boss-data"))["toggleGoal"];
|
||||
let updateGoalText: (typeof import("../src/lib/boss-data"))["updateGoalText"];
|
||||
let createGoal: (typeof import("../src/lib/boss-data"))["createGoal"];
|
||||
let subscribeBossEvents: (typeof import("../src/lib/boss-events"))["subscribeBossEvents"];
|
||||
|
||||
async function setup() {
|
||||
if (runtimeRoot) return;
|
||||
|
||||
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-goal-events-"));
|
||||
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
||||
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
||||
|
||||
const [data, events] = await Promise.all([
|
||||
import("../src/lib/boss-data.ts"),
|
||||
import("../src/lib/boss-events.ts"),
|
||||
]);
|
||||
readState = data.readState;
|
||||
writeState = data.writeState;
|
||||
toggleGoal = data.toggleGoal;
|
||||
updateGoalText = data.updateGoalText;
|
||||
createGoal = data.createGoal;
|
||||
subscribeBossEvents = events.subscribeBossEvents;
|
||||
}
|
||||
|
||||
async function resetGoalState() {
|
||||
const state = await readState();
|
||||
const existingProject = state.projects.find((project) => project.id !== "master-agent") ?? state.projects[0];
|
||||
const project = structuredClone(existingProject);
|
||||
project.id = "project-goal-events";
|
||||
project.name = "项目目标事件测试";
|
||||
project.goals = [
|
||||
{
|
||||
id: "goal-1",
|
||||
text: "完成接入验证",
|
||||
state: "pending",
|
||||
note: "等待执行",
|
||||
},
|
||||
];
|
||||
project.versions = [];
|
||||
project.messages = [];
|
||||
project.lastMessageAt = "2026-04-07T10:00:00.000Z";
|
||||
state.projects = state.projects.filter((item) => item.id !== "project-goal-events");
|
||||
state.projects.unshift(project);
|
||||
await writeState(state);
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await setup();
|
||||
await resetGoalState();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
if (runtimeRoot) {
|
||||
await rm(runtimeRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("toggleGoal publishes project goal refresh marker for the project", async () => {
|
||||
const events: Array<{ event: string; payload: { projectId?: string; note?: string } }> = [];
|
||||
const unsubscribe = subscribeBossEvents((event, payload) => {
|
||||
events.push({ event, payload });
|
||||
});
|
||||
|
||||
await toggleGoal("project-goal-events", "goal-1");
|
||||
unsubscribe();
|
||||
|
||||
const latest = events.at(-1);
|
||||
assert.ok(latest);
|
||||
assert.equal(latest.event, "conversation.updated");
|
||||
assert.equal(latest.payload.projectId, "project-goal-events");
|
||||
assert.equal(latest.payload.note, "project_goals.updated");
|
||||
});
|
||||
|
||||
test("updateGoalText publishes project goal refresh marker for the project", async () => {
|
||||
const events: Array<{ event: string; payload: { projectId?: string; note?: string } }> = [];
|
||||
const unsubscribe = subscribeBossEvents((event, payload) => {
|
||||
events.push({ event, payload });
|
||||
});
|
||||
|
||||
await updateGoalText("project-goal-events", "goal-1", "完成接入验证并复盘");
|
||||
unsubscribe();
|
||||
|
||||
const latest = events.at(-1);
|
||||
assert.ok(latest);
|
||||
assert.equal(latest.event, "conversation.updated");
|
||||
assert.equal(latest.payload.projectId, "project-goal-events");
|
||||
assert.equal(latest.payload.note, "project_goals.updated");
|
||||
});
|
||||
|
||||
test("createGoal publishes project goal refresh marker for the project", async () => {
|
||||
const events: Array<{ event: string; payload: { projectId?: string; note?: string } }> = [];
|
||||
const unsubscribe = subscribeBossEvents((event, payload) => {
|
||||
events.push({ event, payload });
|
||||
});
|
||||
|
||||
await createGoal("project-goal-events", "补一条回归目标");
|
||||
unsubscribe();
|
||||
|
||||
const latest = events.at(-1);
|
||||
assert.ok(latest);
|
||||
assert.equal(latest.event, "conversation.updated");
|
||||
assert.equal(latest.payload.projectId, "project-goal-events");
|
||||
assert.equal(latest.payload.note, "project_goals.updated");
|
||||
});
|
||||
Reference in New Issue
Block a user