0.0.3-alpha.3
This commit is contained in:
312
crates/knowledge-git-lib/tests/git_write_candidate.rs
Normal file
312
crates/knowledge-git-lib/tests/git_write_candidate.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
// file: crates/knowledge-git-lib/tests/git_write_candidate.rs
|
||||
// version: 1
|
||||
|
||||
//! 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<std::ffi::OsString>) -> Option<Vec<u8>> {
|
||||
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<std::path::PathBuf> {
|
||||
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<knowledge_git_api::GitRevision> {
|
||||
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<knowledge_git_api::GitMutationPath> {
|
||||
let result = knowledge_git_api::GitMutationPath::new(value);
|
||||
match result {
|
||||
Ok(value) => return Some(value),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_identity() -> Option<knowledge_git_api::GitCommitIdentity> {
|
||||
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_eq!(snapshot_content, Ok(String::from("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;
|
||||
}
|
||||
Reference in New Issue
Block a user