Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ path = "src/lib.rs"
name = "selfware"
path = "src/main.rs"

[[test]]
name = "unit"
path = "tests/unit/mod.rs"

[[test]]
name = "integration"
path = "tests/integration/mod.rs"
Expand Down
9 changes: 9 additions & 0 deletions tests/unit/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! Unit tests for selfware modules
//!
//! These tests cover individual components without network I/O.

mod test_context;
mod test_file_extended;
mod test_git;
mod test_safety;
mod test_tools;
80 changes: 68 additions & 12 deletions tests/unit/test_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,51 @@ use selfware::agent::context::ContextCompressor;
use selfware::api::types::Message;

#[test]
fn test_compression_threshold() {
fn test_compression_threshold_small() {
let compressor = ContextCompressor::new(100000);


// Small messages should not trigger compression
let small: Vec<Message> = vec![Message::system("test")];
assert!(!compressor.should_compress(&small));

let mut large = vec![Message::system("test".repeat(10000))];
for _ in 0..20 {
large.push(Message::user("more content here".repeat(100)));
}
assert!(compressor.should_compress(&large));
}

#[test]
fn test_compression_threshold_large() {
// Use a smaller budget to make it easier to exceed
let compressor = ContextCompressor::new(1000);
// threshold = 1000 * 0.85 = 850 tokens

// Create messages that will exceed the threshold
// Each message: ~4000 chars / 4 factor + 50 = ~1050 tokens per message
let large: Vec<Message> = vec![
Message::system("A".repeat(4000)),
Message::user("B".repeat(4000)),
];

assert!(
compressor.should_compress(&large),
"Large messages should trigger compression"
);
}

#[test]
fn test_estimate_tokens_text() {
let compressor = ContextCompressor::new(10000);
let messages = vec![Message::user("hello world")]; // 11 chars / 4 + 50 = 52 tokens

let tokens = compressor.estimate_tokens(&messages);
assert!(tokens > 50 && tokens < 100);
}

#[test]
fn test_estimate_tokens_code() {
let compressor = ContextCompressor::new(10000);
// Code uses factor 3 (contains { or ;)
let messages = vec![Message::user("fn main() { println!(); }")]; // ~26 chars

let tokens = compressor.estimate_tokens(&messages);
// 26/3 + 50 = ~58 tokens
assert!(tokens > 55 && tokens < 70);
}

#[test]
Expand All @@ -21,12 +55,34 @@ fn test_hard_compress_preserves_recent() {
let messages = vec![
Message::system("system"),
Message::user("old1"),
Message::user("old2"),
Message::assistant("old2"),
Message::user("old3"),
Message::assistant("old4"),
Message::user("old5"),
Message::assistant("old6"),
Message::user("recent1"),
Message::user("recent2"),
Message::assistant("recent2"),
];


let compressed = compressor.hard_compress(&messages);
// Should preserve: system + min_messages_to_keep(6) recent + compression note
assert!(compressed.len() >= 2, "Should preserve at least system and some recent");
assert_eq!(compressed[0].role, "system");
}

#[test]
fn test_hard_compress_adds_compression_note() {
let compressor = ContextCompressor::new(100000);
let messages = vec![
Message::system("system"),
Message::user("user1"),
];

// hard_compress always adds compression note
let compressed = compressor.hard_compress(&messages);
assert_eq!(compressed.len(), 4); // system + 2 recent + note
// Result: system + compression note + last 3 messages + potential continuation prompt
assert!(compressed.len() >= 2, "Should have at least system and note");
assert_eq!(compressed[0].role, "system");
// Second message should be the compression note
assert!(compressed[1].content.contains("compressed"));
}
Loading
Loading