Files
g3/crates/g3-core/tests/test_acd_integration.rs
Dhanji R. Prasanna 7bfb9efa19 Remove automatic README loading from context window
README.md is no longer auto-loaded into the LLM context at startup.
This saves ~4,600 tokens per session while AGENTS.md and memory.md
still provide all critical information for code tasks.

Changes:
- Delete read_project_readme() function
- Remove readme_content parameter from combine_project_content()
- Rename extract_readme_heading() -> extract_project_heading()
- Rename Agent constructors: *_with_readme_* -> *_with_project_context_*
- Update context preservation to only check for Agent Configuration
- Remove has_readme field from LoadedContent
- Update all tests to use new markers and function names

The LLM can still read README.md on-demand via read_file when needed.
2026-01-29 11:07:41 +11:00

312 lines
12 KiB
Rust

//! Integration tests for Aggressive Context Dehydration (ACD).
use g3_core::acd::{Fragment, list_fragments, get_latest_fragment_id};
use g3_core::context_window::ContextWindow;
use g3_providers::{Message, MessageRole};
/// Test that reset_with_summary_and_stub correctly adds stub before summary
#[test]
fn test_reset_with_summary_and_stub_ordering() {
let mut context = ContextWindow::new(100000);
// Add system prompt
context.add_message(Message::new(
MessageRole::System,
"You are a helpful assistant.".to_string(),
));
// Add some conversation (make it long enough to ensure chars_saved > 0)
context.add_message(Message::new(MessageRole::User, "Hello, I have a question about implementing a complex feature in my application. Can you help me understand how to structure the code properly?".to_string()));
context.add_message(Message::new(MessageRole::Assistant, "Of course! I'd be happy to help you with that. Let me explain the best practices for structuring your code. First, you should consider separating concerns into different modules...".to_string()));
context.add_message(Message::new(MessageRole::User, "That makes sense. What about error handling?".to_string()));
context.add_message(Message::new(MessageRole::Assistant, "Error handling is crucial. You should use Result types and proper error propagation throughout your codebase.".to_string()));
let stub = "---\n⚡ DEHYDRATED CONTEXT (fragment_id: test123)\n---".to_string();
let summary = "User greeted the assistant.".to_string();
let _chars_saved = context.reset_with_summary_and_stub(
summary.clone(),
Some("New question".to_string()),
Some(stub.clone()),
);
// chars_saved is old - new, which could be 0 or negative if summary is longer
// The important thing is that the function completed successfully
// Check message ordering:
// 1. System prompt
// 2. Stub (if present)
// 3. Summary
// 4. Latest user message
assert!(context.conversation_history.len() >= 3);
// First message should be system prompt
assert!(matches!(context.conversation_history[0].role, MessageRole::System));
assert!(context.conversation_history[0].content.contains("helpful assistant"));
// Find the stub message
let stub_idx = context.conversation_history.iter().position(|m|
m.content.contains("DEHYDRATED CONTEXT")
);
assert!(stub_idx.is_some(), "Stub message should be present");
// Find the summary message
let summary_idx = context.conversation_history.iter().position(|m|
m.content.contains("Previous conversation summary")
);
assert!(summary_idx.is_some(), "Summary message should be present");
// Stub should come before summary
assert!(stub_idx.unwrap() < summary_idx.unwrap(), "Stub should come before summary");
// Last message should be the user message
let last = context.conversation_history.last().unwrap();
assert!(matches!(last.role, MessageRole::User));
assert_eq!(last.content, "New question");
}
/// Test reset_with_summary_and_stub without stub (should behave like reset_with_summary)
#[test]
fn test_reset_with_summary_and_stub_no_stub() {
let mut context = ContextWindow::new(100000);
// Add system prompt
context.add_message(Message::new(
MessageRole::System,
"You are a helpful assistant.".to_string(),
));
// Add some conversation (make it long enough)
context.add_message(Message::new(MessageRole::User, "Hello, I have a question about implementing a complex feature in my application.".to_string()));
context.add_message(Message::new(MessageRole::Assistant, "Of course! I'd be happy to help you with that. Let me explain the best practices.".to_string()));
context.add_message(Message::new(MessageRole::User, "That makes sense. What about error handling?".to_string()));
context.add_message(Message::new(MessageRole::Assistant, "Error handling is crucial. You should use Result types.".to_string()));
let summary = "User greeted the assistant.".to_string();
// Call reset - we don't check chars_saved since it depends on content lengths
let _chars_saved = context.reset_with_summary_and_stub(
summary.clone(),
Some("New question".to_string()),
None, // No stub
);
// Should not have any dehydrated context message
let has_stub = context.conversation_history.iter().any(|m|
m.content.contains("DEHYDRATED CONTEXT")
);
assert!(!has_stub, "Should not have stub when None is passed");
// Should still have summary
let has_summary = context.conversation_history.iter().any(|m|
m.content.contains("Previous conversation summary")
);
assert!(has_summary, "Should have summary");
}
/// Test that project context message is preserved during reset
#[test]
fn test_reset_preserves_project_context() {
let mut context = ContextWindow::new(100000);
// Add system prompt
context.add_message(Message::new(
MessageRole::System,
"You are a helpful assistant.".to_string(),
));
// Add project context message (second system message with Agent Configuration)
context.add_message(Message::new(
MessageRole::System,
"🤖 Agent Configuration (from AGENTS.md):\nTest agent config.".to_string(),
));
// Add conversation
context.add_message(Message::new(MessageRole::User, "Hello".to_string()));
context.add_message(Message::new(MessageRole::Assistant, "Hi!".to_string()));
let stub = "---\n⚡ DEHYDRATED CONTEXT\n---".to_string();
context.reset_with_summary_and_stub(
"Summary".to_string(),
Some("Question".to_string()),
Some(stub),
);
// Project context should be preserved
let has_project_context = context.conversation_history.iter().any(|m|
m.content.contains("Agent Configuration")
);
assert!(has_project_context, "Project context message should be preserved");
}
/// Test fragment chain integrity
#[test]
fn test_fragment_chain_integrity() {
let test_session = format!("test_chain_{}", std::process::id());
// Create first fragment (no predecessor)
let messages1 = vec![
Message::new(MessageRole::User, "First message".to_string()),
Message::new(MessageRole::Assistant, "First response".to_string()),
];
let frag1 = Fragment::new(messages1, None);
let frag1_id = frag1.fragment_id.clone();
frag1.save(&test_session).unwrap();
// Create second fragment (links to first)
let messages2 = vec![
Message::new(MessageRole::User, "Second message".to_string()),
Message::new(MessageRole::Assistant, "Second response".to_string()),
];
let frag2 = Fragment::new(messages2, Some(frag1_id.clone()));
let frag2_id = frag2.fragment_id.clone();
frag2.save(&test_session).unwrap();
// Create third fragment (links to second)
let messages3 = vec![
Message::new(MessageRole::User, "Third message".to_string()),
Message::new(MessageRole::Assistant, "Third response".to_string()),
];
let frag3 = Fragment::new(messages3, Some(frag2_id.clone()));
let frag3_id = frag3.fragment_id.clone();
frag3.save(&test_session).unwrap();
// Verify chain by loading and following links
let loaded3 = Fragment::load(&test_session, &frag3_id).unwrap();
assert_eq!(loaded3.preceding_fragment_id, Some(frag2_id.clone()));
let loaded2 = Fragment::load(&test_session, &frag2_id).unwrap();
assert_eq!(loaded2.preceding_fragment_id, Some(frag1_id.clone()));
let loaded1 = Fragment::load(&test_session, &frag1_id).unwrap();
assert!(loaded1.preceding_fragment_id.is_none());
// Verify list_fragments returns all in order
let fragments = list_fragments(&test_session).unwrap();
assert_eq!(fragments.len(), 3);
// Verify get_latest_fragment_id returns the most recent
let latest = get_latest_fragment_id(&test_session).unwrap();
assert!(latest.is_some());
// Note: latest might be frag3 if sorted by creation time
// Cleanup
let fragments_dir = g3_core::paths::get_fragments_dir(&test_session);
let _ = std::fs::remove_dir_all(fragments_dir.parent().unwrap());
}
/// Test fragment with many messages
#[test]
fn test_large_fragment() {
let mut messages = Vec::new();
for i in 0..100 {
messages.push(Message::new(
MessageRole::User,
format!("User message {}", i),
));
messages.push(Message::new(
MessageRole::Assistant,
format!("Assistant response {} with some longer content to make it more realistic", i),
));
}
let fragment = Fragment::new(messages, None);
assert_eq!(fragment.message_count, 200);
assert_eq!(fragment.user_message_count, 100);
assert_eq!(fragment.assistant_message_count, 100);
assert!(fragment.estimated_tokens > 0);
// Stub should still be concise
let stub = fragment.generate_stub();
assert!(stub.len() < 1000, "Stub should be concise even for large fragments");
assert!(stub.contains("200 total msgs"));
}
/// Test fragment with tool calls
#[test]
fn test_fragment_tool_call_summary() {
let messages = vec![
Message::new(MessageRole::User, "Read the file".to_string()),
Message::new(
MessageRole::Assistant,
r#"{"tool": "read_file", "args": {"file_path": "test.rs"}}"#.to_string(),
),
Message::new(MessageRole::User, "Tool result: content".to_string()),
Message::new(MessageRole::User, "Now write it".to_string()),
Message::new(
MessageRole::Assistant,
r#"{"tool": "write_file", "args": {"file_path": "out.rs", "content": "..."}}"#.to_string(),
),
Message::new(
MessageRole::Assistant,
r#"{"tool": "shell", "args": {"command": "cargo build"}}"#.to_string(),
),
];
let fragment = Fragment::new(messages, None);
// Should have extracted tool calls
assert!(!fragment.tool_call_summary.is_empty());
// Stub should mention tool calls
let stub = fragment.generate_stub();
assert!(stub.contains("tool calls"));
}
/// Test context overflow detection in rehydration
#[test]
fn test_rehydration_context_overflow_detection() {
// Create a fragment with known token count
let messages = vec![
Message::new(MessageRole::User, "A".repeat(4000)), // ~1000 tokens
Message::new(MessageRole::Assistant, "B".repeat(4000)), // ~1000 tokens
];
let fragment = Fragment::new(messages, None);
// Fragment should have estimated tokens
assert!(fragment.estimated_tokens > 1000);
// The rehydrate tool checks available_tokens vs fragment_tokens
// This is tested in tools/acd.rs tests
}
/// Test empty session has no fragments
#[test]
fn test_empty_session_no_fragments() {
let test_session = format!("test_empty_{}", std::process::id());
let fragments = list_fragments(&test_session).unwrap();
assert!(fragments.is_empty());
let latest = get_latest_fragment_id(&test_session).unwrap();
assert!(latest.is_none());
}
/// Test fragment topics extraction from various message types
#[test]
fn test_topic_extraction_variety() {
let messages = vec![
Message::new(MessageRole::User, "Please implement the login feature".to_string()),
Message::new(MessageRole::Assistant, "I'll help with that.".to_string()),
Message::new(MessageRole::User, "Tool result: success".to_string()), // Should be skipped
Message::new(MessageRole::User, "Now add password hashing".to_string()),
Message::new(
MessageRole::Assistant,
r#"{"tool": "write_file", "args": {"file_path": "src/auth/password.rs", "content": "..."}}"#.to_string(),
),
];
let fragment = Fragment::new(messages, None);
// Should have extracted meaningful topics
assert!(!fragment.topics.is_empty());
// Should include user requests but not tool results
let topics_str = fragment.topics.join(" ");
assert!(topics_str.contains("login") || topics_str.contains("password"));
assert!(!topics_str.contains("Tool result"));
}