use hippmem_core::model::unit::{ContentType, Language, MemoryContent};
use hippmem_model::deterministic::extract::DeterministicExtractor;
use hippmem_model::traits::Extractor;
fn discover_fixture_locales() -> Vec<String> {
let dir = format!("{}/tests/fixtures/extract", env!("CARGO_MANIFEST_DIR"));
let mut locales = vec![];
if let Ok(entries) = std::fs::read_dir(&dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.ends_with(".json") {
locales.push(name.trim_end_matches(".json").to_string());
}
}
}
locales.sort();
if locales.is_empty() {
panic!("no locale fixtures found in extract/");
}
locales
}
fn load_fixture(locale: &str) -> serde_json::Value {
let path = format!(
"{}/tests/fixtures/extract/{}.json",
env!("CARGO_MANIFEST_DIR"),
locale
);
let data = std::fs::read_to_string(&path).expect("failed to read fixture");
serde_json::from_str(&data).expect("invalid fixture")
}
fn make_content(text: &str, locale: &str) -> MemoryContent {
MemoryContent {
raw: text.to_string(),
summary: None,
normalized: None,
language: if locale == "zh" {
Language::Zh
} else {
Language::En
},
content_type: ContentType::UserStatement,
}
}
#[test]
fn explicit_causal_extraction() {
for locale in discover_fixture_locales() {
let ext = DeterministicExtractor;
let fixture = load_fixture(&locale);
let content = make_content(fixture["causal_text"].as_str().unwrap(), &locale);
let result = ext.extract_sync_immediate(&content).unwrap();
assert!(
!result.explicit_causals.is_empty(),
"[{locale}] text containing '{}' should produce an explicit CausalClaim",
fixture["causal_label"].as_str().unwrap()
);
}
}
#[test]
fn entity_extraction_from_proper_names() {
for locale in discover_fixture_locales() {
let ext = DeterministicExtractor;
let fixture = load_fixture(&locale);
let content = make_content(fixture["tech_text"].as_str().unwrap(), &locale);
let result = ext.extract_sync_immediate(&content).unwrap();
assert!(
!result.entities.is_empty(),
"[{locale}] should extract at least one entity (Rust/Tantivy)"
);
}
}
#[test]
fn strong_extraction_preference_detection() {
for locale in discover_fixture_locales() {
let ext = DeterministicExtractor;
let fixture = load_fixture(&locale);
let content = make_content(fixture["pref_text"].as_str().unwrap(), &locale);
let result = ext.extract_sync_strong(&content).unwrap();
assert!(
!result.preferences.is_empty(),
"[{locale}] text containing '{}' should produce a PreferenceFrame",
fixture["pref_label"].as_str().unwrap()
);
}
}
#[test]
fn strong_extraction_low_confidence() {
for locale in discover_fixture_locales() {
let ext = DeterministicExtractor;
let fixture = load_fixture(&locale);
let content = make_content(fixture["decision_text"].as_str().unwrap(), &locale);
let result = ext.extract_sync_strong(&content).unwrap();
assert!(
result.confidence.value() <= 0.5,
"[{locale}] fallback backend confidence should be <= 0.5"
);
}
}
#[test]
fn backend_id_is_deterministic_rules() {
let ext = DeterministicExtractor;
assert_eq!(ext.backend_id(), "deterministic-rules");
}
#[test]
fn entity_extraction_person_and_place() {
for locale in discover_fixture_locales() {
let ext = DeterministicExtractor;
let fixture = load_fixture(&locale);
let content = make_content(fixture["entity_text"].as_str().unwrap(), &locale);
let result = ext.extract_sync_immediate(&content).unwrap();
let entity_texts: Vec<&str> = result.entities.iter().map(|e| e.text.as_str()).collect();
let person = fixture["entity_person"].as_str().unwrap();
let place = fixture["entity_place"].as_str().unwrap();
assert!(
entity_texts.contains(&person),
"[{locale}] should extract person name '{}', actual entities: {entity_texts:?}",
person
);
assert!(
entity_texts.contains(&place),
"[{locale}] should extract place name '{}', actual entities: {entity_texts:?}",
place
);
assert!(
entity_texts.contains(&"Rust"),
"[{locale}] should extract 'Rust', actual entities: {entity_texts:?}"
);
}
}
#[test]
fn ascii_uppercase_entities_still_work() {
for locale in discover_fixture_locales() {
let ext = DeterministicExtractor;
let fixture = load_fixture(&locale);
let content = make_content(fixture["compare_text"].as_str().unwrap(), &locale);
let result = ext.extract_sync_immediate(&content).unwrap();
let entity_texts: Vec<&str> = result.entities.iter().map(|e| e.text.as_str()).collect();
assert!(
entity_texts.contains(&"Python"),
"[{locale}] should extract Python, actual entities: {entity_texts:?}"
);
assert!(
entity_texts.contains(&"Java"),
"[{locale}] should extract Java, actual entities: {entity_texts:?}"
);
assert!(
entity_texts.contains(&"Rust"),
"[{locale}] should extract Rust, actual entities: {entity_texts:?}"
);
}
}
#[test]
fn named_entity_extraction_locale() {
for locale in discover_fixture_locales() {
let ext = DeterministicExtractor;
let fixture = load_fixture(&locale);
let content = make_content(fixture["cn_entity_text"].as_str().unwrap(), &locale);
let result = ext.extract_sync_immediate(&content).unwrap();
let entity_names: Vec<&str> = fixture["cn_entity_names"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert!(
!result.entities.is_empty(),
"[{locale}] should extract proper names ({}), but entity list is empty",
entity_names.join("/")
);
let entity_texts: Vec<&str> = result.entities.iter().map(|e| e.text.as_str()).collect();
let has_any = entity_names.iter().any(|name| entity_texts.contains(name));
assert!(
has_any,
"[{locale}] should extract at least one proper name, actual entities: {entity_texts:?}"
);
}
}
#[test]
fn english_entities_still_extracted_regression() {
let content = MemoryContent {
raw: "I use Rust and Tantivy for full-text search".to_string(),
summary: None,
normalized: None,
language: Language::En,
content_type: ContentType::UserStatement,
};
let ext = DeterministicExtractor;
let result = ext.extract_sync_immediate(&content).unwrap();
let entity_texts: Vec<&str> = result.entities.iter().map(|e| e.text.as_str()).collect();
assert!(
entity_texts.contains(&"Rust"),
"English input should extract Rust, actual entities: {entity_texts:?}"
);
assert!(
entity_texts.contains(&"Tantivy"),
"English input should extract Tantivy, actual entities: {entity_texts:?}"
);
}
#[test]
fn entity_extraction_supports_cjk() {
let ext = DeterministicExtractor;
let zh = load_fixture("zh");
{
let content = MemoryContent {
raw: zh["batch_text"].as_str().unwrap().to_string(),
summary: None,
normalized: None,
language: Language::Zh,
content_type: ContentType::UserStatement,
};
let result = ext.extract_sync_immediate(&content).unwrap();
let entity_texts: Vec<&str> = result.entities.iter().map(|e| e.text.as_str()).collect();
assert!(
entity_texts.contains(&"Transformer"),
"[zh] should extract 'Transformer' entity, actual: {entity_texts:?}"
);
}
}