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
261 changes: 2 additions & 259 deletions apps/daemon/src/intelligence/repo_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,263 +272,6 @@ fn build_session_section(repo_path: &Path, recent_messages: &[ContextMessage]) -

// ── Tests ─────────────────────────────────────────────────────────────────────

// Tests live in repo_context/tests.rs.
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
use tempfile::TempDir;

// ── helpers ───────────────────────────────────────────────────────────────

fn make_dir() -> TempDir {
tempfile::tempdir().expect("tempdir")
}

fn create_file(dir: &TempDir, name: &str) {
let path = dir.path().join(name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok();
}
let mut f = std::fs::File::create(&path).expect("create file");
writeln!(f, "// {}", name).ok();
}

fn msg(content: &str) -> ContextMessage {
ContextMessage {
role: "user".to_string(),
content: content.to_string(),
pinned: false,
}
}

// ── structure section tests ───────────────────────────────────────────────

#[test]
fn test_structure_small_project_no_truncation() {
let dir = make_dir();
// Create 10 files at root
for i in 0..10 {
create_file(&dir, &format!("file{i}.rs"));
}
create_file(&dir, "Cargo.toml");

let result = build_structure_section(dir.path());
// All 11 files should appear — no truncation message
assert!(!result.contains("more files"), "should not truncate");
assert!(result.contains("Cargo.toml\n") || result.contains("file0.rs\n"));
}

#[test]
fn test_structure_large_project_truncated() {
let dir = make_dir();
// Create 80 files at root — exceeds MAX_ROOT_ENTRIES (50)
for i in 0..80 {
create_file(&dir, &format!("file{i:03}.rs"));
}

let result = build_structure_section(dir.path());
assert!(
result.contains("more files"),
"should have truncation note — got:\n{result}"
);
// Exactly 50 entries + truncation line
let file_lines: Vec<&str> = result.lines().filter(|l| l.starts_with("file")).collect();
assert_eq!(file_lines.len(), 50, "exactly 50 file entries expected");
}

#[test]
fn test_structure_hides_sensitive_files() {
let dir = make_dir();
create_file(&dir, "main.rs");
create_file(&dir, ".env");
create_file(&dir, "private.key");
create_file(&dir, "cert.pem");

let result = build_structure_section(dir.path());
assert!(result.contains("main.rs"), "main.rs should appear");
assert!(!result.contains(".env"), ".env must be hidden");
assert!(!result.contains("private.key"), "*.key must be hidden");
assert!(!result.contains("cert.pem"), "*.pem must be hidden");
}

#[test]
fn test_structure_hides_dot_git() {
let dir = make_dir();
create_file(&dir, "README.md");
// Create a .git directory
std::fs::create_dir(dir.path().join(".git")).ok();

let result = build_structure_section(dir.path());
assert!(result.contains("README.md"), "README should appear");
assert!(!result.contains(".git"), ".git must be hidden");
}

// ── session section tests ─────────────────────────────────────────────────

#[test]
fn test_session_file_ref_included() {
let dir = make_dir();
create_file(&dir, "src/auth.rs");

let messages = vec![
msg("Let's look at src/auth.rs for the auth logic"),
msg("The handler is in src/auth.rs:42"),
];

let result = build_session_section(dir.path(), &messages);
assert!(
result.contains("src/auth.rs"),
"src/auth.rs should appear in session context — got: {result}"
);
}

#[test]
fn test_session_deduplicates_refs() {
let dir = make_dir();
create_file(&dir, "src/main.rs");

let messages = vec![
msg("check src/main.rs"),
msg("also see src/main.rs"),
msg("and src/main.rs again"),
];

let result = build_session_section(dir.path(), &messages);
let count = result.matches("src/main.rs").count();
assert_eq!(count, 1, "should deduplicate repeated refs");
}

#[test]
fn test_session_no_path_traversal() {
let dir = make_dir();
let messages = vec![msg("look at ../../etc/passwd for details")];
let result = build_session_section(dir.path(), &messages);
assert!(
!result.contains("etc/passwd"),
"traversal path must be rejected"
);
}

#[test]
fn test_session_sensitive_refs_excluded() {
let dir = make_dir();
create_file(&dir, "config/.env");
create_file(&dir, "keys/server.key");

let messages = vec![msg("I edited config/.env and keys/server.key today")];
let result = build_session_section(dir.path(), &messages);
assert!(!result.contains(".env"), ".env refs must be excluded");
assert!(
!result.contains("server.key"),
"*.key refs must be excluded"
);
}

#[test]
fn test_session_only_last_5_messages() {
let dir = make_dir();
create_file(&dir, "src/old.rs");
create_file(&dir, "src/new.rs");

// 6 messages — only last 5 should be scanned.
// src/old.rs only in message #1 (index 0), src/new.rs in message #6 (index 5).
let messages = vec![
msg("see src/old.rs"), // index 0 — NOT in last 5
msg("nothing here"),
msg("nothing here"),
msg("nothing here"),
msg("nothing here"),
msg("check src/new.rs"), // index 5 — in last 5
];

let result = build_session_section(dir.path(), &messages);
assert!(result.contains("src/new.rs"), "src/new.rs should appear");
assert!(
!result.contains("src/old.rs"),
"src/old.rs is outside last 5 messages — should not appear"
);
}

// ── full build_repo_context tests ─────────────────────────────────────────

#[test]
fn test_full_output_under_max_chars() {
let dir = make_dir();
for i in 0..30 {
create_file(&dir, &format!("src/module{i}.rs"));
}
create_file(&dir, "Cargo.toml");
create_file(&dir, "README.md");

let messages = vec![msg("let's look at src/module0.rs")];
let output = build_repo_context(dir.path(), &messages).unwrap();

assert!(
output.len() <= MAX_CHARS,
"output length {} exceeds MAX_CHARS {}",
output.len(),
MAX_CHARS
);
}

#[test]
fn test_full_output_has_sections() {
let dir = make_dir();
create_file(&dir, "main.rs");
create_file(&dir, "lib.rs");

let output = build_repo_context(dir.path(), &[]).unwrap();
assert!(
output.contains("## Project Structure"),
"missing structure section"
);
}

#[test]
fn test_full_session_context_section_present() {
let dir = make_dir();
create_file(&dir, "src/auth.rs");
let messages = vec![msg("working on src/auth.rs today")];

let output = build_repo_context(dir.path(), &messages).unwrap();
assert!(
output.contains("## Session Context"),
"missing session section"
);
assert!(output.contains("src/auth.rs"));
}

#[test]
fn test_is_sensitive_patterns() {
assert!(is_sensitive(".env"));
assert!(is_sensitive(".env.local"));
assert!(is_sensitive(".envrc"));
assert!(is_sensitive("private.key"));
assert!(is_sensitive("cert.pem"));
assert!(is_sensitive("bundle.p12"));
assert!(is_sensitive("bundle.pfx"));
assert!(is_sensitive("api.secret"));
assert!(is_sensitive("server.crt"));
assert!(!is_sensitive("main.rs"));
assert!(!is_sensitive("Cargo.toml"));
assert!(!is_sensitive("README.md"));
assert!(!is_sensitive("env.rs")); // not a dotfile
}

#[test]
fn test_directories_have_trailing_slash() {
let dir = make_dir();
std::fs::create_dir(dir.path().join("src")).ok();
create_file(&dir, "main.rs");

let result = build_structure_section(dir.path());
assert!(
result.contains("src/"),
"directories should have trailing /"
);
assert!(
result.contains("main.rs"),
"files should not have trailing /"
);
}
}
mod tests;
Loading