Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
978 changes: 32 additions & 946 deletions apps/elf-api/tests/http/helpers.rs

Large diffs are not rendered by default.

228 changes: 228 additions & 0 deletions apps/elf-api/tests/http/helpers/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
use serde_json::Map;

use elf_config::{
Chunking, Config, EmbeddingProviderConfig, Lifecycle, LlmProviderConfig, Memory, MemoryPolicy,
Postgres, ProviderConfig, Providers, Qdrant, Ranking, RankingBlend, RankingBlendSegment,
RankingDeterministic, RankingDeterministicDecay, RankingDeterministicHits,
RankingDeterministicLexical, RankingDiversity, RankingRetrievalSources, ReadProfiles,
ScopePrecedence, ScopeWriteAllowed, Scopes, Search, SearchCache, SearchDynamic,
SearchExpansion, SearchExplain, SearchGraphContext, SearchPrefilter, SearchRecursive, Security,
Service, Storage, TtlDays,
};

pub(crate) const TEST_TENANT_ID: &str = "tenant_alpha";
pub(crate) const TEST_PROJECT_ID: &str = "project_alpha";
pub(crate) const TEST_PROJECT_ID_B: &str = "project_beta";
pub(crate) const TEST_AGENT_A: &str = "a";
pub(crate) const TEST_AGENT_B: &str = "b";

pub(crate) fn test_ranking() -> Ranking {
Ranking {
recency_tau_days: 60.0,
tie_breaker_weight: 0.1,
deterministic: RankingDeterministic {
enabled: false,
lexical: RankingDeterministicLexical {
enabled: false,
weight: 0.05,
min_ratio: 0.3,
max_query_terms: 16,
max_text_terms: 1_024,
},
hits: RankingDeterministicHits {
enabled: false,
weight: 0.05,
half_saturation: 8.0,
last_hit_tau_days: 14.0,
},
decay: RankingDeterministicDecay { enabled: false, weight: 0.05, tau_days: 30.0 },
},
blend: RankingBlend {
enabled: true,
rerank_normalization: "rank".to_string(),
retrieval_normalization: "rank".to_string(),
segments: vec![
RankingBlendSegment { max_retrieval_rank: 3, retrieval_weight: 0.8 },
RankingBlendSegment { max_retrieval_rank: 10, retrieval_weight: 0.5 },
RankingBlendSegment { max_retrieval_rank: 1_000_000, retrieval_weight: 0.2 },
],
},
diversity: RankingDiversity {
enabled: true,
sim_threshold: 0.88,
mmr_lambda: 0.7,
max_skips: 64,
},
retrieval_sources: RankingRetrievalSources {
fusion_weight: 1.0,
structured_field_weight: 1.0,
fusion_priority: 1,
structured_field_priority: 0,
},
}
}

pub(crate) fn test_config(dsn: String, qdrant_url: String, collection: String) -> Config {
Config {
service: Service {
http_bind: "127.0.0.1:0".to_string(),
mcp_bind: "127.0.0.1:0".to_string(),
admin_bind: "127.0.0.1:0".to_string(),
log_level: "info".to_string(),
},
storage: Storage {
postgres: Postgres { dsn, pool_max_conns: 4 },
qdrant: Qdrant {
url: qdrant_url,
collection: collection.clone(),
docs_collection: format!("{collection}_docs"),
vector_dim: 4_096,
},
},
providers: Providers {
embedding: dummy_embedding_provider(),
rerank: dummy_provider(),
llm_extractor: dummy_llm_provider(),
},
scopes: Scopes {
allowed: vec![
"agent_private".to_string(),
"project_shared".to_string(),
"org_shared".to_string(),
],
read_profiles: ReadProfiles {
private_only: vec!["agent_private".to_string()],
private_plus_project: vec![
"agent_private".to_string(),
"project_shared".to_string(),
],
all_scopes: vec![
"agent_private".to_string(),
"project_shared".to_string(),
"org_shared".to_string(),
],
},
precedence: ScopePrecedence { agent_private: 30, project_shared: 20, org_shared: 10 },
write_allowed: ScopeWriteAllowed {
agent_private: true,
project_shared: true,
org_shared: true,
},
},
memory: Memory {
max_notes_per_add_event: 3,
max_note_chars: 240,
dup_sim_threshold: 0.92,
update_sim_threshold: 0.85,
candidate_k: 60,
top_k: 12,
policy: MemoryPolicy { rules: vec![] },
},
search: test_search(),
ranking: test_ranking(),
lifecycle: Lifecycle {
ttl_days: TtlDays {
plan: 14,
fact: 180,
preference: 0,
constraint: 0,
decision: 0,
profile: 0,
},
purge_deleted_after_days: 30,
purge_deprecated_after_days: 180,
},
security: Security {
bind_localhost_only: true,
reject_non_english: true,
redact_secrets_on_write: true,
evidence_min_quotes: 1,
evidence_max_quotes: 2,
evidence_max_quote_chars: 320,
auth_mode: "off".to_string(),
auth_keys: vec![],
},
chunking: Chunking {
enabled: true,
max_tokens: 512,
overlap_tokens: 128,
tokenizer_repo: "gpt2".to_string(),
},
context: None,
mcp: None,
}
}

