Files
GEWU/tests/adapters/test_git_repository.py
T
2026-10-14 19:21:35 +08:00

93 lines
3.6 KiB
Python

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