use proptest::prelude::*;
mod workspace_tests {
use super::*;
use engram::types::{normalize_workspace, WorkspaceError, MAX_WORKSPACE_LENGTH};
proptest! {
#[test]
fn never_panics(s in ".*") {
let _ = normalize_workspace(&s);
}
#[test]
fn idempotent_when_valid(s in "[a-z0-9_-]{1,64}") {
if let Ok(normalized) = normalize_workspace(&s) {
let twice = normalize_workspace(&normalized);
prop_assert_eq!(Ok(normalized.clone()), twice);
}
}
#[test]
fn output_charset(s in "\\PC{1,100}") {
if let Ok(normalized) = normalize_workspace(&s) {
prop_assert!(normalized.chars().all(|c|
c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_'
));
}
}
#[test]
fn respects_max_length(s in "\\PC{1,200}") {
if let Ok(normalized) = normalize_workspace(&s) {
prop_assert!(normalized.len() <= MAX_WORKSPACE_LENGTH);
}
}
#[test]
fn empty_fails(s in "\\s*") {
let result = normalize_workspace(&s);
if s.trim().is_empty() {
prop_assert_eq!(result, Err(WorkspaceError::Empty));
}
}
#[test]
fn reserved_rejected(prefix in "_{1,5}", suffix in "[a-z0-9]{0,10}") {
let input = format!("{}{}", prefix, suffix);
let result = normalize_workspace(&input);
prop_assert!(result.is_err());
}
}
}
mod alias_tests {
use super::*;
use engram::storage::identity_links::normalize_alias;
proptest! {
#[test]
fn never_panics(s in ".*") {
let _ = normalize_alias(&s);
}
#[test]
fn idempotent(s in "\\PC{0,100}") {
let once = normalize_alias(&s);
let twice = normalize_alias(&once);
prop_assert_eq!(once, twice);
}
#[test]
fn lowercase_output(s in "\\PC{1,50}") {
let normalized = normalize_alias(&s);
prop_assert!(normalized.chars().all(|c| !c.is_ascii_uppercase()));
}
#[test]
fn no_boundary_whitespace(s in "\\PC{1,50}") {
let normalized = normalize_alias(&s);
let trimmed = normalized.trim().to_string();
prop_assert_eq!(normalized, trimmed);
}
#[test]
fn no_multiple_spaces(s in "\\PC{1,50}") {
let normalized = normalize_alias(&s);
prop_assert!(!normalized.contains(" "));
}
}
}
mod extraction_tests {
use super::*;
use engram::intelligence::entity_extraction::{extract_entities, ExtractionConfig};
proptest! {
#[test]
fn never_panics(s in "\\PC{0,1000}") {
let config = ExtractionConfig {
lookup_aliases: false,
..Default::default()
};
let _ = extract_entities(&s, &config, None);
}
#[test]
fn bounded_results(s in "\\PC{0,500}", max in 1usize..50) {
let config = ExtractionConfig {
lookup_aliases: false,
max_entities: max,
..Default::default()
};
let result = extract_entities(&s, &config, None);
prop_assert!(result.entities.len() <= max);
}
#[test]
fn empty_input_empty_result(s in "\\s*") {
let config = ExtractionConfig {
lookup_aliases: false,
..Default::default()
};
let result = extract_entities(&s, &config, None);
prop_assert!(result.entities.is_empty());
}
#[test]
fn entities_have_text(s in "@[a-z]{1,10}( @[a-z]{1,10})*") {
let config = ExtractionConfig {
lookup_aliases: false,
..Default::default()
};
let result = extract_entities(&s, &config, None);
for entity in &result.entities {
prop_assert!(!entity.mention_text.is_empty());
}
}
}
}
mod tier_tests {
use super::*;
use engram::types::MemoryTier;
proptest! {
#[test]
fn tier_roundtrip(tier in prop_oneof![Just(MemoryTier::Permanent), Just(MemoryTier::Daily)]) {
let s = tier.as_str();
let parsed: MemoryTier = s.parse().unwrap();
prop_assert_eq!(tier, parsed);
}
#[test]
fn unknown_tier_fails(s in "[a-z]{5,20}") {
if s != "permanent" && s != "daily" {
let result: Result<MemoryTier, _> = s.parse();
prop_assert!(result.is_err());
}
}
}
}
mod chunking_tests {
use super::*;
use chrono::Utc;
use engram::intelligence::session_indexing::{chunk_conversation, ChunkingConfig, Message};
fn make_messages(count: usize, content_len: usize) -> Vec<Message> {
(0..count)
.map(|i| Message {
role: if i % 2 == 0 {
"user".to_string()
} else {
"assistant".to_string()
},
content: "x".repeat(content_len),
timestamp: Utc::now(),
id: None,
})
.collect()
}
proptest! {
#[test]
fn never_panics(msg_count in 0usize..100, content_len in 1usize..500) {
let messages = make_messages(msg_count, content_len);
let config = ChunkingConfig::default();
let _ = chunk_conversation(&messages, &config);
}
#[test]
fn respects_max_messages(msg_count in 1usize..50, max_msgs in 1usize..20) {
let messages = make_messages(msg_count, 100);
let config = ChunkingConfig {
max_messages: max_msgs,
..Default::default()
};
let chunks = chunk_conversation(&messages, &config);
for chunk in &chunks {
prop_assert!(chunk.messages.len() <= max_msgs);
}
}
#[test]
fn empty_input_empty_chunks(_unused: u8) {
let messages: Vec<Message> = vec![];
let config = ChunkingConfig::default();
let chunks = chunk_conversation(&messages, &config);
prop_assert!(chunks.is_empty());
}
}
}
mod memory_type_tests {
use super::*;
use engram::types::MemoryType;
proptest! {
#[test]
fn roundtrip(memory_type in prop_oneof![
Just(MemoryType::Note),
Just(MemoryType::Todo),
Just(MemoryType::Issue),
Just(MemoryType::Decision),
Just(MemoryType::Preference),
Just(MemoryType::Learning),
Just(MemoryType::Context),
Just(MemoryType::Credential),
Just(MemoryType::Custom),
Just(MemoryType::TranscriptChunk),
]) {
let s = memory_type.as_str();
let parsed: MemoryType = s.parse().unwrap();
prop_assert_eq!(memory_type, parsed);
}
}
}
mod edge_type_tests {
use super::*;
use engram::types::EdgeType;
proptest! {
#[test]
fn roundtrip(edge_type in prop_oneof![
Just(EdgeType::RelatedTo),
Just(EdgeType::DependsOn),
Just(EdgeType::References),
Just(EdgeType::Blocks),
Just(EdgeType::FollowsUp),
Just(EdgeType::Supersedes),
Just(EdgeType::Contradicts),
Just(EdgeType::Implements),
Just(EdgeType::Extends),
]) {
let s = edge_type.as_str();
let parsed: EdgeType = s.parse().unwrap();
prop_assert_eq!(edge_type, parsed);
}
}
}