Skip to content

Commit b34717d

Browse files
authored
test: migrate API and MCP e2e to multi-db (#189)
## Summary - migrate the main API and MCP e2e suites onto shared multi-db test harnesses - add reusable API/MCP multi-db support helpers and expose an MCP e2e make target - fix multi-db-specific route, storage, and verification assumptions uncovered during validation ## Validation - migrated API and MCP e2e suites onto the shared multi-db harness - compatibility-seeded old data and verified old API keys on the current multi-db server - exercised remaining API routes including search, correct, purge, snapshot rollback/delete, and branch merge/delete Approved by: @XuPeng-SH
1 parent 7375b2d commit b34717d

23 files changed

Lines changed: 2098 additions & 1528 deletions

File tree

Makefile

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.PHONY: help check-env up down status logs logs-db \
22
up-db down-db up-api down-api rebuild-api health \
33
dev build build-local check \
4-
test test-unit test-integration test-e2e bench dev-bench \
4+
test test-unit test-integration test-e2e test-e2e-mcp bench dev-bench \
55
new-key list-keys revoke-keys \
66
clean reset
77

@@ -47,6 +47,7 @@ help:
4747
@echo " make test All tests (needs DB)"
4848
@echo " make test-unit Unit tests (no DB)"
4949
@echo " make test-e2e E2E API tests (needs DB)"
50+
@echo " make test-e2e-mcp E2E MCP tests (needs DB)"
5051
@echo " make bench Run benchmark (needs: make up)"
5152
@echo " make dev-bench Load test API (DURATION=60 USERS=10 SEED=20 RPS=0 SCENARIO=all)"
5253
@echo ""
@@ -203,6 +204,89 @@ test-e2e:
203204
LLM_MODEL=$${MEMORIA_LLM_MODEL:-} \
204205
cargo test -p memoria-api --test api_e2e
205206

207+
test-e2e-mcp:
208+
@cd memoria && DATABASE_URL=$(TEST_DB_URL) SQLX_OFFLINE=true \
209+
EMBEDDING_API_KEY=$${MEMORIA_EMBEDDING_API_KEY:-} \
210+
EMBEDDING_BASE_URL=$${MEMORIA_EMBEDDING_BASE_URL:-} \
211+
EMBEDDING_MODEL=$${MEMORIA_EMBEDDING_MODEL:-BAAI/bge-m3} \
212+
EMBEDDING_DIM=$${MEMORIA_EMBEDDING_DIM:-1024} \
213+
LLM_API_KEY=$${MEMORIA_LLM_API_KEY:-} \
214+
LLM_BASE_URL=$${MEMORIA_LLM_BASE_URL:-} \
215+
LLM_MODEL=$${MEMORIA_LLM_MODEL:-} \
216+
cargo test -p memoria-mcp --test mcp_e2e -- --test-threads=1
217+
@cd memoria && DATABASE_URL=$(TEST_DB_URL) SQLX_OFFLINE=true \
218+
EMBEDDING_API_KEY=$${MEMORIA_EMBEDDING_API_KEY:-} \
219+
EMBEDDING_BASE_URL=$${MEMORIA_EMBEDDING_BASE_URL:-} \
220+
EMBEDDING_MODEL=$${MEMORIA_EMBEDDING_MODEL:-BAAI/bge-m3} \
221+
EMBEDDING_DIM=$${MEMORIA_EMBEDDING_DIM:-1024} \
222+
LLM_API_KEY=$${MEMORIA_LLM_API_KEY:-} \
223+
LLM_BASE_URL=$${MEMORIA_LLM_BASE_URL:-} \
224+
LLM_MODEL=$${MEMORIA_LLM_MODEL:-} \
225+
cargo test -p memoria-mcp --test core_tools_e2e -- --test-threads=1
226+
@cd memoria && DATABASE_URL=$(TEST_DB_URL) SQLX_OFFLINE=true \
227+
EMBEDDING_API_KEY=$${MEMORIA_EMBEDDING_API_KEY:-} \
228+
EMBEDDING_BASE_URL=$${MEMORIA_EMBEDDING_BASE_URL:-} \
229+
EMBEDDING_MODEL=$${MEMORIA_EMBEDDING_MODEL:-BAAI/bge-m3} \
230+
EMBEDDING_DIM=$${MEMORIA_EMBEDDING_DIM:-1024} \
231+
LLM_API_KEY=$${MEMORIA_LLM_API_KEY:-} \
232+
LLM_BASE_URL=$${MEMORIA_LLM_BASE_URL:-} \
233+
LLM_MODEL=$${MEMORIA_LLM_MODEL:-} \
234+
cargo test -p memoria-mcp --test branch_e2e -- --test-threads=1
235+
@cd memoria && DATABASE_URL=$(TEST_DB_URL) SQLX_OFFLINE=true \
236+
EMBEDDING_API_KEY=$${MEMORIA_EMBEDDING_API_KEY:-} \
237+
EMBEDDING_BASE_URL=$${MEMORIA_EMBEDDING_BASE_URL:-} \
238+
EMBEDDING_MODEL=$${MEMORIA_EMBEDDING_MODEL:-BAAI/bge-m3} \
239+
EMBEDDING_DIM=$${MEMORIA_EMBEDDING_DIM:-1024} \
240+
LLM_API_KEY=$${MEMORIA_LLM_API_KEY:-} \
241+
LLM_BASE_URL=$${MEMORIA_LLM_BASE_URL:-} \
242+
LLM_MODEL=$${MEMORIA_LLM_MODEL:-} \
243+
cargo test -p memoria-mcp --test feedback_e2e -- --test-threads=1
244+
@cd memoria && DATABASE_URL=$(TEST_DB_URL) SQLX_OFFLINE=true \
245+
EMBEDDING_API_KEY=$${MEMORIA_EMBEDDING_API_KEY:-} \
246+
EMBEDDING_BASE_URL=$${MEMORIA_EMBEDDING_BASE_URL:-} \
247+
EMBEDDING_MODEL=$${MEMORIA_EMBEDDING_MODEL:-BAAI/bge-m3} \
248+
EMBEDDING_DIM=$${MEMORIA_EMBEDDING_DIM:-1024} \
249+
LLM_API_KEY=$${MEMORIA_LLM_API_KEY:-} \
250+
LLM_BASE_URL=$${MEMORIA_LLM_BASE_URL:-} \
251+
LLM_MODEL=$${MEMORIA_LLM_MODEL:-} \
252+
cargo test -p memoria-mcp --test integration_full -- --test-threads=1
253+
@cd memoria && DATABASE_URL=$(TEST_DB_URL) SQLX_OFFLINE=true \
254+
EMBEDDING_API_KEY=$${MEMORIA_EMBEDDING_API_KEY:-} \
255+
EMBEDDING_BASE_URL=$${MEMORIA_EMBEDDING_BASE_URL:-} \
256+
EMBEDDING_MODEL=$${MEMORIA_EMBEDDING_MODEL:-BAAI/bge-m3} \
257+
EMBEDDING_DIM=$${MEMORIA_EMBEDDING_DIM:-1024} \
258+
LLM_API_KEY=$${MEMORIA_LLM_API_KEY:-} \
259+
LLM_BASE_URL=$${MEMORIA_LLM_BASE_URL:-} \
260+
LLM_MODEL=$${MEMORIA_LLM_MODEL:-} \
261+
cargo test -p memoria-mcp --test perf_optimizations_e2e -- --test-threads=1
262+
@cd memoria && DATABASE_URL=$(TEST_DB_URL) SQLX_OFFLINE=true \
263+
EMBEDDING_API_KEY=$${MEMORIA_EMBEDDING_API_KEY:-} \
264+
EMBEDDING_BASE_URL=$${MEMORIA_EMBEDDING_BASE_URL:-} \
265+
EMBEDDING_MODEL=$${MEMORIA_EMBEDDING_MODEL:-BAAI/bge-m3} \
266+
EMBEDDING_DIM=$${MEMORIA_EMBEDDING_DIM:-1024} \
267+
LLM_API_KEY=$${MEMORIA_LLM_API_KEY:-} \
268+
LLM_BASE_URL=$${MEMORIA_LLM_BASE_URL:-} \
269+
LLM_MODEL=$${MEMORIA_LLM_MODEL:-} \
270+
cargo test -p memoria-mcp --test snapshot_e2e -- --test-threads=1
271+
@cd memoria && DATABASE_URL=$(TEST_DB_URL) SQLX_OFFLINE=true \
272+
EMBEDDING_API_KEY=$${MEMORIA_EMBEDDING_API_KEY:-} \
273+
EMBEDDING_BASE_URL=$${MEMORIA_EMBEDDING_BASE_URL:-} \
274+
EMBEDDING_MODEL=$${MEMORIA_EMBEDDING_MODEL:-BAAI/bge-m3} \
275+
EMBEDDING_DIM=$${MEMORIA_EMBEDDING_DIM:-1024} \
276+
LLM_API_KEY=$${MEMORIA_LLM_API_KEY:-} \
277+
LLM_BASE_URL=$${MEMORIA_LLM_BASE_URL:-} \
278+
LLM_MODEL=$${MEMORIA_LLM_MODEL:-} \
279+
cargo test -p memoria-mcp --test edit_log_e2e -- --test-threads=1
280+
@cd memoria && DATABASE_URL=$(TEST_DB_URL) SQLX_OFFLINE=true \
281+
EMBEDDING_API_KEY=$${MEMORIA_EMBEDDING_API_KEY:-} \
282+
EMBEDDING_BASE_URL=$${MEMORIA_EMBEDDING_BASE_URL:-} \
283+
EMBEDDING_MODEL=$${MEMORIA_EMBEDDING_MODEL:-BAAI/bge-m3} \
284+
EMBEDDING_DIM=$${MEMORIA_EMBEDDING_DIM:-1024} \
285+
LLM_API_KEY=$${MEMORIA_LLM_API_KEY:-} \
286+
LLM_BASE_URL=$${MEMORIA_LLM_BASE_URL:-} \
287+
LLM_MODEL=$${MEMORIA_LLM_MODEL:-} \
288+
cargo test -p memoria-mcp --test graph_e2e -- --test-threads=1
289+
206290
# ── Benchmark ───────────────────────────────────────────────────────
207291

208292
BENCH_URL ?= http://localhost:$${API_PORT:-8100}

memoria/Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

memoria/crates/memoria-api/src/routes/admin.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,46 @@ pub async fn health_hygiene_global(
342342
.sql_store
343343
.as_ref()
344344
.ok_or((StatusCode::SERVICE_UNAVAILABLE, "SQL store required".into()))?;
345+
if let Some(router) = sql.db_router() {
346+
let user_ids = router.list_active_users().await.map_err(api_err)?;
347+
let mut inactive = 0i64;
348+
let mut stale_working = 0i64;
349+
let mut orphan_mel = 0i64;
350+
let mut orphan_el = 0i64;
351+
let mut orphan_graph_nodes = 0i64;
352+
let mut orphan_stats = 0i64;
353+
354+
for user_id in user_ids {
355+
let user_store = state.service.user_sql_store(&user_id).await.map_err(api_err)?;
356+
let hygiene = user_store.health_hygiene(&user_id).await.map_err(db_err)?;
357+
inactive += hygiene["inactive_memories"].as_i64().unwrap_or(0);
358+
stale_working += hygiene["stale_working_memories"].as_i64().unwrap_or(0);
359+
orphan_mel += hygiene["orphan_memory_entity_links"].as_i64().unwrap_or(0);
360+
orphan_el += hygiene["orphan_entity_links"].as_i64().unwrap_or(0);
361+
orphan_graph_nodes += hygiene["orphan_graph_nodes"].as_i64().unwrap_or(0);
362+
363+
let memory_stats_table = user_store.t("mem_memories_stats");
364+
let memories_table = user_store.t("mem_memories");
365+
let (user_orphan_stats,): (i64,) = sqlx::query_as(&format!(
366+
"SELECT COUNT(*) FROM {memory_stats_table} s \
367+
LEFT JOIN {memories_table} m ON s.memory_id = m.memory_id \
368+
WHERE m.memory_id IS NULL"
369+
))
370+
.fetch_one(user_store.pool())
371+
.await
372+
.map_err(db_err)?;
373+
orphan_stats += user_orphan_stats;
374+
}
375+
376+
return Ok(Json(serde_json::json!({
377+
"inactive_memories": inactive,
378+
"stale_working_memories": stale_working,
379+
"orphan_memory_entity_links": orphan_mel,
380+
"orphan_entity_links": orphan_el,
381+
"orphan_graph_nodes": orphan_graph_nodes,
382+
"orphan_stats": orphan_stats,
383+
})));
384+
}
345385
let result = sql.health_hygiene_global().await.map_err(db_err)?;
346386
Ok(Json(result))
347387
}

memoria/crates/memoria-api/src/routes/memory.rs

Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,36 @@ pub fn api_err_typed(e: memoria_core::MemoriaError) -> (StatusCode, String) {
3333
(status, e.to_string())
3434
}
3535

36+
async fn find_memory_any_user(
37+
state: &AppState,
38+
memory_id: &str,
39+
) -> Result<Option<(String, memoria_core::Memory)>, (StatusCode, String)> {
40+
let shared = state.service.shared_sql_store().map_err(api_err_typed)?;
41+
let Some(router) = shared.db_router() else {
42+
let memory = state.service.get(memory_id).await.map_err(api_err_typed)?;
43+
return Ok(memory.map(|m| (m.user_id.clone(), m)));
44+
};
45+
46+
let user_ids = router.list_active_users().await.map_err(api_err_typed)?;
47+
for candidate in user_ids {
48+
let sql = state
49+
.service
50+
.user_sql_store(&candidate)
51+
.await
52+
.map_err(api_err_typed)?;
53+
let table = sql.active_table(&candidate).await.map_err(api_err_typed)?;
54+
if let Some(memory) = sql
55+
.get_from(&table, memory_id)
56+
.await
57+
.map_err(api_err_typed)?
58+
{
59+
return Ok(Some((candidate, memory)));
60+
}
61+
}
62+
63+
Ok(None)
64+
}
65+
3666
#[derive(Deserialize, Default)]
3767
pub struct ListQuery {
3868
pub memory_type: Option<String>,
@@ -284,17 +314,24 @@ pub async fn get_memory(
284314
AuthUser { user_id, is_master }: AuthUser,
285315
Path(id): Path<String>,
286316
) -> ApiResult<Option<MemoryResponse>> {
287-
let m = state
317+
let owned = state
288318
.service
289319
.get_for_user(&user_id, &id)
290320
.await
291-
.map_err(api_err)?;
292-
if let Some(ref mem) = m {
293-
if !is_master && mem.user_id != user_id {
294-
return Err((StatusCode::FORBIDDEN, "Not your memory".to_string()));
295-
}
321+
.map_err(api_err_typed)?;
322+
if let Some(memory) = owned {
323+
return Ok(Json(Some(memory.into())));
324+
}
325+
326+
if !is_master {
327+
return Ok(Json(None));
296328
}
297-
Ok(Json(m.map(Into::into)))
329+
330+
if let Some((_, memory)) = find_memory_any_user(&state, &id).await? {
331+
return Ok(Json(Some(memory.into())));
332+
}
333+
334+
Ok(Json(None))
298335
}
299336

300337
pub async fn correct_memory(
@@ -303,22 +340,28 @@ pub async fn correct_memory(
303340
Path(id): Path<String>,
304341
Json(req): Json<CorrectRequest>,
305342
) -> ApiResult<MemoryResponse> {
306-
if !is_master {
307-
let existing = state
343+
let effective_user_id = if is_master {
344+
find_memory_any_user(&state, &id)
345+
.await?
346+
.map(|(owner_id, _)| owner_id)
347+
.unwrap_or_else(|| user_id.clone())
348+
} else {
349+
if state
308350
.service
309351
.get_for_user(&user_id, &id)
310352
.await
311-
.map_err(api_err)?
312-
.ok_or_else(|| (StatusCode::NOT_FOUND, "Memory not found".to_string()))?;
313-
if existing.user_id != user_id {
314-
return Err((StatusCode::FORBIDDEN, "Not your memory".to_string()));
353+
.map_err(api_err_typed)?
354+
.is_none()
355+
{
356+
return Err((StatusCode::NOT_FOUND, "Memory not found".to_string()));
315357
}
316-
}
358+
user_id.clone()
359+
};
317360
let m = state
318361
.service
319-
.correct(&user_id, &id, &req.new_content)
362+
.correct(&effective_user_id, &id, &req.new_content)
320363
.await
321-
.map_err(api_err)?;
364+
.map_err(api_err_typed)?;
322365
Ok(Json(m.into()))
323366
}
324367

@@ -351,18 +394,28 @@ pub async fn delete_memory(
351394
AuthUser { user_id, is_master }: AuthUser,
352395
Path(id): Path<String>,
353396
) -> Result<StatusCode, (StatusCode, String)> {
354-
if !is_master {
355-
let existing = state
397+
let effective_user_id = if is_master {
398+
find_memory_any_user(&state, &id)
399+
.await?
400+
.map(|(owner_id, _)| owner_id)
401+
.unwrap_or_else(|| user_id.clone())
402+
} else {
403+
if state
356404
.service
357405
.get_for_user(&user_id, &id)
358406
.await
359-
.map_err(api_err)?
360-
.ok_or_else(|| (StatusCode::NOT_FOUND, "Memory not found".to_string()))?;
361-
if existing.user_id != user_id {
362-
return Err((StatusCode::FORBIDDEN, "Not your memory".to_string()));
407+
.map_err(api_err_typed)?
408+
.is_none()
409+
{
410+
return Err((StatusCode::NOT_FOUND, "Memory not found".to_string()));
363411
}
364-
}
365-
let _ = state.service.purge(&user_id, &id).await.map_err(api_err)?;
412+
user_id.clone()
413+
};
414+
let _ = state
415+
.service
416+
.purge(&effective_user_id, &id)
417+
.await
418+
.map_err(api_err_typed)?;
366419
Ok(StatusCode::NO_CONTENT)
367420
}
368421

0 commit comments

Comments
 (0)