mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
220 lines
8.3 KiB
Java
220 lines
8.3 KiB
Java
package vip.mate.plugin.mem0;
|
|
|
|
import com.sun.net.httpserver.HttpExchange;
|
|
import com.sun.net.httpserver.HttpHandler;
|
|
import com.sun.net.httpserver.HttpServer;
|
|
import org.junit.jupiter.api.AfterEach;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.net.InetSocketAddress;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
class Mem0ProviderTest {
|
|
|
|
private HttpServer server;
|
|
private Mem0Provider provider;
|
|
private final AtomicInteger addCount = new AtomicInteger();
|
|
private final AtomicInteger searchCount = new AtomicInteger();
|
|
private final AtomicReference<String> lastAddBody = new AtomicReference<>();
|
|
|
|
@BeforeEach
|
|
void setUp() throws IOException {
|
|
addCount.set(0);
|
|
searchCount.set(0);
|
|
lastAddBody.set(null);
|
|
HttpHandler handler = this::handle;
|
|
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
|
server.createContext("/", handler);
|
|
server.start();
|
|
|
|
String baseUrl = "http://127.0.0.1:" + server.getAddress().getPort();
|
|
Mem0Config config = new Mem0Config(baseUrl, null, true, true, 3, 3000);
|
|
Mem0Client client = new Mem0Client(config);
|
|
provider = new Mem0Provider(config, client, LoggerFactory.getLogger("test"));
|
|
}
|
|
|
|
@AfterEach
|
|
void tearDown() {
|
|
if (server != null) server.stop(0);
|
|
}
|
|
|
|
private void handle(HttpExchange exchange) throws IOException {
|
|
String body;
|
|
try (InputStream in = exchange.getRequestBody()) {
|
|
body = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
|
}
|
|
String path = exchange.getRequestURI().getPath();
|
|
byte[] resp;
|
|
if ("/memories/".equals(path)) {
|
|
addCount.incrementAndGet();
|
|
lastAddBody.set(body);
|
|
resp = "{\"results\":[]}".getBytes(StandardCharsets.UTF_8);
|
|
} else if ("/memories/search/".equals(path)) {
|
|
searchCount.incrementAndGet();
|
|
resp = "{\"results\":[{\"id\":\"m1\",\"memory\":\"likes PostgreSQL\",\"score\":0.9}]}".getBytes(StandardCharsets.UTF_8);
|
|
} else {
|
|
resp = "{}".getBytes(StandardCharsets.UTF_8);
|
|
}
|
|
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
|
exchange.sendResponseHeaders(200, resp.length);
|
|
exchange.getResponseBody().write(resp);
|
|
exchange.close();
|
|
}
|
|
|
|
@Test
|
|
void id_isMem0() {
|
|
assertThat(provider.id()).isEqualTo("mem0");
|
|
}
|
|
|
|
@Test
|
|
void isAvailable_true_whenConfigUsableAndAtLeastOneFeatureEnabled() {
|
|
assertThat(provider.isAvailable()).isTrue();
|
|
}
|
|
|
|
@Test
|
|
void isAvailable_false_whenBaseUrlMissing() {
|
|
Mem0Config cfg = new Mem0Config(null, null, true, true, 5, 1000);
|
|
Mem0Provider p = new Mem0Provider(cfg, new Mem0Client(cfg), LoggerFactory.getLogger("test"));
|
|
assertThat(p.isAvailable()).isFalse();
|
|
}
|
|
|
|
@Test
|
|
void isAvailable_false_whenBothFeaturesDisabled() {
|
|
Mem0Config cfg = new Mem0Config("http://localhost:8080", null, false, false, 5, 1000);
|
|
Mem0Provider p = new Mem0Provider(cfg, new Mem0Client(cfg), LoggerFactory.getLogger("test"));
|
|
assertThat(p.isAvailable()).isFalse();
|
|
}
|
|
|
|
@Test
|
|
void systemPromptBlock_isEmpty() {
|
|
assertThat(provider.systemPromptBlock(1L)).isEmpty();
|
|
}
|
|
|
|
@Test
|
|
void twoArgPrefetch_returnsEmptyBecauseNoOwnerKey() {
|
|
// Without ownerKey, Mem0 cannot isolate per-user; provider skips.
|
|
assertThat(provider.prefetch(1L, "hello")).isEmpty();
|
|
assertThat(searchCount.get()).isZero();
|
|
}
|
|
|
|
@Test
|
|
void threeArgPrefetch_returnsRecallBlock() {
|
|
String result = provider.prefetch(1L, "what database", "user:42");
|
|
|
|
assertThat(result).startsWith("[Mem0 Recall");
|
|
assertThat(result).contains("likes PostgreSQL");
|
|
assertThat(searchCount.get()).isEqualTo(1);
|
|
}
|
|
|
|
@Test
|
|
void threeArgPrefetch_returnsEmptyWhenOwnerKeyBlank() {
|
|
assertThat(provider.prefetch(1L, "query", "")).isEmpty();
|
|
assertThat(provider.prefetch(1L, "query", null)).isEmpty();
|
|
assertThat(searchCount.get()).isZero();
|
|
}
|
|
|
|
@Test
|
|
void threeArgPrefetch_returnsEmptyWhenQueryBlank() {
|
|
assertThat(provider.prefetch(1L, "", "user:42")).isEmpty();
|
|
assertThat(provider.prefetch(1L, null, "user:42")).isEmpty();
|
|
assertThat(searchCount.get()).isZero();
|
|
}
|
|
|
|
@Test
|
|
void threeArgPrefetch_returnsEmptyOnServerError() {
|
|
// Replace handler to fail; the provider should swallow and return "".
|
|
server.removeContext("/");
|
|
server.createContext("/", ex -> {
|
|
ex.sendResponseHeaders(500, 0);
|
|
ex.close();
|
|
});
|
|
|
|
String result = provider.prefetch(1L, "q", "user:42");
|
|
assertThat(result).isEmpty();
|
|
}
|
|
|
|
@Test
|
|
void syncTurn_pushesAsynchronouslyWithOwnerKeyAsUserId() throws Exception {
|
|
provider.syncTurn(1L, "conv-1", "hello", "world", "user:42");
|
|
|
|
// Wait briefly for the async executor to fire the POST.
|
|
long deadline = System.currentTimeMillis() + 2000;
|
|
while (addCount.get() == 0 && System.currentTimeMillis() < deadline) {
|
|
Thread.sleep(20);
|
|
}
|
|
assertThat(addCount.get()).isEqualTo(1);
|
|
// The write must land under the same user_id that prefetch recalls by.
|
|
assertThat(lastAddBody.get()).contains("\"user_id\":\"user:42\"");
|
|
assertThat(lastAddBody.get()).contains("\"agent_id\":\"1\"");
|
|
}
|
|
|
|
@Test
|
|
void fourArgSyncTurn_skipsBecauseNoOwnerKey() throws Exception {
|
|
// Without ownerKey, a write would be keyed by an identifier that
|
|
// owner-scoped prefetch never queries; the provider must skip.
|
|
provider.syncTurn(1L, "conv-1", "hello", "world");
|
|
Thread.sleep(200); // give async a chance to (not) fire
|
|
assertThat(addCount.get()).isZero();
|
|
}
|
|
|
|
@Test
|
|
void syncTurn_skipsWhenOwnerKeyBlank() throws Exception {
|
|
provider.syncTurn(1L, "conv-1", "hello", "world", "");
|
|
provider.syncTurn(1L, "conv-1", "hello", "world", null);
|
|
Thread.sleep(200);
|
|
assertThat(addCount.get()).isZero();
|
|
}
|
|
|
|
@Test
|
|
void syncTurn_skipsWhenBothMessagesBlank() throws Exception {
|
|
provider.syncTurn(1L, "conv-1", " ", "", "user:42");
|
|
Thread.sleep(200); // give async a chance to (not) fire
|
|
assertThat(addCount.get()).isZero();
|
|
}
|
|
|
|
@Test
|
|
void syncTurn_failureIsSwallowedAndDoesNotThrow() throws Exception {
|
|
// Stop the server so the async POST fails; provider must not propagate.
|
|
server.stop(0);
|
|
// Re-create a stub server just so tearDown doesn't NPE; not listening
|
|
// on the original port anymore — the client will get connection refused.
|
|
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
|
server.createContext("/", ex -> { ex.sendResponseHeaders(200, 0); ex.close(); });
|
|
// Note: client still points at the old port → connection refused.
|
|
|
|
provider.syncTurn(1L, "conv-1", "hi", "there", "user:42");
|
|
Thread.sleep(500);
|
|
// No exception thrown; nothing to assert beyond "test didn't blow up".
|
|
}
|
|
|
|
@Test
|
|
void syncTurn_skippedWhenSyncDisabled() throws Exception {
|
|
// Build a provider with sync disabled.
|
|
Mem0Config cfg = new Mem0Config(
|
|
"http://127.0.0.1:" + server.getAddress().getPort(),
|
|
null, true, false, 3, 3000);
|
|
Mem0Provider p = new Mem0Provider(cfg, new Mem0Client(cfg), LoggerFactory.getLogger("test"));
|
|
p.syncTurn(1L, "conv-1", "hi", "there", "user:42");
|
|
Thread.sleep(200);
|
|
assertThat(addCount.get()).isZero();
|
|
}
|
|
|
|
@Test
|
|
void prefetch_skippedWhenSearchDisabled() {
|
|
Mem0Config cfg = new Mem0Config(
|
|
"http://127.0.0.1:" + server.getAddress().getPort(),
|
|
null, false, true, 3, 3000);
|
|
Mem0Provider p = new Mem0Provider(cfg, new Mem0Client(cfg), LoggerFactory.getLogger("test"));
|
|
assertThat(p.prefetch(1L, "q", "user:42")).isEmpty();
|
|
assertThat(searchCount.get()).isZero();
|
|
}
|
|
}
|