from concurrent.futures import ThreadPoolExecutor from pathlib import Path from threading import Barrier import pytest from gewu.adapters.git_repository import ( GitCanonicalRepository, GitConflictError, GitRepositoryError, ) def test_bare_repository_commits_only_explicit_files_and_checks_expected_head( tmp_path: Path, ) -> None: repository = GitCanonicalRepository(tmp_path / "private" / "knowledge.git") base = repository.head() record_path = "projects/project-a/nodes/node-a.yaml" commit = repository.publish_files( expected_head=base, files={record_path: b"title: accepted\n"}, message="Accept fixture", index_directory=tmp_path / "staging" / "publishing", ) assert repository.read_file(commit, record_path) == b"title: accepted\n" assert repository.list_files(commit, "projects") == (record_path,) assert repository.list_files(commit, "sources") == () assert repository.read_file(commit, "unrelated/user-file.txt") is None assert repository.introducing_commit(commit, record_path) == commit with pytest.raises(GitConflictError, match="Canonical branch changed"): repository.publish_files( expected_head=base, files={"projects/project-b/nodes/node-b.yaml": b"title: stale\n"}, message="Stale writer", index_directory=tmp_path / "staging" / "publishing", ) def test_record_paths_reject_traversal_and_noncanonical_separators(tmp_path: Path) -> None: repository = GitCanonicalRepository(tmp_path / "knowledge.git") for unsafe in ("../escape", "/absolute", "projects//node", "projects/./node", "a\\b"): with pytest.raises(GitRepositoryError, match="record path is invalid"): repository.read_file(repository.head(), unsafe) def test_independent_repository_adapters_allow_one_compare_and_swap_winner( tmp_path: Path, ) -> None: repository_path = tmp_path / "knowledge.git" first_adapter = GitCanonicalRepository(repository_path) second_adapter = GitCanonicalRepository(repository_path) base = first_adapter.head() start = Barrier(2) index_directory = tmp_path / "staging" / "publishing" def publish(repository: GitCanonicalRepository, name: str) -> tuple[str, str | None]: path = f"projects/{name}/nodes/node.yaml" start.wait(timeout=5) try: revision = repository.publish_files( expected_head=base, files={path: f"title: {name}\n".encode()}, message=f"Publish {name}", index_directory=index_directory, ) except GitConflictError: return name, None return name, revision with ThreadPoolExecutor(max_workers=2) as workers: results = list( workers.map( lambda args: publish(*args), ((first_adapter, "project-a"), (second_adapter, "project-b")), ) ) winners = [(name, revision) for name, revision in results if revision is not None] losers = [name for name, revision in results if revision is None] assert len(winners) == len(losers) == 1 winner, winning_revision = winners[0] assert winning_revision is not None assert first_adapter.head() == winning_revision assert second_adapter.head() == winning_revision assert ( first_adapter.read_file(winning_revision, f"projects/{winner}/nodes/node.yaml") == f"title: {winner}\n".encode() ) loser = losers[0] assert first_adapter.read_file(winning_revision, f"projects/{loser}/nodes/node.yaml") is None