Files
IdeA/crates/domain/tests/embedder_profile.rs
Blomios 785e9935fd feat(memory): config embedders (LOT C2) + suggestion contextuelle (LOT C3) + contexte projet partagé
- LOT C2 (§14.5.3) : use cases de configuration des embedders déclaratifs
  (List/Save/Delete + DescribeEmbedderEngines : modèles ONNX recommandés,
  environnement local détecté, stratégies compilées). UI EmbedderSettings.
- LOT C3 (§14.5.5) : suggestion contextuelle best-effort à l'activation quand la
  mémoire dépasse le budget de recall sans embedder configuré (event
  EmbedderSuggested, anti-spam 1×/session, « ne plus demander »).
- Contexte projet partagé .ideai/CONTEXT.md (model-agnostic) injecté à tous les
  agents/profils au lancement, avant la persona. UI ProjectContextPanel.

Tests : backend workspace vert (0 échec) ; frontend 306/306.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 09:24:51 +02:00

199 lines
5.7 KiB
Rust

//! Tests for the declarative [`EmbedderProfile`] / [`EmbedderStrategy`] (LOT C,
//! étage 2 vectoriel): serde camelCase round-trip, the validated `new`
//! constructor, and the dependency-free `none()` default.
use domain::{DomainError, EmbedderProfile, EmbedderStrategy};
fn roundtrip<T>(value: &T) -> T
where
T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug,
{
let json = serde_json::to_string(value).expect("serialize");
let back: T = serde_json::from_str(&json).expect("deserialize");
assert_eq!(&back, value, "round-trip mismatch via {json}");
back
}
// ---------------------------------------------------------------------------
// EmbedderStrategy: camelCase wire form (`localOnnx` / `localServer` / `api` /
// `none`).
// ---------------------------------------------------------------------------
#[test]
fn strategy_serializes_camel_case() {
assert_eq!(
serde_json::to_string(&EmbedderStrategy::LocalOnnx).unwrap(),
"\"localOnnx\""
);
assert_eq!(
serde_json::to_string(&EmbedderStrategy::LocalServer).unwrap(),
"\"localServer\""
);
assert_eq!(
serde_json::to_string(&EmbedderStrategy::Api).unwrap(),
"\"api\""
);
assert_eq!(
serde_json::to_string(&EmbedderStrategy::None).unwrap(),
"\"none\""
);
}
#[test]
fn strategy_deserializes_camel_case() {
assert_eq!(
serde_json::from_str::<EmbedderStrategy>("\"localOnnx\"").unwrap(),
EmbedderStrategy::LocalOnnx
);
assert_eq!(
serde_json::from_str::<EmbedderStrategy>("\"localServer\"").unwrap(),
EmbedderStrategy::LocalServer
);
assert_eq!(
serde_json::from_str::<EmbedderStrategy>("\"api\"").unwrap(),
EmbedderStrategy::Api
);
assert_eq!(
serde_json::from_str::<EmbedderStrategy>("\"none\"").unwrap(),
EmbedderStrategy::None
);
}
#[test]
fn strategy_roundtrips() {
for s in [
EmbedderStrategy::LocalOnnx,
EmbedderStrategy::LocalServer,
EmbedderStrategy::Api,
EmbedderStrategy::None,
] {
roundtrip(&s);
}
}
// ---------------------------------------------------------------------------
// EmbedderProfile: camelCase fields and optional-field skipping.
// ---------------------------------------------------------------------------
#[test]
fn profile_roundtrips_with_all_fields() {
let p = EmbedderProfile::new(
"local-onnx-minilm",
"Local MiniLM",
EmbedderStrategy::LocalOnnx,
Some("all-MiniLM-L6-v2".to_owned()),
Some("http://localhost:1234".to_owned()),
Some("OPENAI_API_KEY".to_owned()),
384,
)
.unwrap();
let back = roundtrip(&p);
assert_eq!(back.dimension, 384);
assert_eq!(back.strategy, EmbedderStrategy::LocalOnnx);
}
#[test]
fn profile_uses_camel_case_keys_and_skips_none_options() {
let p = EmbedderProfile::new(
"id-1",
"Name",
EmbedderStrategy::Api,
None,
Some("https://api.example/embed".to_owned()),
Some("MY_KEY".to_owned()),
16,
)
.unwrap();
let json = serde_json::to_string(&p).unwrap();
assert!(
json.contains("\"apiKeyEnv\":\"MY_KEY\""),
"camelCase apiKeyEnv: {json}"
);
assert!(
json.contains("\"strategy\":\"api\""),
"camelCase strategy: {json}"
);
// `model` is None ⇒ skipped from the wire form.
assert!(
!json.contains("\"model\""),
"None model must be skipped: {json}"
);
}
#[test]
fn profile_deserializes_from_camel_case_document() {
let json = r#"{
"id": "srv",
"name": "Server",
"strategy": "localServer",
"endpoint": "http://127.0.0.1:9000",
"dimension": 512
}"#;
let p: EmbedderProfile = serde_json::from_str(json).unwrap();
assert_eq!(p.id, "srv");
assert_eq!(p.strategy, EmbedderStrategy::LocalServer);
assert_eq!(p.endpoint.as_deref(), Some("http://127.0.0.1:9000"));
assert_eq!(p.dimension, 512);
assert!(p.model.is_none());
assert!(p.api_key_env.is_none());
}
// ---------------------------------------------------------------------------
// none(): the dependency-free default.
// ---------------------------------------------------------------------------
#[test]
fn none_profile_has_none_strategy() {
let p = EmbedderProfile::none();
assert_eq!(p.strategy, EmbedderStrategy::None);
assert_eq!(p.id, "none");
assert!(
p.dimension > 0,
"even the none profile keeps a non-zero dimension"
);
// And it round-trips like any other profile.
roundtrip(&p);
}
// ---------------------------------------------------------------------------
// new(): validation.
// ---------------------------------------------------------------------------
#[test]
fn new_rejects_empty_id() {
assert!(matches!(
EmbedderProfile::new("", "Name", EmbedderStrategy::None, None, None, None, 8),
Err(DomainError::EmptyField {
field: "embedder.id"
})
));
}
#[test]
fn new_rejects_empty_name() {
assert!(matches!(
EmbedderProfile::new("id", "", EmbedderStrategy::None, None, None, None, 8),
Err(DomainError::EmptyField {
field: "embedder.name"
})
));
}
#[test]
fn new_rejects_zero_dimension() {
assert!(matches!(
EmbedderProfile::new("id", "Name", EmbedderStrategy::None, None, None, None, 0),
Err(DomainError::EmptyField {
field: "embedder.dimension"
})
));
}
#[test]
fn new_accepts_valid_minimal_profile() {
let p =
EmbedderProfile::new("id", "Name", EmbedderStrategy::None, None, None, None, 1).unwrap();
assert_eq!(p.dimension, 1);
assert_eq!(p.strategy, EmbedderStrategy::None);
}