diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..f7c3be7
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,34 @@
+name: CI
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Check Formatting (Spotless)
+ run: mvn spotless:check
+
+ - name: Build and Verify (Tests + Coverage)
+ run: mvn clean verify
+
+ - name: Upload Coverage Reports
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: jacoco-report
+ path: target/site/jacoco/
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4347e3c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+target/
+*.class
+.idea/
+*.iml
+.vscode/
+*.log
+.DS_Store
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..aeb13c2
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,47 @@
+# Contributing to MemU Java SDK
+
+Thank you for your interest in contributing to the MemU Java SDK! We welcome contributions from the community.
+
+## Development Setup
+
+1. **Java Version**: Ensure you have Java 21 installed.
+2. **Maven**: This project uses Maven for dependency management and building.
+
+## Coding Style
+
+We use **Google Java Format** to maintain code consistency. This is enforced via the [Spotless Maven Plugin](https://github.com/diffplug/spotless).
+
+Before committing your changes, please run the following command to automatically format your code:
+
+```bash
+mvn spotless:apply
+```
+
+If you do not run this, the CI build will fail during the formatting check.
+
+## Running Tests
+
+To run the unit and integration tests, use the standard Maven test command:
+
+```bash
+mvn test
+```
+
+To run the full verification suite (tests + code coverage + formatting check), run:
+
+```bash
+mvn clean verify
+```
+
+## Pull Requests
+
+1. Fork the repository.
+2. Create a feature branch (`git checkout -b feature/amazing-feature`).
+3. Commit your changes.
+4. Run `mvn spotless:apply` and `mvn verify` to ensure everything is correct.
+5. Push to the branch (`git push origin feature/amazing-feature`).
+6. Open a Pull Request.
+
+## Code of Conduct
+
+Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.
diff --git a/README.md b/README.md
index f712014..cc2f391 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,96 @@
-# memU-sdk-java
+# MemU Java SDK
+
+
+
+
+
+The official Java SDK for MemU, an advanced memory storage and retrieval API for AI applications.
+
+## Installation
+
+Add the following dependency to your `pom.xml`:
+
+```xml
+
+ com.nevamind.ai
+ memu-sdk-java
+ 0.1.0-SNAPSHOT
+
+```
+
+## Quick Start
+
+Here is a complete example of how to initialize the client, create a memory, and search for it.
+
+```java
+import com.nevamind.memu.MemUClient;
+import com.nevamind.memu.model.*;
+import java.util.List;
+
+public class MemUExample {
+ public static void main(String[] args) {
+ // 1. Initialize the client
+ String baseUrl = "https://api.memu.ai";
+ String apiKey = System.getenv("MEMU_API_KEY");
+
+ MemUClient client = new MemUClient(baseUrl, apiKey);
+
+ // 2. Create a Memory
+ MemoryItem memory = MemoryItem.builder()
+ .summary("The user prefers dark mode in their IDE.")
+ .memoryType(MemoryType.preference)
+ .build();
+
+ try {
+ boolean success = client.createMemory(memory);
+ System.out.println("Memory created: " + success);
+ } catch (Exception e) {
+ System.err.println("Failed to create memory: " + e.getMessage());
+ }
+
+ // 3. Search for Memories
+ SearchRequest request = SearchRequest.builder()
+ .queries(List.of("What IDE theme does the user like?"))
+ .limit(3)
+ .build();
+
+ try {
+ SearchResponse response = client.search(request);
+ response.getResults().forEach(result -> {
+ System.out.println("Found: " + result.getContent() + " (Score: " + result.getScore() + ")");
+ });
+ } catch (Exception e) {
+ System.err.println("Search failed: " + e.getMessage());
+ }
+ }
+}
+```
+
+## Configuration
+
+The `MemUClient` constructor requires two parameters:
+
+| Parameter | Description | Example |
+|-----------|-------------|---------|
+| `baseUrl` | The URL of your MemU API instance. | `https://api.memu.ai` |
+| `apiKey` | Your authentication token. | `mem_12345abcde` |
+
+We recommend storing your API key in an environment variable (e.g., `MEMU_API_KEY`) rather than hardcoding it.
+
+## Building from Source
+
+To build the SDK and run tests locally, ensure you have **Java 21** and **Maven** installed.
+
+```bash
+# Clone the repository
+git clone https://github.com/nevamind-ai/memu-sdk-java.git
+cd memu-sdk-java
+
+# Build and run integrity checks (Tests + Formatting + Coverage)
+mvn clean verify
+```
+
+## Requirements
+
+* Java 21 or higher
+* Maven 3.6+
diff --git a/lombok.config b/lombok.config
new file mode 100644
index 0000000..df71bb6
--- /dev/null
+++ b/lombok.config
@@ -0,0 +1,2 @@
+config.stopBubbling = true
+lombok.addLombokGeneratedAnnotation = true
diff --git a/null b/null
new file mode 100644
index 0000000..e69de29
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..f33ea07
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,136 @@
+
+
+ 4.0.0
+
+ com.nevamind.ai
+ memu-sdk-java
+ 0.1.0-SNAPSHOT
+
+
+ 21
+ 21
+ 21
+ UTF-8
+
+
+
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.12.0
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.16.1
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ 2.16.1
+
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+ provided
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.1
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.8.0
+ test
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ 4.12.0
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.24.2
+ test
+
+
+
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+ 2.41.1
+
+
+
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.11
+
+
+
+ prepare-agent
+
+
+
+ report
+ verify
+
+ report
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.3
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.6.3
+
+ false
+
+
+
+
+
diff --git a/src/main/java/com/nevamind/memu/MemUClient.java b/src/main/java/com/nevamind/memu/MemUClient.java
new file mode 100644
index 0000000..5f76827
--- /dev/null
+++ b/src/main/java/com/nevamind/memu/MemUClient.java
@@ -0,0 +1,124 @@
+package com.nevamind.memu;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.nevamind.memu.exception.MemUException;
+import com.nevamind.memu.model.MemoryItem;
+import com.nevamind.memu.model.SearchRequest;
+import com.nevamind.memu.model.SearchResponse;
+import java.io.IOException;
+import okhttp3.*;
+
+/**
+ * Client for interacting with the MemU API.
+ *
+ *
Provides methods to create memories and search/retrieve information.
+ *
+ *
Usage Example:
+ *
+ * {@code
+ * MemUClient client = new MemUClient("https://api.memu.ai", "your-api-key");
+ *
+ * // Create a memory
+ * MemoryItem item = MemoryItem.builder()
+ * .summary("User passed the Java certification")
+ * .memoryType(MemoryType.SKILL)
+ * .build();
+ * client.createMemory(item);
+ *
+ * // Search for memories
+ * SearchRequest request = SearchRequest.builder()
+ * .queries(List.of("java certification"))
+ * .limit(5)
+ * .build();
+ * SearchResponse response = client.search(request);
+ * }
+ */
+public class MemUClient {
+
+ private static final String ENDPOINT_MEMORY = "/v1/memory";
+ private static final String ENDPOINT_RETRIEVE = "/v1/retrieve";
+ private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
+
+ private final String baseUrl;
+ private final String apiKey;
+ private final OkHttpClient client;
+ private final ObjectMapper jsonMapper;
+
+ /**
+ * Constructs a new MemUClient.
+ *
+ * @param baseUrl The base URL of the MemU API (e.g., "https://api.memu.ai").
+ * @param apiKey The API key for authentication.
+ */
+ public MemUClient(String baseUrl, String apiKey) {
+ this.baseUrl = baseUrl;
+ this.apiKey = apiKey;
+ this.client = new OkHttpClient();
+ this.jsonMapper = new ObjectMapper();
+ this.jsonMapper.registerModule(new JavaTimeModule());
+ }
+
+ /**
+ * Creates a new memory item in the MemU system.
+ *
+ * @param item The {@link MemoryItem} to create.
+ * @return {@code true} if the operation was successful.
+ * @throws MemUException If a network error occurs or the API returns a non-success status code.
+ */
+ public boolean createMemory(MemoryItem item) {
+ try {
+ String json = jsonMapper.writeValueAsString(item);
+ RequestBody body = RequestBody.create(json, JSON);
+ Request request =
+ new Request.Builder()
+ .url(baseUrl + ENDPOINT_MEMORY)
+ .addHeader("Authorization", "Bearer " + apiKey)
+ .post(body)
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new MemUException(
+ "Create memory failed: " + response.code() + " " + response.message());
+ }
+ return true;
+ }
+ } catch (IOException e) {
+ throw new MemUException("Network error creating memory", e);
+ }
+ }
+
+ /**
+ * Searches for memories based on the provided criteria.
+ *
+ * @param searchRequest The {@link SearchRequest} containing queries and filters.
+ * @return A {@link SearchResponse} containing the search results.
+ * @throws MemUException If a network error occurs, the API returns a non-success status, or the
+ * response body is empty.
+ */
+ public SearchResponse search(SearchRequest searchRequest) {
+ try {
+ String json = jsonMapper.writeValueAsString(searchRequest);
+ RequestBody body = RequestBody.create(json, JSON);
+ Request request =
+ new Request.Builder()
+ .url(baseUrl + ENDPOINT_RETRIEVE)
+ .addHeader("Authorization", "Bearer " + apiKey)
+ .post(body)
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new MemUException("Search failed: " + response.code() + " " + response.message());
+ }
+ if (response.body() == null) {
+ throw new MemUException("Search returned empty body");
+ }
+ return jsonMapper.readValue(response.body().string(), SearchResponse.class);
+ }
+ } catch (IOException e) {
+ throw new MemUException("Network error searching", e);
+ }
+ }
+}
diff --git a/src/main/java/com/nevamind/memu/exception/MemUException.java b/src/main/java/com/nevamind/memu/exception/MemUException.java
new file mode 100644
index 0000000..8e46894
--- /dev/null
+++ b/src/main/java/com/nevamind/memu/exception/MemUException.java
@@ -0,0 +1,12 @@
+package com.nevamind.memu.exception;
+
+public class MemUException extends RuntimeException {
+
+ public MemUException(String message) {
+ super(message);
+ }
+
+ public MemUException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/com/nevamind/memu/model/BaseRecord.java b/src/main/java/com/nevamind/memu/model/BaseRecord.java
new file mode 100644
index 0000000..5c26a3a
--- /dev/null
+++ b/src/main/java/com/nevamind/memu/model/BaseRecord.java
@@ -0,0 +1,23 @@
+package com.nevamind.memu.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import java.time.ZonedDateTime;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
+public abstract class BaseRecord {
+
+ @lombok.Builder.Default private String id = UUID.randomUUID().toString();
+
+ private ZonedDateTime createdAt;
+
+ private ZonedDateTime updatedAt;
+}
diff --git a/src/main/java/com/nevamind/memu/model/CategoryItem.java b/src/main/java/com/nevamind/memu/model/CategoryItem.java
new file mode 100644
index 0000000..1af33fe
--- /dev/null
+++ b/src/main/java/com/nevamind/memu/model/CategoryItem.java
@@ -0,0 +1,18 @@
+package com.nevamind.memu.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@lombok.EqualsAndHashCode(callSuper = true)
+public class CategoryItem extends BaseRecord {
+
+ private String itemId;
+
+ private String categoryId;
+}
diff --git a/src/main/java/com/nevamind/memu/model/MemoryCategory.java b/src/main/java/com/nevamind/memu/model/MemoryCategory.java
new file mode 100644
index 0000000..e783af6
--- /dev/null
+++ b/src/main/java/com/nevamind/memu/model/MemoryCategory.java
@@ -0,0 +1,23 @@
+package com.nevamind.memu.model;
+
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@lombok.EqualsAndHashCode(callSuper = true)
+public class MemoryCategory extends BaseRecord {
+
+ private String name;
+
+ private String description;
+
+ private String summary;
+
+ private List embedding;
+}
diff --git a/src/main/java/com/nevamind/memu/model/MemoryItem.java b/src/main/java/com/nevamind/memu/model/MemoryItem.java
new file mode 100644
index 0000000..570003d
--- /dev/null
+++ b/src/main/java/com/nevamind/memu/model/MemoryItem.java
@@ -0,0 +1,23 @@
+package com.nevamind.memu.model;
+
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@lombok.EqualsAndHashCode(callSuper = true)
+public class MemoryItem extends BaseRecord {
+
+ private String resourceId;
+
+ private MemoryType memoryType;
+
+ private String summary;
+
+ private List embedding;
+}
diff --git a/src/main/java/com/nevamind/memu/model/MemoryType.java b/src/main/java/com/nevamind/memu/model/MemoryType.java
new file mode 100644
index 0000000..f70fa25
--- /dev/null
+++ b/src/main/java/com/nevamind/memu/model/MemoryType.java
@@ -0,0 +1,23 @@
+package com.nevamind.memu.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public enum MemoryType {
+ @JsonProperty("profile")
+ PROFILE,
+
+ @JsonProperty("event")
+ EVENT,
+
+ @JsonProperty("knowledge")
+ KNOWLEDGE,
+
+ @JsonProperty("behavior")
+ BEHAVIOR,
+
+ @JsonProperty("skill")
+ SKILL,
+
+ @JsonProperty("episodic")
+ EPISODIC;
+}
diff --git a/src/main/java/com/nevamind/memu/model/Resource.java b/src/main/java/com/nevamind/memu/model/Resource.java
new file mode 100644
index 0000000..9aeb81a
--- /dev/null
+++ b/src/main/java/com/nevamind/memu/model/Resource.java
@@ -0,0 +1,27 @@
+package com.nevamind.memu.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+@lombok.EqualsAndHashCode(callSuper = true)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Resource extends BaseRecord {
+
+ private String url;
+
+ private String modality;
+
+ private String localPath;
+
+ private String caption;
+
+ private List embedding;
+}
diff --git a/src/main/java/com/nevamind/memu/model/SearchRequest.java b/src/main/java/com/nevamind/memu/model/SearchRequest.java
new file mode 100644
index 0000000..f784675
--- /dev/null
+++ b/src/main/java/com/nevamind/memu/model/SearchRequest.java
@@ -0,0 +1,20 @@
+package com.nevamind.memu.model;
+
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SearchRequest {
+
+ private List queries;
+
+ @Builder.Default private Integer limit = 10;
+
+ private Double minScore;
+}
diff --git a/src/main/java/com/nevamind/memu/model/SearchResponse.java b/src/main/java/com/nevamind/memu/model/SearchResponse.java
new file mode 100644
index 0000000..bd52f3f
--- /dev/null
+++ b/src/main/java/com/nevamind/memu/model/SearchResponse.java
@@ -0,0 +1,14 @@
+package com.nevamind.memu.model;
+
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class SearchResponse {
+
+ private List results;
+}
diff --git a/src/main/java/com/nevamind/memu/model/SearchResultItem.java b/src/main/java/com/nevamind/memu/model/SearchResultItem.java
new file mode 100644
index 0000000..1b05e01
--- /dev/null
+++ b/src/main/java/com/nevamind/memu/model/SearchResultItem.java
@@ -0,0 +1,20 @@
+package com.nevamind.memu.model;
+
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class SearchResultItem {
+
+ private String id;
+
+ private String content;
+
+ private Double score;
+
+ private Map metadata;
+}
diff --git a/src/test/java/com/nevamind/memu/MemUClientTest.java b/src/test/java/com/nevamind/memu/MemUClientTest.java
new file mode 100644
index 0000000..4736c85
--- /dev/null
+++ b/src/test/java/com/nevamind/memu/MemUClientTest.java
@@ -0,0 +1,135 @@
+package com.nevamind.memu;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.nevamind.memu.exception.MemUException;
+import com.nevamind.memu.model.*;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class MemUClientTest {
+
+ private MockWebServer mockWebServer;
+ private MemUClient client;
+ private ObjectMapper mapper;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ mockWebServer = new MockWebServer();
+ mockWebServer.start();
+
+ // Inject mock URL into client
+ String baseUrl = mockWebServer.url("/").toString();
+ if (baseUrl.endsWith("/")) {
+ baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
+ }
+ client = new MemUClient(baseUrl, "test-api-key");
+
+ mapper = new ObjectMapper();
+ mapper.registerModule(new JavaTimeModule());
+ }
+
+ @AfterEach
+ void tearDown() throws IOException {
+ mockWebServer.shutdown();
+ }
+
+ @Test
+ void testCreateMemory_Success() throws Exception {
+ // Arrange
+ MemoryItem item =
+ MemoryItem.builder()
+ .summary("Test memory")
+ .memoryType(MemoryType.EVENT)
+ .resourceId("res-123")
+ .build();
+
+ // Enqueue 200 OK (Assuming API returns empty JSON or similar on success)
+ mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));
+
+ // Act
+ boolean success = client.createMemory(item);
+
+ // Assert
+ assertThat(success).isTrue();
+
+ RecordedRequest request = mockWebServer.takeRequest();
+ assertThat(request.getMethod()).isEqualTo("POST");
+ assertThat(request.getPath()).isEqualTo("/v1/memory");
+ assertThat(request.getHeader("Authorization")).isEqualTo("Bearer test-api-key");
+ assertThat(request.getHeader("Content-Type")).contains("application/json");
+
+ // Verify Body
+ MemoryItem sentItem = mapper.readValue(request.getBody().readUtf8(), MemoryItem.class);
+ assertThat(sentItem.getSummary()).isEqualTo("Test memory");
+ assertThat(sentItem.getMemoryType()).isEqualTo(MemoryType.EVENT);
+ assertThat(sentItem.getResourceId()).isEqualTo("res-123");
+ }
+
+ @Test
+ void testSearch_Success() throws Exception {
+ // Arrange
+ SearchRequest searchRequest =
+ SearchRequest.builder().queries(Collections.singletonList("test query")).limit(5).build();
+
+ SearchResultItem resultItem =
+ new SearchResultItem("mem-1", "Found content", 0.95, Map.of("source", "chat"));
+ SearchResponse responseBody = new SearchResponse(List.of(resultItem));
+
+ mockWebServer.enqueue(
+ new MockResponse().setResponseCode(200).setBody(mapper.writeValueAsString(responseBody)));
+
+ // Act
+ SearchResponse response = client.search(searchRequest);
+
+ // Assert
+ assertThat(response).isNotNull();
+ assertThat(response.getResults()).hasSize(1);
+ assertThat(response.getResults().get(0).getId()).isEqualTo("mem-1");
+ assertThat(response.getResults().get(0).getContent()).isEqualTo("Found content");
+
+ RecordedRequest request = mockWebServer.takeRequest();
+ assertThat(request.getPath()).isEqualTo("/v1/retrieve");
+
+ // Verify Request Body
+ SearchRequest sentRequest = mapper.readValue(request.getBody().readUtf8(), SearchRequest.class);
+ assertThat(sentRequest.getQueries()).containsExactly("test query");
+ assertThat(sentRequest.getLimit()).isEqualTo(5);
+ }
+
+ @Test
+ void testUnauthorized_ThrowsException() {
+ // Arrange
+ MemoryItem item = MemoryItem.builder().summary("Secret").build();
+
+ mockWebServer.enqueue(
+ new MockResponse().setResponseCode(401).setBody("{\"error\":\"Unauthorized\"}"));
+
+ // Act & Assert
+ MemUException exception = assertThrows(MemUException.class, () -> client.createMemory(item));
+ assertThat(exception.getMessage()).contains("401");
+ }
+
+ @Test
+ void testServerError_ThrowsException() {
+ // Arrange
+ SearchRequest request = SearchRequest.builder().queries(List.of("query")).build();
+
+ mockWebServer.enqueue(new MockResponse().setResponseCode(500).setBody("Internal Server Error"));
+
+ // Act & Assert
+ MemUException exception = assertThrows(MemUException.class, () -> client.search(request));
+ assertThat(exception.getMessage()).contains("500");
+ }
+}
diff --git a/src/test/java/com/nevamind/memu/model/ModelTest.java b/src/test/java/com/nevamind/memu/model/ModelTest.java
new file mode 100644
index 0000000..b3c9ae5
--- /dev/null
+++ b/src/test/java/com/nevamind/memu/model/ModelTest.java
@@ -0,0 +1,214 @@
+package com.nevamind.memu.model;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import java.time.ZonedDateTime;
+import java.util.Collections;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class ModelTest {
+
+ private ObjectMapper mapper;
+
+ @BeforeEach
+ void setUp() {
+ mapper = new ObjectMapper();
+ mapper.registerModule(new JavaTimeModule());
+ }
+
+ @Test
+ void testMemoryItemCoverage() throws Exception {
+ MemoryItem item1 =
+ MemoryItem.builder()
+ .id("test-id")
+ .createdAt(ZonedDateTime.now())
+ .updatedAt(ZonedDateTime.now())
+ .resourceId("res-1")
+ .memoryType(MemoryType.EVENT)
+ .summary("Summary")
+ .embedding(Collections.singletonList(0.1f))
+ .build();
+
+ MemoryItem item2 =
+ MemoryItem.builder()
+ .id("test-id")
+ .createdAt(item1.getCreatedAt())
+ .updatedAt(item1.getUpdatedAt())
+ .resourceId("res-1")
+ .memoryType(MemoryType.EVENT)
+ .summary("Summary")
+ .embedding(Collections.singletonList(0.1f))
+ .build();
+
+ assertEquals(item1, item2);
+ assertEquals(item1.hashCode(), item2.hashCode());
+ assertNotNull(item1.toString());
+
+ String json = mapper.writeValueAsString(item1);
+ MemoryItem deserialized = mapper.readValue(json, MemoryItem.class);
+
+ assertEquals(item1.getId(), deserialized.getId());
+ assertEquals(item1.getResourceId(), deserialized.getResourceId());
+ assertEquals(item1.getMemoryType(), deserialized.getMemoryType());
+ assertEquals(item1.getSummary(), deserialized.getSummary());
+ }
+
+ @Test
+ void testResourceCoverage() throws Exception {
+ Resource resource1 =
+ Resource.builder()
+ .id("res-id")
+ .url("http://example.com")
+ .modality("text")
+ .localPath("/tmp/file")
+ .caption("Caption")
+ .embedding(Collections.singletonList(0.5f))
+ .build();
+
+ Resource resource2 =
+ Resource.builder()
+ .id("res-id")
+ .url("http://example.com")
+ .modality("text")
+ .localPath("/tmp/file")
+ .caption("Caption")
+ .embedding(Collections.singletonList(0.5f))
+ .build();
+
+ assertEquals(resource1, resource2);
+ assertEquals(resource1.hashCode(), resource2.hashCode());
+ assertNotNull(resource1.toString());
+
+ String json = mapper.writeValueAsString(resource1);
+ Resource deserialized = mapper.readValue(json, Resource.class);
+ assertEquals(resource1.getId(), deserialized.getId());
+ assertEquals(resource1.getUrl(), deserialized.getUrl());
+ }
+
+ @Test
+ void testMemoryCategoryCoverage() throws Exception {
+ MemoryCategory cat1 =
+ MemoryCategory.builder()
+ .id("cat-id")
+ .name("Category")
+ .description("Desc")
+ .summary("Sum")
+ .embedding(Collections.singletonList(0.9f))
+ .build();
+
+ MemoryCategory cat2 =
+ MemoryCategory.builder()
+ .id("cat-id")
+ .name("Category")
+ .description("Desc")
+ .summary("Sum")
+ .embedding(Collections.singletonList(0.9f))
+ .build();
+
+ assertEquals(cat1, cat2);
+ assertEquals(cat1.hashCode(), cat2.hashCode());
+ assertNotNull(cat1.toString());
+
+ String json = mapper.writeValueAsString(cat1);
+ MemoryCategory deserialized = mapper.readValue(json, MemoryCategory.class);
+ assertEquals(cat1.getName(), deserialized.getName());
+ }
+
+ @Test
+ void testCategoryItemCoverage() throws Exception {
+ CategoryItem item1 =
+ CategoryItem.builder().id("join-id").itemId("item-1").categoryId("cat-1").build();
+
+ CategoryItem item2 =
+ CategoryItem.builder().id("join-id").itemId("item-1").categoryId("cat-1").build();
+
+ assertEquals(item1, item2);
+ assertEquals(item1.hashCode(), item2.hashCode());
+ assertNotNull(item1.toString());
+
+ String json = mapper.writeValueAsString(item1);
+ CategoryItem deserialized = mapper.readValue(json, CategoryItem.class);
+ assertEquals(item1.getItemId(), deserialized.getItemId());
+ }
+
+ @Test
+ void testMemoryTypeCoverage() throws Exception {
+ // Test Enum serialization
+ String json = mapper.writeValueAsString(MemoryType.EVENT);
+ assertEquals("\"event\"", json);
+
+ MemoryType deserialized = mapper.readValue("\"event\"", MemoryType.class);
+ assertEquals(MemoryType.EVENT, deserialized);
+
+ // Verify all values present
+ assertNotNull(MemoryType.valueOf("EVENT"));
+ assertNotNull(MemoryType.valueOf("PROFILE"));
+ assertNotNull(MemoryType.valueOf("KNOWLEDGE"));
+ assertNotNull(MemoryType.valueOf("BEHAVIOR"));
+ assertNotNull(MemoryType.valueOf("SKILL"));
+ assertNotNull(MemoryType.valueOf("EPISODIC"));
+ }
+
+ @Test
+ void testSearchRequestCoverage() throws Exception {
+ SearchRequest req1 =
+ SearchRequest.builder()
+ .queries(Collections.singletonList("query"))
+ .limit(10)
+ .minScore(0.5)
+ .build();
+
+ SearchRequest req2 = new SearchRequest();
+ req2.setQueries(Collections.singletonList("query"));
+ req2.setLimit(10);
+ req2.setMinScore(0.5);
+
+ // Test AllArgsConstructor via Builder vs NoArgsConstructor + Setters
+ // (indirectly)
+ assertEquals(req1, req2);
+ assertEquals(req1.hashCode(), req2.hashCode());
+ assertNotNull(req1.toString());
+
+ String json = mapper.writeValueAsString(req1);
+ SearchRequest deserialized = mapper.readValue(json, SearchRequest.class);
+ assertEquals(req1.getLimit(), deserialized.getLimit());
+ }
+
+ @Test
+ void testSearchResponseCoverage() throws Exception {
+ SearchResultItem item = new SearchResultItem("id", "content", 1.0, null);
+ SearchResponse res1 = new SearchResponse(Collections.singletonList(item));
+ SearchResponse res2 = new SearchResponse();
+ res2.setResults(Collections.singletonList(item));
+
+ assertEquals(res1, res2);
+ assertEquals(res1.hashCode(), res2.hashCode());
+ assertNotNull(res1.toString());
+
+ String json = mapper.writeValueAsString(res1);
+ SearchResponse deserialized = mapper.readValue(json, SearchResponse.class);
+ assertEquals(1, deserialized.getResults().size());
+ }
+
+ @Test
+ void testSearchResultItemCoverage() throws Exception {
+ SearchResultItem item1 = new SearchResultItem("id", "content", 0.9, Map.of("key", "val"));
+ SearchResultItem item2 = new SearchResultItem();
+ item2.setId("id");
+ item2.setContent("content");
+ item2.setScore(0.9);
+ item2.setMetadata(Map.of("key", "val"));
+
+ assertEquals(item1, item2);
+ assertEquals(item1.hashCode(), item2.hashCode());
+ assertNotNull(item1.toString());
+
+ String json = mapper.writeValueAsString(item1);
+ SearchResultItem deserialized = mapper.readValue(json, SearchResultItem.class);
+ assertEquals(item1.getContent(), deserialized.getContent());
+ }
+}