use std::fs;
use git2::{Repository, Signature};
use tempfile::TempDir;
use whogitit::capture::pending::PendingBuffer;
use whogitit::capture::threeway::ThreeWayAnalyzer;
use whogitit::core::attribution::{AIAttribution, PromptInfo, SessionMetadata};
use whogitit::storage::notes::NotesStore;
use whogitit::storage::trailers::TrailerGenerator;
#[test]
fn test_full_workflow() {
let dir = TempDir::new().unwrap();
let repo = Repository::init(dir.path()).unwrap();
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test User").unwrap();
config.set_str("user.email", "test@example.com").unwrap();
let file_path = dir.path().join("test.rs");
fs::write(&file_path, "fn main() {}\n").unwrap();
{
let mut index = repo.index().unwrap();
index.add_path(std::path::Path::new("test.rs")).unwrap();
index.write().unwrap();
let tree_id = index.write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
let sig = Signature::now("Test User", "test@example.com").unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
.unwrap();
}
let ai_content = r#"fn main() {
println!("Hello, AI!");
}
"#;
let mut buffer = PendingBuffer::new("test-session-123", "claude-opus-4-5-20251101");
buffer.record_edit(
"test.rs",
Some("fn main() {}\n"),
ai_content,
"Edit",
"Add greeting to main function",
None,
);
assert!(buffer.has_changes());
assert_eq!(buffer.file_count(), 1);
let history = buffer.get_file_history("test.rs").unwrap();
assert_eq!(history.edits.len(), 1);
assert_eq!(history.original.content, "fn main() {}\n");
assert!(history.edits[0].after.content.contains("println"));
let final_content = r#"// Author: Test User
fn main() {
println!("Hello, AI!");
}
"#;
fs::write(&file_path, final_content).unwrap();
let result = ThreeWayAnalyzer::analyze_with_diff(history, final_content);
assert_eq!(result.path, "test.rs");
assert!(
result.summary.human_lines >= 1,
"Should have at least 1 human line"
);
assert!(
result.summary.ai_lines >= 1,
"Should have at least 1 AI line"
);
let first_line = &result.lines[0];
assert!(first_line.content.contains("Author"));
assert!(
first_line.source.is_human(),
"First line should be human-added"
);
}
#[test]
fn test_privacy_redaction() {
let redactor = whogitit::privacy::Redactor::default_patterns();
let sensitive = "Use api_key = sk-secret123 for auth with user@email.com";
let redacted = redactor.redact(sensitive);
assert!(!redacted.contains("sk-secret123"));
assert!(!redacted.contains("user@email.com"));
assert!(redacted.contains("[REDACTED]"));
}
#[test]
fn test_trailers() {
use whogitit::capture::snapshot::{AttributionSummary, FileAttributionResult};
use whogitit::core::attribution::ModelInfo;
let attribution = AIAttribution {
version: 2,
session: SessionMetadata {
session_id: "abc123".to_string(),
model: ModelInfo::claude("claude-opus-4-5-20251101"),
started_at: "2026-01-30T10:00:00Z".to_string(),
prompt_count: 5,
used_plan_mode: false,
subagent_count: 0,
},
prompts: vec![],
files: vec![FileAttributionResult {
path: "test.rs".to_string(),
lines: vec![],
summary: AttributionSummary {
total_lines: 10,
ai_lines: 5,
ai_modified_lines: 2,
human_lines: 3,
original_lines: 0,
unknown_lines: 0,
},
}],
};
let trailers = TrailerGenerator::generate(&attribution);
assert!(trailers.iter().any(|(k, _)| k == "AI-Session"));
assert!(trailers.iter().any(|(k, _)| k == "AI-Model"));
assert!(trailers
.iter()
.any(|(k, v)| k == "Co-Authored-By" && v.contains("Claude")));
}
#[test]
fn test_multiple_ai_edits() {
let mut buffer = PendingBuffer::new("test-session", "claude-opus-4-5-20251101");
buffer.record_edit(
"test.rs",
Some("original\n"),
"original\nfirst_ai_line\n",
"Edit",
"First prompt: add a line",
None,
);
buffer.record_edit(
"test.rs",
None, "original\nfirst_ai_line\nsecond_ai_line\n",
"Edit",
"Second prompt: add another line",
None,
);
let history = buffer.get_file_history("test.rs").unwrap();
assert_eq!(history.edits.len(), 2);
assert_eq!(history.edits[0].prompt_index, 0);
assert_eq!(history.edits[1].prompt_index, 1);
assert_eq!(history.edits[1].before.content, "original\nfirst_ai_line\n");
let result = ThreeWayAnalyzer::analyze(history, "original\nfirst_ai_line\nsecond_ai_line\n");
assert_eq!(result.summary.ai_lines, 2);
assert_eq!(result.summary.original_lines, 1);
let second_ai = result
.lines
.iter()
.find(|l| l.content == "second_ai_line")
.unwrap();
assert_eq!(second_ai.prompt_index, Some(1));
}
#[test]
fn test_human_modifies_ai_code() {
let mut buffer = PendingBuffer::new("test-session", "claude-opus-4-5-20251101");
buffer.record_edit(
"test.rs",
Some(""),
"fn hello() {\n println!(\"hello\");\n}\n",
"Write",
"Create hello function",
None,
);
let history = buffer.get_file_history("test.rs").unwrap();
let final_content = "fn hello() {\n println!(\"hello, world!\");\n}\n";
let result = ThreeWayAnalyzer::analyze_with_diff(history, final_content);
let fn_line = result
.lines
.iter()
.find(|l| l.content.contains("fn hello"))
.unwrap();
assert!(fn_line.source.is_ai(), "fn line should be AI");
let _println_line = result
.lines
.iter()
.find(|l| l.content.contains("println"))
.unwrap();
}
#[test]
fn test_new_file_attribution() {
let mut buffer = PendingBuffer::new("test-session", "claude-opus-4-5-20251101");
buffer.record_edit(
"new_file.rs",
None, "// New file\nfn new_func() {}\n",
"Write",
"Create new file",
None,
);
let history = buffer.get_file_history("new_file.rs").unwrap();
assert!(history.was_new_file);
assert!(history.original.content.is_empty());
let result = ThreeWayAnalyzer::analyze(history, "// New file\nfn new_func() {}\n");
assert_eq!(result.summary.ai_lines, 2);
assert_eq!(result.summary.human_lines, 0);
assert_eq!(result.summary.original_lines, 0);
}
#[test]
fn test_copy_attribution() {
use whogitit::capture::snapshot::{
AttributionSummary, FileAttributionResult, LineAttribution, LineSource,
};
use whogitit::core::attribution::ModelInfo;
let dir = TempDir::new().unwrap();
let repo = Repository::init(dir.path()).unwrap();
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test User").unwrap();
config.set_str("user.email", "test@example.com").unwrap();
let sig = Signature::now("Test", "test@test.com").unwrap();
let tree_id = repo.index().unwrap().write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
let first_commit = repo
.commit(Some("HEAD"), &sig, &sig, "First commit", &tree, &[])
.unwrap();
fs::write(dir.path().join("file.txt"), "content").unwrap();
let mut index = repo.index().unwrap();
index.add_path(std::path::Path::new("file.txt")).unwrap();
index.write().unwrap();
let tree_id = index.write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
let second_commit = repo
.commit(
Some("HEAD"),
&sig,
&sig,
"Second commit",
&tree,
&[&repo.find_commit(first_commit).unwrap()],
)
.unwrap();
let store = NotesStore::new(&repo).unwrap();
let attribution = AIAttribution {
version: 2,
session: SessionMetadata {
session_id: "copy-test-session".to_string(),
model: ModelInfo::claude("claude-opus-4-5-20251101"),
started_at: "2026-01-30T10:00:00Z".to_string(),
prompt_count: 1,
used_plan_mode: false,
subagent_count: 0,
},
prompts: vec![PromptInfo {
index: 0,
text: "Test copy functionality".to_string(),
timestamp: "2026-01-30T10:00:00Z".to_string(),
affected_files: vec!["test.rs".to_string()],
}],
files: vec![FileAttributionResult {
path: "test.rs".to_string(),
lines: vec![LineAttribution {
line_number: 1,
content: "fn test() {}".to_string(),
source: LineSource::AI {
edit_id: "e1".to_string(),
},
edit_id: Some("e1".to_string()),
prompt_index: Some(0),
confidence: 1.0,
}],
summary: AttributionSummary {
total_lines: 1,
ai_lines: 1,
ai_modified_lines: 0,
human_lines: 0,
original_lines: 0,
unknown_lines: 0,
},
}],
};
store.store_attribution(first_commit, &attribution).unwrap();
assert!(store.has_attribution(first_commit));
assert!(!store.has_attribution(second_commit));
store.copy_attribution(first_commit, second_commit).unwrap();
assert!(store.has_attribution(first_commit));
assert!(store.has_attribution(second_commit));
let original = store.fetch_attribution(first_commit).unwrap().unwrap();
let copied = store.fetch_attribution(second_commit).unwrap().unwrap();
assert_eq!(original.session.session_id, copied.session.session_id);
assert_eq!(original.prompts.len(), copied.prompts.len());
assert_eq!(original.files.len(), copied.files.len());
}
#[test]
fn test_notes_roundtrip() {
use whogitit::capture::snapshot::{
AttributionSummary, FileAttributionResult, LineAttribution, LineSource,
};
use whogitit::core::attribution::ModelInfo;
let dir = TempDir::new().unwrap();
let repo = Repository::init(dir.path()).unwrap();
{
let sig = Signature::now("Test", "test@test.com").unwrap();
let tree_id = repo.index().unwrap().write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "Initial", &tree, &[])
.unwrap();
}
let head = repo.head().unwrap().peel_to_commit().unwrap();
let store = NotesStore::new(&repo).unwrap();
let attribution = AIAttribution {
version: 2,
session: SessionMetadata {
session_id: "test-session".to_string(),
model: ModelInfo::claude("claude-opus-4-5-20251101"),
started_at: "2026-01-30T10:00:00Z".to_string(),
prompt_count: 1,
used_plan_mode: false,
subagent_count: 0,
},
prompts: vec![PromptInfo {
index: 0,
text: "Create test function".to_string(),
timestamp: "2026-01-30T10:00:00Z".to_string(),
affected_files: vec!["test.rs".to_string()],
}],
files: vec![FileAttributionResult {
path: "test.rs".to_string(),
lines: vec![LineAttribution {
line_number: 1,
content: "fn test() {}".to_string(),
source: LineSource::AI {
edit_id: "e1".to_string(),
},
edit_id: Some("e1".to_string()),
prompt_index: Some(0),
confidence: 1.0,
}],
summary: AttributionSummary {
total_lines: 1,
ai_lines: 1,
ai_modified_lines: 0,
human_lines: 0,
original_lines: 0,
unknown_lines: 0,
},
}],
};
store.store_attribution(head.id(), &attribution).unwrap();
let fetched = store.fetch_attribution(head.id()).unwrap().unwrap();
assert_eq!(fetched.version, 2);
assert_eq!(fetched.session.session_id, "test-session");
assert_eq!(fetched.files.len(), 1);
assert_eq!(fetched.prompts.len(), 1);
assert_eq!(fetched.prompts[0].text, "Create test function");
}