feat: add master-agent prompts and memory management
This commit is contained in:
@@ -109,6 +109,85 @@ public class BossApiClientDispatchPlansTest {
|
||||
assertEquals("{\"modelOverride\":\"gpt-5.4\",\"reasoningEffortOverride\":\"high\"}", connection.requestBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateProjectAgentControlsWritesPromptOverrideWhenProvided() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/agent-controls"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
BossApiClient.ApiResponse response = apiClient.updateProjectAgentControls("master-agent", "gpt-5.4", "high", "当前对话提示词");
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/master-agent/agent-controls", apiClient.lastPath);
|
||||
assertEquals("POST", connection.requestMethodValue);
|
||||
assertEquals(
|
||||
"{\"modelOverride\":\"gpt-5.4\",\"reasoningEffortOverride\":\"high\",\"promptOverride\":\"当前对话提示词\"}",
|
||||
connection.requestBody()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMasterAgentPromptProfileUsesScopedEndpoint() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/prompt-profile"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
BossApiClient.ApiResponse response = apiClient.getMasterAgentPromptProfile("master-agent");
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/master-agent/prompt-profile", apiClient.lastPath);
|
||||
assertEquals("GET", connection.requestMethodValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateMasterAgentPromptProfileWritesUserPromptAndOverride() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/prompt-profile"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("userPromptContent", "用户私有主提示词")
|
||||
.put("promptOverride", "当前对话提示词");
|
||||
BossApiClient.ApiResponse response = apiClient.updateMasterAgentPromptProfile("master-agent", payload);
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/master-agent/prompt-profile", apiClient.lastPath);
|
||||
assertEquals("POST", connection.requestMethodValue);
|
||||
assertEquals("{\"userPromptContent\":\"用户私有主提示词\",\"promptOverride\":\"当前对话提示词\"}", connection.requestBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMasterAgentMemoriesUsesScopedEndpoint() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/memories"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
BossApiClient.ApiResponse response = apiClient.getMasterAgentMemories("master-agent");
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/master-agent/memories", apiClient.lastPath);
|
||||
assertEquals("GET", connection.requestMethodValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMasterAgentMemoryWritesStructuredPayload() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/memories"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("scope", "project")
|
||||
.put("projectId", "boss-console")
|
||||
.put("title", "项目目标")
|
||||
.put("content", "把会话页收成微信式列表")
|
||||
.put("memoryType", "project_progress")
|
||||
.put("tags", new JSONArray().put("ui").put("progress"));
|
||||
BossApiClient.ApiResponse response = apiClient.createMasterAgentMemory("master-agent", payload);
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/master-agent/memories", apiClient.lastPath);
|
||||
assertEquals("POST", connection.requestMethodValue);
|
||||
assertEquals(
|
||||
"{\"scope\":\"project\",\"projectId\":\"boss-console\",\"title\":\"项目目标\",\"content\":\"把会话页收成微信式列表\",\"memoryType\":\"project_progress\",\"tags\":[\"ui\",\"progress\"]}",
|
||||
connection.requestBody()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendProjectMessageUsesQueueFriendlyReadTimeoutForMasterAgent() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/messages"));
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
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;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.AbstractExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class MasterAgentMemoryActivityTest {
|
||||
@Test
|
||||
public void renderMemoriesShowsGlobalAndProjectSections() throws Exception {
|
||||
TestMasterAgentMemoryActivity activity = Robolectric
|
||||
.buildActivity(
|
||||
TestMasterAgentMemoryActivity.class,
|
||||
new Intent()
|
||||
.putExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_ID, "master-agent")
|
||||
.putExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_NAME, "主 Agent")
|
||||
)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
JSONObject globalMemory = new JSONObject()
|
||||
.put("memoryId", "mem-global")
|
||||
.put("scope", "global")
|
||||
.put("title", "偏好")
|
||||
.put("content", "优先中文回复")
|
||||
.put("memoryType", "user_preference")
|
||||
.put("tags", new JSONArray().put("ui"));
|
||||
JSONObject projectMemory = new JSONObject()
|
||||
.put("memoryId", "mem-project")
|
||||
.put("scope", "project")
|
||||
.put("projectId", "master-agent")
|
||||
.put("title", "项目进度")
|
||||
.put("content", "主 Agent 对话链已接通")
|
||||
.put("memoryType", "project_progress")
|
||||
.put("tags", new JSONArray().put("progress"));
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("memories", new JSONObject()
|
||||
.put("global", new JSONObject().put("items", new JSONArray().put(globalMemory)))
|
||||
.put("project", new JSONObject().put("items", new JSONArray().put(projectMemory))));
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderMemories",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
|
||||
);
|
||||
|
||||
View content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content, "我的通用记忆"));
|
||||
assertTrue(viewTreeContainsText(content, "当前项目记忆"));
|
||||
assertTrue(viewTreeContainsText(content, "优先中文回复"));
|
||||
JSONObject memories = payload.getJSONObject("memories");
|
||||
JSONArray globalMemoryItems = (JSONArray) ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"extractMemoryItems",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, memories),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "global")
|
||||
);
|
||||
JSONArray projectMemoryItems = (JSONArray) ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"extractMemoryItems",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, memories),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "project")
|
||||
);
|
||||
assertEquals(1, globalMemoryItems.length());
|
||||
assertEquals(1, projectMemoryItems.length());
|
||||
assertEquals("偏好", globalMemoryItems.getJSONObject(0).getString("title"));
|
||||
assertEquals("项目进度", projectMemoryItems.getJSONObject(0).getString("title"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveMemoryWritesStructuredCreatePayload() throws Exception {
|
||||
TestMasterAgentMemoryActivity activity = Robolectric
|
||||
.buildActivity(
|
||||
TestMasterAgentMemoryActivity.class,
|
||||
new Intent()
|
||||
.putExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_ID, "master-agent")
|
||||
.putExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_NAME, "主 Agent")
|
||||
)
|
||||
.setup()
|
||||
.get();
|
||||
ReflectionHelpers.setField(activity, "contentLoaded", true);
|
||||
ReflectionHelpers.setField(activity, "apiClient", new ScriptedBossApiClient(
|
||||
new RecordingConnection(
|
||||
new URL("https://boss.hyzq.net/api/v1/projects/master-agent/memories"),
|
||||
200,
|
||||
"{\"ok\":true}",
|
||||
"{\"ok\":false,\"message\":\"MEMORY_SAVE_FAILED\"}"
|
||||
)
|
||||
));
|
||||
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
|
||||
ReflectionHelpers.setField(activity, "projectId", "master-agent");
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"saveMemory",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, null),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "project"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "项目目标"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "把会话页收成微信式列表"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "project_progress"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "ui,progress")
|
||||
);
|
||||
org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle();
|
||||
|
||||
assertEquals(
|
||||
"{\"scope\":\"project\",\"projectId\":\"master-agent\",\"title\":\"项目目标\",\"content\":\"把会话页收成微信式列表\",\"memoryType\":\"project_progress\",\"tags\":[\"ui\",\"progress\"]}",
|
||||
((ScriptedBossApiClient) ReflectionHelpers.getField(activity, "apiClient")).connection.requestBody()
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private static final class TestMasterAgentMemoryActivity extends MasterAgentMemoryActivity {
|
||||
@Override
|
||||
protected void reload() {
|
||||
// Tests render synthetic payloads directly.
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ScriptedBossApiClient extends BossApiClient {
|
||||
private final Map<String, RecordingConnection> connections;
|
||||
private final RecordingConnection connection;
|
||||
|
||||
private static final class InMemorySharedPreferences implements android.content.SharedPreferences {
|
||||
private final Map<String, Object> values = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Map<String, ?> getAll() {
|
||||
return new HashMap<>(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(String key, String defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof String ? (String) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Set<String> getStringSet(String key, java.util.Set<String> defValues) {
|
||||
Object value = values.get(key);
|
||||
if (!(value instanceof java.util.Set)) {
|
||||
return defValues;
|
||||
}
|
||||
// noinspection unchecked
|
||||
return (java.util.Set<String>) value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String key, int defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Integer ? (Integer) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(String key, long defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Long ? (Long) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(String key, float defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Float ? (Float) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Boolean ? (Boolean) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor edit() {
|
||||
return new Editor() {
|
||||
@Override public Editor putString(String key, String value) { values.put(key, value); return this; }
|
||||
@Override public Editor putStringSet(String key, java.util.Set<String> value) { values.put(key, value); return this; }
|
||||
@Override public Editor putInt(String key, int value) { values.put(key, value); return this; }
|
||||
@Override public Editor putLong(String key, long value) { values.put(key, value); return this; }
|
||||
@Override public Editor putFloat(String key, float value) { values.put(key, value); return this; }
|
||||
@Override public Editor putBoolean(String key, boolean value) { values.put(key, value); return this; }
|
||||
@Override public Editor remove(String key) { values.remove(key); return this; }
|
||||
@Override public Editor clear() { values.clear(); return this; }
|
||||
@Override public boolean commit() { return true; }
|
||||
@Override public void apply() {}
|
||||
};
|
||||
}
|
||||
|
||||
@Override public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
@Override public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
}
|
||||
|
||||
ScriptedBossApiClient(RecordingConnection connection) {
|
||||
super(new InMemorySharedPreferences(), "https://boss.hyzq.net");
|
||||
this.connection = connection;
|
||||
this.connections = new HashMap<>();
|
||||
this.connections.put(connection.getURL().getPath(), connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
HttpURLConnection openConnection(String path) {
|
||||
RecordingConnection scripted = connections.get(path);
|
||||
if (scripted == null) {
|
||||
throw new IllegalStateException("Missing scripted connection for " + path);
|
||||
}
|
||||
return scripted;
|
||||
}
|
||||
|
||||
@Override
|
||||
String encode(String value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
void rememberIdentity(JSONObject json) {
|
||||
// JVM 单测不需要落 Android 侧身份缓存。
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InMemorySharedPreferences implements android.content.SharedPreferences {
|
||||
private final Map<String, Object> values = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Map<String, ?> getAll() {
|
||||
return new HashMap<>(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(String key, String defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof String ? (String) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Set<String> getStringSet(String key, java.util.Set<String> defValues) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof java.util.Set ? (java.util.Set<String>) value : defValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String key, int defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Integer ? (Integer) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(String key, long defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Long ? (Long) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(String key, float defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Float ? (Float) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Boolean ? (Boolean) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor edit() {
|
||||
return new Editor() {
|
||||
@Override
|
||||
public Editor putString(String key, String value) { values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putStringSet(String key, java.util.Set<String> values) { InMemorySharedPreferences.this.values.put(key, values); return this; }
|
||||
@Override
|
||||
public Editor putInt(String key, int value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putLong(String key, long value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putFloat(String key, float value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putBoolean(String key, boolean value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor remove(String key) { InMemorySharedPreferences.this.values.remove(key); return this; }
|
||||
@Override
|
||||
public Editor clear() { InMemorySharedPreferences.this.values.clear(); return this; }
|
||||
@Override
|
||||
public boolean commit() { return true; }
|
||||
@Override
|
||||
public void apply() {}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
|
||||
@Override
|
||||
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
}
|
||||
|
||||
private static final class RecordingConnection extends HttpURLConnection {
|
||||
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream();
|
||||
private final Map<String, String> requestHeaders = new HashMap<>();
|
||||
private final int responseCodeValue;
|
||||
private final String responseBody;
|
||||
private final String errorBody;
|
||||
private String requestMethodValue = "GET";
|
||||
|
||||
RecordingConnection(URL url, int responseCodeValue, String responseBody, String errorBody) {
|
||||
super(url);
|
||||
this.responseCodeValue = responseCodeValue;
|
||||
this.responseBody = responseBody;
|
||||
this.errorBody = errorBody;
|
||||
}
|
||||
|
||||
@Override public void disconnect() {}
|
||||
@Override public boolean usingProxy() { return false; }
|
||||
@Override public void connect() {}
|
||||
@Override public void setRequestMethod(String method) throws ProtocolException { requestMethodValue = method; }
|
||||
@Override public void setRequestProperty(String key, String value) { requestHeaders.put(key, value); }
|
||||
@Override public OutputStream getOutputStream() { return requestBody; }
|
||||
@Override public int getResponseCode() { return responseCodeValue; }
|
||||
@Override public InputStream getInputStream() { return new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8)); }
|
||||
@Override public InputStream getErrorStream() {
|
||||
if (responseCodeValue < 400) {
|
||||
return null;
|
||||
}
|
||||
return new ByteArrayInputStream(errorBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
String requestBody() { return requestBody.toString(StandardCharsets.UTF_8); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
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;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.AbstractExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class MasterAgentPromptActivityTest {
|
||||
@Test
|
||||
public void renderPromptProfileShowsAdminUserAndConversationLayers() throws Exception {
|
||||
TestMasterAgentPromptActivity activity = Robolectric
|
||||
.buildActivity(
|
||||
TestMasterAgentPromptActivity.class,
|
||||
new Intent()
|
||||
.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_ID, "master-agent")
|
||||
.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_NAME, "主 Agent")
|
||||
)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("promptPolicy", new JSONObject().put("globalPrompt", "全局主提示词"))
|
||||
.put("userPrompt", new JSONObject().put("content", "用户私有主提示词"))
|
||||
.put("projectControls", new JSONObject().put("promptOverride", "当前对话提示词"));
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderPromptProfile",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
|
||||
);
|
||||
|
||||
View content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content, "管理员全局主提示词"));
|
||||
assertTrue(viewTreeContainsText(content, "全局主提示词"));
|
||||
assertTrue(viewTreeContainsText(content, "用户私有主提示词"));
|
||||
assertTrue(viewTreeContainsText(content, "当前对话提示词"));
|
||||
assertTrue(viewTreeContainsText(content, "合成预览"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void savePromptProfileWritesBothEditableLayers() throws Exception {
|
||||
TestMasterAgentPromptActivity activity = Robolectric
|
||||
.buildActivity(
|
||||
TestMasterAgentPromptActivity.class,
|
||||
new Intent()
|
||||
.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_ID, "master-agent")
|
||||
.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_NAME, "主 Agent")
|
||||
)
|
||||
.setup()
|
||||
.get();
|
||||
ReflectionHelpers.setField(activity, "contentLoaded", true);
|
||||
ReflectionHelpers.setField(activity, "apiClient", new ScriptedBossApiClient(
|
||||
new RecordingConnection(
|
||||
new URL("https://boss.hyzq.net/api/v1/projects/master-agent/prompt-profile"),
|
||||
200,
|
||||
"{\"ok\":true,\"promptPolicy\":{\"globalPrompt\":\"全局主提示词\"},\"userPrompt\":{\"content\":\"用户私有主提示词\"},\"projectControls\":{\"promptOverride\":\"当前对话提示词\"}}",
|
||||
"{\"ok\":false,\"message\":\"PROMPT_SAVE_FAILED\"}"
|
||||
)
|
||||
));
|
||||
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("promptPolicy", new JSONObject().put("globalPrompt", "全局主提示词"))
|
||||
.put("userPrompt", new JSONObject().put("content", "用户私有主提示词"))
|
||||
.put("projectControls", new JSONObject().put("promptOverride", "当前对话提示词"));
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderPromptProfile",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
|
||||
);
|
||||
|
||||
EditText userInput = ReflectionHelpers.getField(activity, "userPromptInput");
|
||||
EditText conversationInput = ReflectionHelpers.getField(activity, "projectPromptInput");
|
||||
userInput.setText("更新后的用户提示词");
|
||||
conversationInput.setText("更新后的对话提示词");
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(activity, "savePromptProfile");
|
||||
org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle();
|
||||
|
||||
assertEquals(
|
||||
"{\"userPromptContent\":\"更新后的用户提示词\",\"promptOverride\":\"更新后的对话提示词\"}",
|
||||
((ScriptedBossApiClient) ReflectionHelpers.getField(activity, "apiClient")).connection.requestBody()
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private static final class TestMasterAgentPromptActivity extends MasterAgentPromptActivity {
|
||||
@Override
|
||||
protected void reload() {
|
||||
// Tests render synthetic payloads directly.
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ScriptedBossApiClient extends BossApiClient {
|
||||
private final Map<String, RecordingConnection> connections;
|
||||
private final RecordingConnection connection;
|
||||
|
||||
ScriptedBossApiClient(RecordingConnection connection) {
|
||||
super(new InMemorySharedPreferences(), "https://boss.hyzq.net");
|
||||
this.connection = connection;
|
||||
this.connections = new HashMap<>();
|
||||
this.connections.put(connection.getURL().getPath(), connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
HttpURLConnection openConnection(String path) {
|
||||
RecordingConnection scripted = connections.get(path);
|
||||
if (scripted == null) {
|
||||
throw new IllegalStateException("Missing scripted connection for " + path);
|
||||
}
|
||||
return scripted;
|
||||
}
|
||||
|
||||
@Override
|
||||
String encode(String value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
void rememberIdentity(JSONObject json) {
|
||||
// JVM 单测不需要落 Android 侧身份缓存。
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InMemorySharedPreferences implements android.content.SharedPreferences {
|
||||
private final Map<String, Object> values = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Map<String, ?> getAll() {
|
||||
return new HashMap<>(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(String key, String defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof String ? (String) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getStringSet(String key, Set<String> defValues) {
|
||||
Object value = values.get(key);
|
||||
if (!(value instanceof Set)) {
|
||||
return defValues;
|
||||
}
|
||||
// noinspection unchecked
|
||||
return (Set<String>) value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String key, int defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Integer ? (Integer) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(String key, long defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Long ? (Long) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(String key, float defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Float ? (Float) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Boolean ? (Boolean) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor edit() {
|
||||
return new Editor() {
|
||||
@Override
|
||||
public Editor putString(String key, String value) { values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putStringSet(String key, java.util.Set<String> values) { InMemorySharedPreferences.this.values.put(key, values); return this; }
|
||||
@Override
|
||||
public Editor putInt(String key, int value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putLong(String key, long value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putFloat(String key, float value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putBoolean(String key, boolean value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor remove(String key) { InMemorySharedPreferences.this.values.remove(key); return this; }
|
||||
@Override
|
||||
public Editor clear() { InMemorySharedPreferences.this.values.clear(); return this; }
|
||||
@Override
|
||||
public boolean commit() { return true; }
|
||||
@Override
|
||||
public void apply() {}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
|
||||
@Override
|
||||
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
}
|
||||
|
||||
private static final class RecordingConnection extends HttpURLConnection {
|
||||
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream();
|
||||
private final Map<String, String> requestHeaders = new HashMap<>();
|
||||
private final int responseCodeValue;
|
||||
private final String responseBody;
|
||||
private final String errorBody;
|
||||
private String requestMethodValue = "GET";
|
||||
|
||||
RecordingConnection(URL url, int responseCodeValue, String responseBody, String errorBody) {
|
||||
super(url);
|
||||
this.responseCodeValue = responseCodeValue;
|
||||
this.responseBody = responseBody;
|
||||
this.errorBody = errorBody;
|
||||
}
|
||||
|
||||
@Override public void disconnect() {}
|
||||
@Override public boolean usingProxy() { return false; }
|
||||
@Override public void connect() {}
|
||||
@Override public void setRequestMethod(String method) throws ProtocolException { requestMethodValue = method; }
|
||||
@Override public void setRequestProperty(String key, String value) { requestHeaders.put(key, value); }
|
||||
@Override public OutputStream getOutputStream() { return requestBody; }
|
||||
@Override public int getResponseCode() { return responseCodeValue; }
|
||||
@Override public InputStream getInputStream() { return new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8)); }
|
||||
@Override public InputStream getErrorStream() {
|
||||
if (responseCodeValue < 400) {
|
||||
return null;
|
||||
}
|
||||
return new ByteArrayInputStream(errorBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
String requestBody() { return requestBody.toString(StandardCharsets.UTF_8); }
|
||||
}
|
||||
}
|
||||
@@ -44,8 +44,10 @@ public class ProjectDetailActivityMasterAgentMenuTest {
|
||||
|
||||
assertMenuItem(listView, 0, "模型");
|
||||
assertMenuItem(listView, 1, "推理强度");
|
||||
assertMenuItem(listView, 2, "会话信息");
|
||||
assertMenuItem(listView, 3, "刷新");
|
||||
assertMenuItem(listView, 2, "提示词");
|
||||
assertMenuItem(listView, 3, "记忆");
|
||||
assertMenuItem(listView, 4, "会话信息");
|
||||
assertMenuItem(listView, 5, "刷新");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user