// file: crates/knowledge-git-lib/tests/git_write_candidate.rs // version: 2 //! Integration tests for isolated real Git candidate creation using local temporary repositories. fn test_root(label: &str) -> std::path::PathBuf { let nanos_result = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH); let nanos = match nanos_result { Ok(value) => value.as_nanos(), Err(_) => 0, }; let mut path = std::env::temp_dir(); path.push(format!("sasedev-knowledge-git-write-{label}-{}-{nanos}", std::process::id())); let _ = std::fs::remove_dir_all(&path); return path; } fn run_git(repository: &std::path::Path, arguments: &[&str]) -> bool { let mut command = std::process::Command::new("git"); command.current_dir(repository); command.args(arguments); command.env("GIT_TERMINAL_PROMPT", "0"); let status_result = command.status(); match status_result { Ok(status) => return status.success(), Err(_) => return false, } } fn run_git_output(arguments: Vec) -> Option> { let mut command = std::process::Command::new("git"); command.args(arguments); command.env("GIT_TERMINAL_PROMPT", "0"); let output_result = command.output(); let output = match output_result { Ok(value) => value, Err(_) => return None, }; if !output.status.success() { return None; } return Some(output.stdout); } fn prepare_source(root: &std::path::Path) -> Option { let source = root.join("source"); if std::fs::create_dir_all(&source).is_err() { return None; } if !run_git(&source, &["init", "-b", "main"]) { return None; } if !run_git(&source, &["config", "user.name", "Ambient Identity"]) { return None; } if !run_git(&source, &["config", "user.email", "ambient@example.invalid"]) { return None; } if !run_git(&source, &["config", "commit.gpgsign", "false"]) { return None; } if std::fs::write(source.join("content.txt"), "first\n").is_err() { return None; } if std::fs::write(source.join("delete.txt"), "delete me\n").is_err() { return None; } if !run_git(&source, &["add", "content.txt", "delete.txt"]) { return None; } if !run_git(&source, &["commit", "-m", "first"]) { return None; } return Some(source); } fn configured_backend(root: &std::path::Path, source: &std::path::Path) -> Option<(knowledge_git_lib::GitBackend, knowledge_domain_lib::SourceId)> { let source_id_result = knowledge_domain_lib::SourceId::new("integration-write-source"); let source_id = match source_id_result { Ok(value) => value, Err(_) => return None, }; let config_result = knowledge_git_lib::GitBackendConfig::new(root.join("state")); let config = match config_result { Ok(value) => value, Err(_) => return None, }; let source_result = knowledge_git_lib::GitSourceConfig::local(source_id.clone(), source.to_path_buf()); let source_config = match source_result { Ok(value) => value, Err(_) => return None, }; let mut backend = knowledge_git_lib::GitBackend::new(config); if backend.register_source(source_config).is_err() { return None; } return Some((backend, source_id)); } fn resolved_main(backend: &mut knowledge_git_lib::GitBackend, source_id: &knowledge_domain_lib::SourceId) -> Option { if knowledge_git_api::GitRepository::refresh(backend, source_id).is_err() { return None; } let reference = knowledge_git_api::GitReference::new("refs/heads/main"); let revision_result = knowledge_git_api::GitRepository::resolve_revision(backend, source_id, &reference); match revision_result { Ok(value) => return Some(value), Err(_) => return None, } } fn mutation_path(value: &str) -> Option { let result = knowledge_git_api::GitMutationPath::new(value); match result { Ok(value) => return Some(value), Err(_) => return None, } } fn commit_identity() -> Option { let result = knowledge_git_api::GitCommitIdentity::new("Knowledge Writer", "writer@example.invalid"); match result { Ok(value) => return Some(value), Err(_) => return None, } } #[test] fn candidate_commit_applies_exact_mutations_uses_explicit_identity_and_cleans_worktree() { let root = test_root("candidate"); let source_result = prepare_source(&root); assert!(source_result.is_some()); let source = match source_result { Some(value) => value, None => return, }; let backend_result = configured_backend(&root, &source); assert!(backend_result.is_some()); let (mut backend, source_id) = match backend_result { Some(value) => value, None => return, }; let base_result = resolved_main(&mut backend, &source_id); assert!(base_result.is_some()); let base = match base_result { Some(value) => value, None => return, }; let snapshot_result = knowledge_git_api::GitRepository::prepare_snapshot(&backend, &source_id, &base); assert!(snapshot_result.is_ok()); let snapshot = match snapshot_result { Ok(value) => value, Err(_) => return, }; let content_path = match mutation_path("content.txt") { Some(value) => value, None => return, }; let nested_path = match mutation_path("nested/new.txt") { Some(value) => value, None => return, }; let delete_path = match mutation_path("delete.txt") { Some(value) => value, None => return, }; let identity = match commit_identity() { Some(value) => value, None => return, }; let request_result = knowledge_git_api::GitWriteRequest::new( base.clone(), vec![ knowledge_git_api::GitFileMutation::Write { path: content_path, content: b"second\n".to_vec() }, knowledge_git_api::GitFileMutation::Write { path: nested_path, content: b"nested\n".to_vec() }, knowledge_git_api::GitFileMutation::Delete { path: delete_path }, ], identity, "candidate commit", ); assert!(request_result.is_ok()); let request = match request_result { Ok(value) => value, Err(_) => return, }; let candidate_result = backend.create_candidate(&source_id, &request); assert!(candidate_result.is_ok()); let candidate = match candidate_result { Ok(value) => value, Err(_) => return, }; assert_ne!(candidate, base); let repository_path = backend.repository_path(&source_id); let parent_output = run_git_output(vec![ std::ffi::OsString::from("--git-dir"), repository_path.as_os_str().to_os_string(), std::ffi::OsString::from("rev-parse"), std::ffi::OsString::from(format!("{}^1", candidate.as_str())), ]); assert!(parent_output.is_some()); let parent = match parent_output { Some(value) => String::from_utf8_lossy(&value).trim().to_owned(), None => return, }; assert_eq!(parent, base.as_str()); let content_output = run_git_output(vec![ std::ffi::OsString::from("--git-dir"), repository_path.as_os_str().to_os_string(), std::ffi::OsString::from("show"), std::ffi::OsString::from(format!("{}:content.txt", candidate.as_str())), ]); assert_eq!(content_output, Some(b"second\n".to_vec())); let nested_output = run_git_output(vec![ std::ffi::OsString::from("--git-dir"), repository_path.as_os_str().to_os_string(), std::ffi::OsString::from("show"), std::ffi::OsString::from(format!("{}:nested/new.txt", candidate.as_str())), ]); assert_eq!(nested_output, Some(b"nested\n".to_vec())); let deleted_output = run_git_output(vec![ std::ffi::OsString::from("--git-dir"), repository_path.as_os_str().to_os_string(), std::ffi::OsString::from("show"), std::ffi::OsString::from(format!("{}:delete.txt", candidate.as_str())), ]); assert!(deleted_output.is_none()); let identity_output = run_git_output(vec![ std::ffi::OsString::from("--git-dir"), repository_path.as_os_str().to_os_string(), std::ffi::OsString::from("show"), std::ffi::OsString::from("-s"), std::ffi::OsString::from("--format=%an%x00%ae%x00%cn%x00%ce"), std::ffi::OsString::from(candidate.as_str()), ]); assert!(identity_output.is_some()); let identity_text = match identity_output { Some(value) => String::from_utf8_lossy(&value).trim().to_owned(), None => return, }; assert_eq!(identity_text, "Knowledge Writer\0writer@example.invalid\0Knowledge Writer\0writer@example.invalid"); let candidate_ref = format!("refs/knowledge/candidates/{}", candidate.as_str()); let retained = run_git_output(vec![ std::ffi::OsString::from("--git-dir"), repository_path.as_os_str().to_os_string(), std::ffi::OsString::from("show-ref"), std::ffi::OsString::from("--verify"), std::ffi::OsString::from(candidate_ref), ]); assert!(retained.is_some()); let source_key = knowledge_git_lib::GitSourceKey::from_source_id(&source_id); assert!(!backend.config().write_worktrees_path().join(source_key.as_str()).join("candidate").exists()); let snapshot_content = std::fs::read_to_string(snapshot.path().join("content.txt")); assert!(matches!(snapshot_content.as_deref(), Ok("first\n"))); assert!(snapshot.path().join("delete.txt").is_file()); let _ = std::fs::remove_dir_all(&root); return; } #[cfg(unix)] #[test] fn candidate_rejects_symlink_escape_and_removes_owned_failed_worktree() { let root = test_root("symlink"); let source_result = prepare_source(&root); assert!(source_result.is_some()); let source = match source_result { Some(value) => value, None => return, }; let outside = root.join("outside"); assert!(std::fs::create_dir_all(&outside).is_ok()); assert!(std::os::unix::fs::symlink(&outside, source.join("escape")).is_ok()); assert!(run_git(&source, &["add", "escape"])); assert!(run_git(&source, &["commit", "-m", "add escape symlink"])); let backend_result = configured_backend(&root, &source); assert!(backend_result.is_some()); let (mut backend, source_id) = match backend_result { Some(value) => value, None => return, }; let base_result = resolved_main(&mut backend, &source_id); assert!(base_result.is_some()); let base = match base_result { Some(value) => value, None => return, }; let path = match mutation_path("escape/pwned.txt") { Some(value) => value, None => return, }; let identity = match commit_identity() { Some(value) => value, None => return, }; let request_result = knowledge_git_api::GitWriteRequest::new( base, vec![knowledge_git_api::GitFileMutation::Write { path, content: b"forbidden\n".to_vec() }], identity, "must fail", ); assert!(request_result.is_ok()); let request = match request_result { Ok(value) => value, Err(_) => return, }; let result = backend.create_candidate(&source_id, &request); assert_eq!(result, Err(knowledge_git_lib::GitBackendError::UnsafeMutationTarget)); assert!(!outside.join("pwned.txt").exists()); let source_key = knowledge_git_lib::GitSourceKey::from_source_id(&source_id); assert!(!backend.config().write_worktrees_path().join(source_key.as_str()).join("candidate").exists()); let _ = std::fs::remove_dir_all(&root); return; }