pub(crate) fn test_search() -> Search {
Search {
expansion: SearchExpansion {
mode: "off".to_string(),
max_queries: 4,
include_original: true,
},
dynamic: SearchDynamic { min_candidates: 10, min_top_score: 0.12 },
prefilter: SearchPrefilter { max_candidates: 0 },
cache: SearchCache {
enabled: true,
expansion_ttl_days: 7,
rerank_ttl_days: 7,
max_payload_bytes: Some(262_144),
},
explain: SearchExplain {
retention_days: 7,
capture_candidates: false,
candidate_retention_days: 2,
write_mode: "outbox".to_string(),
},
recursive: SearchRecursive {
enabled: false,
max_depth: 2,
max_children_per_node: 4,
max_nodes_per_scope: 32,
max_total_nodes: 256,
},
graph_context: SearchGraphContext {
enabled: false,
max_facts_per_item: 16,
max_evidence_notes_per_fact: 16,
},
}
}

pub(crate) fn dummy_embedding_provider() -> EmbeddingProviderConfig {
EmbeddingProviderConfig {
provider_id: "local".to_string(),
api_base: "http://127.0.0.1:1".to_string(),
api_key: "test-key".to_string(),
path: "/".to_string(),
model: "local-hash".to_string(),
dimensions: 4_096,
timeout_ms: 1_000,
default_headers: Map::new(),
}
}

pub(crate) fn dummy_provider() -> ProviderConfig {
ProviderConfig {
provider_id: "local".to_string(),
api_base: "http://127.0.0.1:1".to_string(),
api_key: "test-key".to_string(),
path: "/".to_string(),
model: "local-token-overlap".to_string(),
timeout_ms: 1_000,
default_headers: Map::new(),
}
}

pub(crate) fn dummy_llm_provider() -> LlmProviderConfig {
LlmProviderConfig {
provider_id: "test".to_string(),
api_base: "http://127.0.0.1:1".to_string(),
api_key: "test-key".to_string(),
path: "/".to_string(),
model: "test".to_string(),
temperature: 0.1,
timeout_ms: 1_000,
default_headers: Map::new(),
}
}
38 changes: 38 additions & 0 deletions apps/elf-api/tests/http/helpers/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use axum::{
body::{self, Body},
http::{Request, StatusCode},
};
use serde_json::Value;
use tower::util::ServiceExt as _;

use elf_api::routes::{self, OPENAPI_JSON_PATH};

pub(crate) fn assert_openapi_method(spec: &Value, path: &str, method: &str) {
let operation = spec
.get("paths")
.and_then(|paths| paths.get(path))
.and_then(|path_item| path_item.get(method));

assert!(operation.is_some(), "Missing OpenAPI operation {method} {path}");
}

pub(crate) async fn contract_json() -> Value {
let app = routes::contract_router::<()>();
let response = app
.oneshot(
Request::builder()
.uri(OPENAPI_JSON_PATH)
.body(Body::empty())
.expect("Failed to build OpenAPI request."),
)
.await
.expect("Failed to call OpenAPI route.");

assert_eq!(response.status(), StatusCode::OK);

let body = body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("Failed to read OpenAPI response body.");

serde_json::from_slice(&body).expect("Failed to parse OpenAPI response.")
}
70 changes: 70 additions & 0 deletions apps/elf-api/tests/http/helpers/core_blocks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use axum::{Router, body, http::StatusCode};
use tower::util::ServiceExt as _;
use uuid::Uuid;

use crate::helpers::{self, TEST_AGENT_A};

pub(crate) async fn create_core_block(
admin_app: &Router,
scope: &str,
key: &str,
content: &str,
) -> Uuid {
let payload = serde_json::json!({
"scope": scope,
"key": key,
"title": "Operating context",
"content": content,
"source_ref": {
"schema": "core_block_source/v1",
"ref": { "issue": "XY-832" }
}
});
let (status, body) =
helpers::post_admin_json(admin_app, "/v2/admin/core-blocks", TEST_AGENT_A, payload).await;

assert_eq!(status, StatusCode::OK);

Uuid::parse_str(
body.pointer("/block/block_id")
.and_then(serde_json::Value::as_str)
.expect("Missing core block id."),
)
.expect("Invalid core block id.")
}

pub(crate) async fn attach_core_block(
admin_app: &Router,
block_id: Uuid,
target_agent_id: &str,
read_profile: &str,
) -> (StatusCode, serde_json::Value) {
let payload = serde_json::json!({
"target_agent_id": target_agent_id,
"read_profile": read_profile,
"reason": "Attach fixture block."
});
let uri = format!("/v2/admin/core-blocks/{block_id}/attachments");

helpers::post_admin_json(admin_app, uri, TEST_AGENT_A, payload).await
}

pub(crate) async fn get_core_blocks(
app: &Router,
agent_id: &str,
read_profile: &str,
) -> serde_json::Value {
let response = app
.clone()
.oneshot(helpers::context_request("GET", "/v2/core-blocks", agent_id, read_profile))
.await
.expect("Failed to fetch core blocks.");

assert_eq!(response.status(), StatusCode::OK);

let body = body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("Failed to read core blocks response body.");

serde_json::from_slice(&body).expect("Failed to parse core blocks response.")
}
Loading