Files
g3/crates/g3-core/tests/test_context_thinning.rs
Dhanji R. Prasanna 182f5f98fe Centralize g3 status message formatting
Extract a new g3_status module in g3-cli that provides consistent formatting
for all 'g3:' prefixed system status messages.

Key changes:
- Add G3Status struct with methods for progress, done, failed, error, etc.
- Add Status enum with Done, Failed, Error, Resolved, Insufficient, NoChanges
- Add ThinResult struct in g3-core for semantic thinning data
- Update UiWriter trait with print_thin_result() method
- Refactor context thinning to return ThinResult instead of formatted strings
- Update all callers to use the new centralized formatting
- Session resume/decline messages now use G3Status
- Compaction status messages now use G3Status

This maintains clean separation of concerns: g3-core emits semantic data,
g3-cli handles all terminal formatting and colors.
2026-01-20 09:50:55 +05:30

278 lines
8.9 KiB
Rust

use g3_core::ContextWindow;
use g3_providers::{Message, MessageRole};
#[test]
fn test_thinning_thresholds() {
let mut context = ContextWindow::new(10000);
// At 0%, should not thin
assert!(!context.should_thin());
// Simulate reaching 50% usage
context.used_tokens = 5000;
assert!(context.should_thin());
// After thinning at 50%, should not thin again until next threshold
context.last_thinning_percentage = 50;
assert!(!context.should_thin());
// At 60%, should thin again
context.used_tokens = 6000;
assert!(context.should_thin());
// After thinning at 60%, should not thin
context.last_thinning_percentage = 60;
assert!(!context.should_thin());
// At 70%, should thin
context.used_tokens = 7000;
assert!(context.should_thin());
// At 80%, should thin
context.last_thinning_percentage = 70;
context.used_tokens = 8000;
assert!(context.should_thin());
// After 80%, should not thin (compaction takes over)
context.last_thinning_percentage = 80;
context.used_tokens = 8500;
assert!(!context.should_thin());
}
#[test]
fn test_thin_context_basic() {
let mut context = ContextWindow::new(10000);
// Add some messages to the first third
for i in 0..9 {
if i % 2 == 0 {
context.add_message(Message::new(
MessageRole::Assistant,
format!("Assistant message {}", i),
));
} else {
// Add tool results with varying sizes
let content = if i == 1 {
// Large tool result (> 1000 chars)
format!("Tool result: {}", "x".repeat(1500))
} else if i == 3 {
// Another large tool result
format!("Tool result: {}", "y".repeat(2000))
} else {
// Small tool result (< 1000 chars)
format!("Tool result: small result {}", i)
};
context.add_message(Message::new(MessageRole::User, content));
}
}
// Trigger thinning at 50%
context.used_tokens = 5000;
let result = context.thin_context(None);
println!("Thinning result: {:?}", result);
// Should have made changes
assert!(result.had_changes, "Expected thinning to make changes");
assert!(result.chars_saved > 0, "Expected chars to be saved");
// Check that the large tool results were replaced
let first_third_end = context.conversation_history.len() / 3;
for i in 0..first_third_end {
if let Some(msg) = context.conversation_history.get(i) {
if matches!(msg.role, MessageRole::User) && msg.content.starts_with("Tool result:") {
if msg.content.len() > 1000 {
panic!("Found un-thinned large tool result at index {}", i);
}
}
}
}
}
#[test]
fn test_thin_write_file_tool_calls() {
let mut context = ContextWindow::new(10000);
// Add some messages including a write_file tool call with large content
context.add_message(Message::new(
MessageRole::User,
"Please create a large file".to_string(),
));
// Add an assistant message with a write_file tool call containing large content
let large_content = "x".repeat(1500);
let tool_call_json = format!(
r#"{{"tool": "write_file", "args": {{"file_path": "test.txt", "content": "{}"}}}}"#,
large_content
);
context.add_message(Message::new(
MessageRole::Assistant,
format!("I'll create that file.\n\n{}", tool_call_json),
));
context.add_message(Message::new(
MessageRole::User,
"Tool result: ✅ Successfully wrote 1500 lines".to_string(),
));
// Add more messages to ensure we have enough for "first third" logic
for i in 0..6 {
context.add_message(Message::new(
MessageRole::Assistant,
format!("Response {}", i),
));
}
// Trigger thinning at 50%
context.used_tokens = 5000;
let result = context.thin_context(None);
println!("Thinning result: {:?}", result);
// Should have made changes
assert!(result.had_changes, "Expected thinning to make changes");
assert!(result.chars_saved > 0, "Expected chars to be saved");
// Check that the large content was replaced with a file reference
let first_third_end = context.conversation_history.len() / 3;
for i in 0..first_third_end {
if let Some(msg) = context.conversation_history.get(i) {
if matches!(msg.role, MessageRole::Assistant) && msg.content.contains("write_file") {
// The content should now reference an external file
assert!(msg.content.contains("<content saved to"));
assert!(!msg.content.contains(&large_content));
}
}
}
}
#[test]
fn test_thin_str_replace_tool_calls() {
let mut context = ContextWindow::new(10000);
// Add some messages including a str_replace tool call with large diff
context.add_message(Message::new(
MessageRole::User,
"Please update the file".to_string(),
));
// Add an assistant message with a str_replace tool call containing large diff
let large_diff = format!(
"--- old\n{}\n+++ new\n{}",
"-old line\n".repeat(100),
"+new line\n".repeat(100)
);
let tool_call_json = format!(
r#"{{"tool": "str_replace", "args": {{"file_path": "test.txt", "diff": "{}"}}}}"#,
large_diff.replace('\n', "\\n")
);
context.add_message(Message::new(
MessageRole::Assistant,
format!("I'll update that file.\n\n{}", tool_call_json),
));
context.add_message(Message::new(
MessageRole::User,
"Tool result: ✅ applied unified diff".to_string(),
));
// Add more messages to ensure we have enough for "first third" logic
for i in 0..6 {
context.add_message(Message::new(
MessageRole::Assistant,
format!("Response {}", i),
));
}
// Trigger thinning at 50%
context.used_tokens = 5000;
let result = context.thin_context(None);
println!("Thinning result: {:?}", result);
// Should have made changes
assert!(result.had_changes, "Expected thinning to make changes");
assert!(result.chars_saved > 0, "Expected chars to be saved");
// Check that the large diff was replaced with a file reference
let first_third_end = context.conversation_history.len() / 3;
for i in 0..first_third_end {
if let Some(msg) = context.conversation_history.get(i) {
if matches!(msg.role, MessageRole::Assistant) && msg.content.contains("str_replace") {
// The diff should now reference an external file
assert!(msg.content.contains("<diff saved to"));
// Should not contain the large diff content
assert!(!msg.content.contains("old line"));
}
}
}
}
#[test]
fn test_thin_context_no_large_results() {
let mut context = ContextWindow::new(10000);
// Add only small messages
for i in 0..9 {
context.add_message(Message::new(
MessageRole::User,
format!("Tool result: small {}", i),
));
}
context.used_tokens = 5000;
let result = context.thin_context(None);
// Should report no changes (no large results found)
assert!(!result.had_changes, "Expected no changes");
assert_eq!(result.chars_saved, 0, "Expected no chars saved");
assert_eq!(result.leaned_count, 0, "Expected no messages thinned");
}
#[test]
fn test_thin_context_only_affects_first_third() {
let mut context = ContextWindow::new(10000);
// Add 12 messages (first third = 4 messages)
for i in 0..12 {
let content = if i % 2 == 1 {
// All odd indices are large tool results
format!("Tool result: {}", "x".repeat(1500))
} else {
format!("Assistant message {}", i)
};
let role = if i % 2 == 1 {
MessageRole::User
} else {
MessageRole::Assistant
};
context.add_message(Message::new(role, content));
}
context.used_tokens = 5000;
let result = context.thin_context(None);
// Should have made changes
assert!(result.had_changes, "Expected thinning to make changes");
// Check that messages after the first third are NOT thinned
let first_third_end = context.conversation_history.len() / 3;
for i in first_third_end..context.conversation_history.len() {
if let Some(msg) = context.conversation_history.get(i) {
if matches!(msg.role, MessageRole::User) && msg.content.starts_with("Tool result:") {
// These should still be large (not thinned)
if i % 2 == 1 {
assert!(
msg.content.len() > 1000,
"Message at index {} should not have been thinned",
i
);
}
}
}
}
}