0.0.3-alpha.2

This commit is contained in:
2026-09-29 11:31:59 +02:00
parent 3004e4b581
commit 88a63b06cc
11 changed files with 881 additions and 9 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/knowledge-git-api/unit_tests/lib.rs
// version: 3
// version: 4
#[derive(Debug, Eq, PartialEq)]
struct FakeGitError;
@@ -22,6 +22,20 @@ impl crate::GitRepository for FakeGitRepository {
}
}
struct FakeGitWriter;
impl crate::GitRepositoryWriter for FakeGitWriter {
type Error = FakeGitError;
fn create_candidate(&mut self, _source_id: &knowledge_domain_lib::SourceId, request: &crate::GitWriteRequest) -> Result<crate::GitRevision, Self::Error> {
return Ok(crate::GitRevision::new(&format!("candidate:{}", request.base_revision().as_str())));
}
fn push_candidate(&mut self, _source_id: &knowledge_domain_lib::SourceId, request: &crate::GitPushRequest) -> Result<crate::GitPushOutcome, Self::Error> {
return Ok(crate::GitPushOutcome::Updated(request.candidate_revision().clone()));
}
}
#[test]
fn generic_repository_contract_does_not_require_a_forge() {
let source_result = knowledge_domain_lib::SourceId::new("local-pgr");
@@ -43,3 +57,140 @@ fn generic_repository_contract_does_not_require_a_forge() {
assert_eq!(snapshot_result, Ok(crate::GitSnapshot::new(revision, std::path::PathBuf::from("/controlled/snapshot"))));
return;
}
#[test]
fn mutation_paths_are_relative_normalized_and_protect_git_metadata() {
let valid = crate::GitMutationPath::new("docs/rules/200-GOVERNANCE.md");
assert_eq!(valid.as_ref().map(crate::GitMutationPath::as_str), Ok("docs/rules/200-GOVERNANCE.md"));
assert_eq!(crate::GitMutationPath::new(""), Err(crate::GitWriteInputError::EmptyMutationPath));
assert_eq!(crate::GitMutationPath::new("/etc/passwd"), Err(crate::GitWriteInputError::AbsoluteMutationPath));
assert_eq!(crate::GitMutationPath::new("C:/outside.txt"), Err(crate::GitWriteInputError::AbsoluteMutationPath));
assert_eq!(crate::GitMutationPath::new("docs\\rules.md"), Err(crate::GitWriteInputError::AbsoluteMutationPath));
assert_eq!(crate::GitMutationPath::new("docs/../RULES.md"), Err(crate::GitWriteInputError::UnnormalizedMutationPath));
assert_eq!(crate::GitMutationPath::new("docs/./RULES.md"), Err(crate::GitWriteInputError::UnnormalizedMutationPath));
assert_eq!(crate::GitMutationPath::new(".git/config"), Err(crate::GitWriteInputError::ProtectedGitPath));
assert_eq!(crate::GitMutationPath::new("nested/.GIT/config"), Err(crate::GitWriteInputError::ProtectedGitPath));
return;
}
#[test]
fn commit_identity_is_explicit_and_rejects_line_breaks() {
let identity = crate::GitCommitIdentity::new("Knowledge Publisher", "knowledge@example.invalid");
assert_eq!(identity.as_ref().map(crate::GitCommitIdentity::name), Ok("Knowledge Publisher"));
assert_eq!(identity.as_ref().map(crate::GitCommitIdentity::email), Ok("knowledge@example.invalid"));
assert_eq!(crate::GitCommitIdentity::new(" ", "knowledge@example.invalid"), Err(crate::GitWriteInputError::InvalidCommitIdentityName));
assert_eq!(
crate::GitCommitIdentity::new("Knowledge\nPublisher", "knowledge@example.invalid"),
Err(crate::GitWriteInputError::InvalidCommitIdentityName)
);
assert_eq!(crate::GitCommitIdentity::new("Knowledge Publisher", " "), Err(crate::GitWriteInputError::InvalidCommitIdentityEmail));
return;
}
#[test]
fn target_branch_is_limited_to_valid_heads_references() {
let branch = crate::GitBranch::new("refs/heads/knowledge/proposal-42");
assert_eq!(branch.as_ref().map(crate::GitBranch::as_str), Ok("refs/heads/knowledge/proposal-42"));
for invalid in [
"main",
"refs/tags/v1",
"refs/heads/",
"refs/heads/.hidden",
"refs/heads/topic..next",
"refs/heads/topic.lock",
"refs/heads/topic@{next",
"refs/heads/topic\\next",
"refs/heads/topic//next",
] {
assert_eq!(crate::GitBranch::new(invalid), Err(crate::GitWriteInputError::InvalidBranch), "{invalid}");
}
return;
}
#[test]
fn write_request_requires_mutations_unique_paths_and_message() {
let identity_result = crate::GitCommitIdentity::new("Knowledge Publisher", "knowledge@example.invalid");
let identity = match identity_result {
Ok(value) => value,
Err(_) => return,
};
let empty = crate::GitWriteRequest::new(crate::GitRevision::new("base"), Vec::new(), identity.clone(), "candidate");
assert_eq!(empty, Err(crate::GitWriteInputError::NoMutations));
let path_result = crate::GitMutationPath::new("content.md");
let path = match path_result {
Ok(value) => value,
Err(_) => return,
};
let duplicates =
vec![crate::GitFileMutation::Write { path: path.clone(), content: b"one".to_vec() }, crate::GitFileMutation::Delete { path: path.clone() }];
let duplicate_request = crate::GitWriteRequest::new(crate::GitRevision::new("base"), duplicates, identity.clone(), "candidate");
assert_eq!(duplicate_request, Err(crate::GitWriteInputError::DuplicateMutationPath));
let mutation = crate::GitFileMutation::Write { path, content: b"content".to_vec() };
let invalid_message = crate::GitWriteRequest::new(crate::GitRevision::new("base"), vec![mutation.clone()], identity.clone(), " ");
assert_eq!(invalid_message, Err(crate::GitWriteInputError::InvalidCommitMessage));
let request = crate::GitWriteRequest::new(crate::GitRevision::new("base"), vec![mutation], identity, "candidate");
assert!(request.is_ok());
return;
}
#[test]
fn push_request_models_absent_or_revision_remote_state() {
let branch_result = crate::GitBranch::new("refs/heads/proposals/example");
let branch = match branch_result {
Ok(value) => value,
Err(_) => return,
};
let candidate = crate::GitRevision::new("candidate");
let create = crate::GitPushRequest::new(branch.clone(), crate::GitRemoteState::Absent, candidate.clone());
assert_eq!(create.target_branch(), &branch);
assert_eq!(create.expected_remote(), &crate::GitRemoteState::Absent);
assert_eq!(create.candidate_revision(), &candidate);
let existing = crate::GitPushRequest::new(branch, crate::GitRemoteState::Revision(crate::GitRevision::new("old")), candidate);
assert_eq!(existing.expected_remote(), &crate::GitRemoteState::Revision(crate::GitRevision::new("old")));
return;
}
#[test]
fn write_contract_is_separate_from_read_contract_and_provider_neutral() {
let source_result = knowledge_domain_lib::SourceId::new("snippets-extra");
let source_id = match source_result {
Ok(value) => value,
Err(_) => return,
};
let path_result = crate::GitMutationPath::new("snippets/example.md");
let path = match path_result {
Ok(value) => value,
Err(_) => return,
};
let identity_result = crate::GitCommitIdentity::new("Knowledge Publisher", "knowledge@example.invalid");
let identity = match identity_result {
Ok(value) => value,
Err(_) => return,
};
let write_result = crate::GitWriteRequest::new(
crate::GitRevision::new("base"),
vec![crate::GitFileMutation::Write { path, content: b"snippet".to_vec() }],
identity,
"Add snippet",
);
let write_request = match write_result {
Ok(value) => value,
Err(_) => return,
};
let mut writer = FakeGitWriter;
let candidate_result = crate::GitRepositoryWriter::create_candidate(&mut writer, &source_id, &write_request);
assert_eq!(candidate_result, Ok(crate::GitRevision::new("candidate:base")));
let candidate = match candidate_result {
Ok(value) => value,
Err(_) => return,
};
let branch_result = crate::GitBranch::new("refs/heads/proposals/snippet");
let branch = match branch_result {
Ok(value) => value,
Err(_) => return,
};
let push_request = crate::GitPushRequest::new(branch, crate::GitRemoteState::Absent, candidate.clone());
let push_result = crate::GitRepositoryWriter::push_candidate(&mut writer, &source_id, &push_request);
assert_eq!(push_result, Ok(crate::GitPushOutcome::Updated(candidate)));
return;
}