26382a7ac6
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
JetBrains Plugin / Actionlint (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
JetBrains Plugin / Validation (push) Waiting to run
JetBrains Plugin / Build (push) Waiting to run
JetBrains Plugin / Test (push) Blocked by required conditions
Security Check / Security Scan (push) Waiting to run
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
227 lines
7.0 KiB
Rust
227 lines
7.0 KiB
Rust
use std::sync::Arc;
|
|
|
|
use axum::body::Body;
|
|
use axum::http::{Request, StatusCode};
|
|
use rmcp::transport::{StreamableHttpServerConfig, StreamableHttpService};
|
|
use serde_json::json;
|
|
|
|
type LocalSessionManager =
|
|
rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
|
|
|
|
fn jsonrpc_call(tool: &str, arguments: &serde_json::Value) -> String {
|
|
json!({
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tools/call",
|
|
"params": {
|
|
"name": tool,
|
|
"arguments": arguments
|
|
}
|
|
})
|
|
.to_string()
|
|
}
|
|
|
|
async fn call_tool_json(
|
|
svc: &StreamableHttpService<lean_ctx::tools::LeanCtxServer, LocalSessionManager>,
|
|
tool: &str,
|
|
arguments: serde_json::Value,
|
|
) -> serde_json::Value {
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/")
|
|
.header("Host", "localhost")
|
|
.header("Accept", "application/json, text/event-stream")
|
|
.header("Content-Type", "application/json")
|
|
.body(Body::from(jsonrpc_call(tool, &arguments)))
|
|
.expect("request");
|
|
|
|
let resp = svc.handle(req).await;
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
|
|
let bytes = axum::body::to_bytes(Body::new(resp.into_body()), usize::MAX)
|
|
.await
|
|
.expect("read body");
|
|
let v: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
|
|
assert!(
|
|
v.get("error").is_none(),
|
|
"expected jsonrpc result, got error: {v}"
|
|
);
|
|
v
|
|
}
|
|
|
|
/// Poll `cond` up to ~10s. The memory stores are persisted on a background
|
|
/// thread (the consolidation pipeline), so asserting immediately after a tool
|
|
/// call returns raced the writer and made this test flaky under full-suite load
|
|
/// (#215). Polling keeps the assertion strict (it still fails if the artifact
|
|
/// never lands) while tolerating scheduling latency.
|
|
async fn wait_until(mut cond: impl FnMut() -> bool) -> bool {
|
|
for _ in 0..200 {
|
|
if cond() {
|
|
return true;
|
|
}
|
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
|
}
|
|
cond()
|
|
}
|
|
|
|
/// True once `dir` exists and contains at least one entry. Tolerates the dir not
|
|
/// existing yet (the background writer may not have created it).
|
|
fn dir_has_entry(dir: &std::path::Path) -> bool {
|
|
std::fs::read_dir(dir).is_ok_and(|mut it| it.next().is_some())
|
|
}
|
|
|
|
#[allow(clippy::await_holding_lock)]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn memory_benchmark_suite_persists_core_artifacts() {
|
|
let _lock = lean_ctx::core::data_dir::test_env_lock();
|
|
let data_dir = tempfile::tempdir().expect("data dir");
|
|
// TODO: Audit that the environment access only happens in single-threaded code.
|
|
unsafe { std::env::set_var("LEAN_CTX_DATA_DIR", data_dir.path()) };
|
|
|
|
let dir = tempfile::tempdir().expect("project dir");
|
|
// Ensure project-root detection can anchor on a marker.
|
|
std::fs::create_dir_all(dir.path().join(".git")).expect("create .git marker");
|
|
let file_path = dir.path().join("a.txt");
|
|
std::fs::write(&file_path, "hello\n").expect("write file");
|
|
|
|
let root = lean_ctx::core::pathutil::safe_canonicalize_or_self(dir.path());
|
|
let root_str = root.to_string_lossy().to_string();
|
|
let base = lean_ctx::tools::LeanCtxServer::new_with_project_root(Some(&root_str));
|
|
|
|
let service_factory = move || Ok(base.clone());
|
|
let cfg = StreamableHttpServerConfig::default()
|
|
.with_stateful_mode(false)
|
|
.with_json_response(true);
|
|
let svc = StreamableHttpService::new(
|
|
service_factory,
|
|
Arc::new(LocalSessionManager::default()),
|
|
cfg,
|
|
);
|
|
|
|
// 1) Establish project context (tool pipeline).
|
|
let _ = call_tool_json(
|
|
&svc,
|
|
"ctx_read",
|
|
json!({
|
|
"path": file_path.to_string_lossy().to_string(),
|
|
"mode": "full"
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// 2) Knowledge: remember + feedback + relations.
|
|
let _ = call_tool_json(
|
|
&svc,
|
|
"ctx_knowledge",
|
|
json!({
|
|
"action": "remember",
|
|
"category": "arch",
|
|
"key": "db",
|
|
"value": "MySQL",
|
|
"confidence": 0.9
|
|
}),
|
|
)
|
|
.await;
|
|
let _ = call_tool_json(
|
|
&svc,
|
|
"ctx_knowledge",
|
|
json!({
|
|
"action": "remember",
|
|
"category": "arch",
|
|
"key": "cache",
|
|
"value": "Redis",
|
|
"confidence": 0.9
|
|
}),
|
|
)
|
|
.await;
|
|
let _ = call_tool_json(
|
|
&svc,
|
|
"ctx_knowledge",
|
|
json!({
|
|
"action": "feedback",
|
|
"category": "arch",
|
|
"key": "db",
|
|
"value": "up"
|
|
}),
|
|
)
|
|
.await;
|
|
let relate_resp = call_tool_json(
|
|
&svc,
|
|
"ctx_knowledge",
|
|
json!({
|
|
"action": "relate",
|
|
"category": "arch",
|
|
"key": "db",
|
|
"value": "depends_on",
|
|
"query": "arch/cache"
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// 3) Episodic + procedural memory persistence.
|
|
let _ = call_tool_json(
|
|
&svc,
|
|
"ctx_session",
|
|
json!({
|
|
"action": "episodes",
|
|
"value": "record"
|
|
}),
|
|
)
|
|
.await;
|
|
let _ = call_tool_json(
|
|
&svc,
|
|
"ctx_session",
|
|
json!({
|
|
"action": "procedures",
|
|
"value": "detect"
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// 4) Assert artifacts exist on disk. Persistence runs on a background thread,
|
|
// so poll with a bounded timeout rather than asserting immediately (#215).
|
|
let knowledge = lean_ctx::core::knowledge::ProjectKnowledge::load_or_create(&root_str);
|
|
let knowledge_dir = data_dir
|
|
.path()
|
|
.join("knowledge")
|
|
.join(&knowledge.project_hash);
|
|
|
|
assert!(
|
|
wait_until(|| knowledge_dir.join("knowledge.json").exists()).await,
|
|
"expected knowledge.json to exist"
|
|
);
|
|
assert!(
|
|
wait_until(|| knowledge_dir.join("relations.json").exists()).await,
|
|
"expected relations.json to exist (relate said: {relate_resp})"
|
|
);
|
|
|
|
let edge_present = wait_until(|| {
|
|
lean_ctx::core::knowledge_relations::KnowledgeRelationGraph::load(&knowledge.project_hash)
|
|
.is_some_and(|graph| {
|
|
graph.edges.iter().any(|e| {
|
|
e.from.category == "arch"
|
|
&& e.from.key == "db"
|
|
&& e.to.category == "arch"
|
|
&& e.to.key == "cache"
|
|
})
|
|
})
|
|
})
|
|
.await;
|
|
assert!(edge_present, "expected at least one relation edge");
|
|
|
|
let episodes_dir = data_dir.path().join("memory").join("episodes");
|
|
assert!(
|
|
wait_until(|| dir_has_entry(&episodes_dir)).await,
|
|
"expected at least one episodic store file"
|
|
);
|
|
|
|
let procs_dir = data_dir.path().join("memory").join("procedures");
|
|
assert!(
|
|
wait_until(|| dir_has_entry(&procs_dir)).await,
|
|
"expected at least one procedural store file"
|
|
);
|
|
|
|
// TODO: Audit that the environment access only happens in single-threaded code.
|
|
unsafe { std::env::remove_var("LEAN_CTX_DATA_DIR") };
|
|
}
|