import hashlib import json import subprocess import sys from pathlib import Path import portalocker import pytest import yaml from gewu.adapters.git_repository import GitCanonicalRepository from gewu.application.registry import LocalContextProvider, LocalProjectRegistry from gewu.domain.contracts import AutoAcceptPolicy, ProjectRecord, ProjectStatus from gewu.domain.validation import extraction_digest from tests.domain.proposal_fixtures import EXTRACTION, NODE, POLICIES, PROJECTS def test_project_registry_create_and_archive_are_atomic_and_idempotent(tmp_path: Path) -> None: registry_file = tmp_path / "control" / "registry.json" registry = LocalProjectRegistry( registry_file, repository=GitCanonicalRepository(tmp_path / "knowledge.git"), index_directory=tmp_path / "publishing", authority_lock_path=tmp_path / "workflow" / "locks" / "canonical-writer.lock", ) project = ProjectRecord( project_id="new-research", title="New research", status=ProjectStatus.ACTIVE, owners=("owner-one",), north_star="Investigate a new research area.", key_questions=("What is known?",), autonomy_policy_id="new-policy", default_includes=("shared",), ) policy = AutoAcceptPolicy( policy_id="new-policy", policy_version=1, project_id="new-research", require_supported_assessment=True, ) assert registry.create(project, policy) == project assert registry.repository is not None revision_after_create = registry.repository.head() assert ( registry.repository.read_file(revision_after_create, "projects/new-research/project.md") is not None ) assert registry.repository.list_files(revision_after_create, "policies") canonical_context = LocalContextProvider(registry.repository, registry_file)() assert canonical_context.projects["new-research"] == project assert canonical_context.policies["new-policy"] == policy assert registry.records() == (project,) assert "new-research" in registry() lock_path = tmp_path / "workflow" / "locks" / "canonical-writer.lock" with portalocker.Lock(lock_path, mode="a", timeout=0): with pytest.raises(portalocker.exceptions.LockException): registry.archive("new-research") assert registry.archive("new-research").status == ProjectStatus.ARCHIVED assert registry.repository.head() != revision_after_create assert registry.archive("new-research").status == ProjectStatus.ARCHIVED assert registry.records()[0].status == ProjectStatus.ARCHIVED assert ( LocalContextProvider(registry.repository, registry_file)().projects["new-research"].status == ProjectStatus.ARCHIVED ) def test_archive_migrates_legacy_project_to_canonical_registry(tmp_path: Path) -> None: registry_file = tmp_path / "control" / "registry.json" registry_file.parent.mkdir(parents=True) registry_file.write_text( json.dumps( { "schema_version": 1, "projects": [project.model_dump(mode="json") for project in PROJECTS.values()], "policies": [policy.model_dump(mode="json") for policy in POLICIES.values()], } ), encoding="utf-8", ) repository = GitCanonicalRepository(tmp_path / "knowledge.git") registry = LocalProjectRegistry( registry_file, repository=repository, index_directory=tmp_path / "publishing", authority_lock_path=tmp_path / "workflow" / "locks" / "canonical-writer.lock", ) archived = registry.archive("dome-performance") context = LocalContextProvider(repository, registry_file)() assert archived.status == ProjectStatus.ARCHIVED assert context.projects["dome-performance"].status == ProjectStatus.ARCHIVED assert context.projects["interactive-video"] == PROJECTS["interactive-video"] assert context.policies == POLICIES def test_canonical_registry_retains_prior_extractions_after_parser_replacement( tmp_path: Path, ) -> None: repository = GitCanonicalRepository(tmp_path / "knowledge.git") prior = EXTRACTION replacement_candidate = prior.model_copy( update={"extraction_id": "0" * 64, "parser_version": "replacement-parser-1"} ) replacement = replacement_candidate.model_copy( update={"extraction_id": extraction_digest(replacement_candidate)} ) node_path = ( f"projects/{hashlib.sha256(NODE.project_id.encode()).hexdigest()}/nodes/" f"{hashlib.sha256(NODE.node_id.encode()).hexdigest()}.yaml" ) first_revision = repository.publish_files( expected_head=repository.head(), files={ f"extractions/{prior.extraction_id}.json": prior.model_dump_json().encode(), node_path: yaml.safe_dump(NODE.model_dump(mode="json")).encode(), }, message="Publish a cited node using the original parser extraction", index_directory=tmp_path / "publishing", ) replacement_revision = repository.publish_files( expected_head=repository.head(), files={ f"extractions/{replacement.extraction_id}.json": replacement.model_dump_json().encode() }, message="Add replacement parser extraction without deleting history", index_directory=tmp_path / "publishing", ) context = LocalContextProvider(repository, tmp_path / "control/registry.json")() assert context.extractions[prior.extraction_id] == prior assert context.extractions[replacement.extraction_id] == replacement assert context.existing_nodes[NODE.node_id] == NODE assert ( context.existing_nodes[NODE.node_id].claims[0].evidence[0].extraction_id == prior.extraction_id ) assert ( repository.read_file(first_revision, f"extractions/{prior.extraction_id}.json") is not None ) assert ( repository.read_file(replacement_revision, f"extractions/{prior.extraction_id}.json") is not None ) def test_disposable_registry_seed_loads_with_versioned_contracts( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("GEWU_DATA_ROOT", str(tmp_path)) result = subprocess.run( [sys.executable, "scripts/seed_test_registry.py"], check=False, capture_output=True, text=True, ) assert result.returncode == 0, result.stderr repository = GitCanonicalRepository(tmp_path / "knowledge.git") context = LocalContextProvider(repository, tmp_path / "control/registry.json")() assert set(context.projects) == {"dome-performance", "interactive-video"} assert set(context.policies) == {"dome-pilot-policy", "video-pilot-policy", "shared"}