use rand::Rng;
use clawft_core::agent::context::{CompressionConfig, compress_context, count_tokens};
use clawft_core::pipeline::mutation::{MutationStrategy, TrajectoryHint, mutate_prompt};
use clawft_core::pipeline::scorer::FitnessScorer;
use clawft_core::pipeline::traits::{ChatRequest, LlmMessage, QualityScorer};
use clawft_types::provider::{ContentBlock, LlmResponse, StopReason, Usage};
use std::collections::HashMap;
fn random_string(rng: &mut impl Rng, word_count: usize) -> String {
let words: Vec<String> = (0..word_count)
.map(|_| {
let len = rng.gen_range(1..=12);
(0..len)
.map(|_| rng.gen_range(b'a'..=b'z') as char)
.collect::<String>()
})
.collect();
words.join(" ")
}
fn make_msg(role: &str, content: &str) -> LlmMessage {
LlmMessage {
role: role.into(),
content: content.into(),
tool_call_id: None,
tool_calls: None,
}
}
fn make_request_with_content(content: &str) -> ChatRequest {
ChatRequest {
messages: vec![LlmMessage {
role: "user".into(),
content: content.into(),
tool_call_id: None,
tool_calls: None,
}],
tools: vec![],
model: None,
max_tokens: None,
temperature: None,
auth_context: None,
complexity_boost: 0.0,
tool_choice: None,
}
}
#[allow(dead_code)]
fn make_response_with_text(text: &str, output_tokens: u32) -> LlmResponse {
LlmResponse {
id: "test".into(),
content: vec![ContentBlock::Text { text: text.into() }],
stop_reason: StopReason::EndTurn,
usage: Usage {
input_tokens: 10,
output_tokens,
total_tokens: 0,
},
metadata: HashMap::new(),
}
}
#[test]
fn count_tokens_monotonic_with_word_count() {
let mut rng = rand::thread_rng();
for _ in 0..200 {
let n1 = rng.gen_range(0..50);
let n2 = rng.gen_range(n1..=n1 + 50);
let s1 = random_string(&mut rng, n1);
let s2 = format!("{} {}", s1, random_string(&mut rng, n2 - n1));
let t1 = count_tokens(&s1);
let t2 = count_tokens(&s2);
assert!(
t2 >= t1,
"count_tokens must be monotonically increasing: \
{} words -> {} tokens, {} words -> {} tokens",
n1,
t1,
n2,
t2,
);
}
}
#[test]
fn count_tokens_empty_is_zero() {
assert_eq!(count_tokens(""), 0);
assert!(count_tokens(" ") <= 3);
assert!(count_tokens("\n\t") <= 3);
}
#[test]
fn count_tokens_single_word_at_least_one() {
let mut rng = rand::thread_rng();
for _ in 0..100 {
let word = random_string(&mut rng, 1);
let tokens = count_tokens(&word);
assert!(
tokens >= 1,
"single word '{}' should produce >= 1 token, got {}",
word,
tokens,
);
}
}
#[test]
fn compress_context_respects_budget() {
let mut rng = rand::thread_rng();
for _ in 0..50 {
let msg_count = rng.gen_range(1..=40);
let max_tokens = rng.gen_range(20..=500);
let mut messages = vec![make_msg("system", "You are helpful.")];
for _ in 0..msg_count {
let word_count = rng.gen_range(5..=50);
let text = random_string(&mut rng, word_count);
let role = if rng.gen_bool(0.5) {
"user"
} else {
"assistant"
};
messages.push(make_msg(role, &text));
}
let config = CompressionConfig {
max_context_tokens: max_tokens,
recent_message_count: rng.gen_range(1..=10),
compression_enabled: true,
};
let original_count = msg_count + 1; let result = compress_context(messages, &config);
if result.metadata.original_tokens > max_tokens {
assert!(
result.messages.len() <= original_count + 1, "compressed message count ({}) should be <= original + 1 ({})",
result.messages.len(),
original_count + 1,
);
assert!(
result.metadata.messages_summarized > 0 || result.messages.len() <= original_count,
"if tokens exceed budget, summarization should occur or messages should shrink",
);
}
}
}
#[test]
fn compress_context_disabled_returns_original() {
let mut rng = rand::thread_rng();
for _ in 0..20 {
let msg_count = rng.gen_range(1..=20);
let messages: Vec<LlmMessage> = (0..msg_count)
.map(|_| make_msg("user", &random_string(&mut rng, 10)))
.collect();
let config = CompressionConfig {
max_context_tokens: 1, recent_message_count: 5,
compression_enabled: false,
};
let original_len = messages.len();
let result = compress_context(messages, &config);
assert_eq!(result.messages.len(), original_len);
}
}
#[test]
fn compress_context_never_returns_empty_for_nonempty_input() {
let mut rng = rand::thread_rng();
for _ in 0..50 {
let msg_count = rng.gen_range(1..=30);
let messages: Vec<LlmMessage> = (0..msg_count)
.map(|i| {
let role = if i == 0 { "system" } else { "user" };
let wc = rng.gen_range(1..=20);
make_msg(role, &random_string(&mut rng, wc))
})
.collect();
let config = CompressionConfig {
max_context_tokens: rng.gen_range(1..=100),
recent_message_count: rng.gen_range(1..=5),
compression_enabled: true,
};
let result = compress_context(messages, &config);
assert!(
!result.messages.is_empty(),
"compress_context should never return empty for non-empty input",
);
}
}
#[test]
fn fitness_scorer_scores_in_unit_range() {
let mut rng = rand::thread_rng();
let scorer = FitnessScorer::new();
for _ in 0..200 {
let req_len = rng.gen_range(0..=50);
let resp_len = rng.gen_range(0..=200);
let request_text = random_string(&mut rng, req_len);
let response_text = random_string(&mut rng, resp_len);
let output_tokens = rng.gen_range(0..=10000);
let request = make_request_with_content(&request_text);
let stop_reason = if rng.gen_bool(0.2) {
StopReason::MaxTokens
} else {
StopReason::EndTurn
};
let response = LlmResponse {
id: "test".into(),
content: if response_text.is_empty() {
vec![]
} else {
vec![ContentBlock::Text {
text: response_text,
}]
},
stop_reason,
usage: Usage {
input_tokens: rng.gen_range(0..=1000),
output_tokens,
total_tokens: 0,
},
metadata: HashMap::new(),
};
let score = scorer.score(&request, &response);
assert!(
score.overall >= 0.0 && score.overall <= 1.0,
"overall must be in [0,1], got {}",
score.overall,
);
assert!(
score.relevance >= 0.0 && score.relevance <= 1.0,
"relevance must be in [0,1], got {}",
score.relevance,
);
assert!(
score.coherence >= 0.0 && score.coherence <= 1.0,
"coherence must be in [0,1], got {}",
score.coherence,
);
}
}
#[test]
fn fitness_scorer_empty_response_low_score() {
let scorer = FitnessScorer::new();
let request = make_request_with_content("Tell me about Rust");
let response = LlmResponse {
id: "empty".into(),
content: vec![],
stop_reason: StopReason::EndTurn,
usage: Usage {
input_tokens: 10,
output_tokens: 0,
total_tokens: 0,
},
metadata: HashMap::new(),
};
let score = scorer.score(&request, &response);
assert!(
score.overall < 0.5,
"empty response should score < 0.5, got {}",
score.overall,
);
}
#[test]
fn mutate_prompt_never_empty_for_nonempty_input() {
let mut rng = rand::thread_rng();
let strategies = [
MutationStrategy::Rephrase,
MutationStrategy::AddExamples,
MutationStrategy::RemoveIneffective,
MutationStrategy::Emphasize,
];
for _ in 0..100 {
let word_count = rng.gen_range(1..=50);
let prompt = random_string(&mut rng, word_count);
let trajectory_count = rng.gen_range(0..=5);
let trajectories: Vec<TrajectoryHint> = (0..trajectory_count)
.map(|_| {
let req_len = rng.gen_range(3..=20);
let fb_len = rng.gen_range(3..=15);
TrajectoryHint {
request_content: random_string(&mut rng, req_len),
quality_score: rng.gen_range(0.0..=1.0),
feedback: random_string(&mut rng, fb_len),
}
})
.collect();
for &strategy in &strategies {
let result = mutate_prompt(&prompt, &trajectories, strategy);
assert!(
!result.trim().is_empty(),
"mutate_prompt({:?}) returned empty for non-empty input '{}' \
with {} trajectories",
strategy,
&prompt[..prompt.len().min(40)],
trajectory_count,
);
}
}
}
#[test]
fn mutate_prompt_rephrase_idempotent_for_non_instructions() {
let mut rng = rand::thread_rng();
for _ in 0..50 {
let prefixes = ["The", "A", "My", "Our", "That", "This", "It"];
let prefix = prefixes[rng.gen_range(0..prefixes.len())];
let rest_len = rng.gen_range(3..=10);
let rest = random_string(&mut rng, rest_len);
let prompt = format!("{} {}", prefix, rest);
let result = mutate_prompt(&prompt, &[], MutationStrategy::Rephrase);
assert!(
result.trim().contains(prompt.trim()),
"Rephrase should preserve non-instruction line: '{}' not in '{}'",
prompt.trim(),
result.trim(),
);
}
}
#[test]
fn mutate_prompt_emphasize_adds_markers_to_instructions() {
let instruction_verbs = [
"Use", "Execute", "Run", "Create", "Write", "Read", "Check", "Ensure", "Verify", "Always",
"Never", "Avoid",
];
let mut rng = rand::thread_rng();
for verb in &instruction_verbs {
let rest_len = rng.gen_range(2..=8);
let rest = random_string(&mut rng, rest_len);
let prompt = format!("{} {}", verb, rest);
let result = mutate_prompt(&prompt, &[], MutationStrategy::Emphasize);
assert!(
result.contains("IMPORTANT:"),
"Emphasize should add IMPORTANT: to '{}'",
prompt,
);
}
}