import json from datetime import UTC, date, datetime from pathlib import Path import pytest from pydantic import ValidationError from gewu.domain.contracts import ( AutoAcceptPolicy, Confidence, Locator, NodeType, ProjectRecord, ProjectStatus, ProposalReceipt, ProposalState, ReliabilityGrade, Scope, SearchRequest, SourceRevisionRecord, ) def test_project_contract_rejects_unknown_fields() -> None: with pytest.raises(ValidationError): ProjectRecord.model_validate( { "project_id": "dome", "title": "Dome", "status": ProjectStatus.ACTIVE, "north_star": "Understand interactive environments", "key_questions": ("What works?",), "autonomy_policy_id": "pilot", "untrusted_extra": "ignored?", } ) def test_project_status_supports_pause_without_losing_registry_record() -> None: project = ProjectRecord( project_id="dome", title="Dome", status=ProjectStatus.PAUSED, north_star="Understand interactive environments", key_questions=("What works?",), autonomy_policy_id="pilot", ) assert ( ProjectRecord.model_validate_json(project.model_dump_json()).status == ProjectStatus.PAUSED ) @pytest.mark.parametrize("project_id", ["Dome", "two_words", "project.slug"]) def test_project_ids_must_be_lowercase_slugs(project_id: str) -> None: with pytest.raises(ValidationError): ProjectRecord( project_id=project_id, title="Invalid project ID", status=ProjectStatus.ACTIVE, north_star="A stable project lens.", key_questions=("What matters?",), autonomy_policy_id="policy", ) def test_scope_requires_explicit_all_projects_flag() -> None: with pytest.raises(ValidationError): Scope(projects=("all",)) assert Scope(projects=("all",), all_projects=True) @pytest.mark.parametrize("unsafe_id", ["../private", "bad/id", " leading-space"]) def test_scope_rejects_unsafe_project_ids(unsafe_id: str) -> None: with pytest.raises(ValidationError): Scope(projects=(unsafe_id,)) def test_search_facets_are_typed_and_reject_duplicate_node_types() -> None: request = SearchRequest( query="audience", scope=Scope(projects=("dome-performance",)), node_types=(NodeType.PROBLEM, NodeType.TECHNIQUE), tags=("attendance", "real-time"), audience=("product",), use_for=("comparison",), domain=("production",), confidences=(Confidence.MEDIUM,), as_of_from=date(2024, 1, 1), as_of_to=date(2024, 12, 31), stale_only=True, budget_tokens=1000, ) assert request.stale_only assert request.tags == ("attendance", "real-time") assert request.domain == ("production",) assert request.confidences == (Confidence.MEDIUM,) assert request.as_of_from == date(2024, 1, 1) with pytest.raises(ValidationError, match="must not be later"): SearchRequest( query="audience", scope=Scope(projects=("dome-performance",)), as_of_from=date(2025, 1, 1), as_of_to=date(2024, 12, 31), budget_tokens=1000, ) with pytest.raises(ValidationError, match="confidences must be unique"): SearchRequest( query="audience", scope=Scope(projects=("dome-performance",)), confidences=(Confidence.MEDIUM, Confidence.MEDIUM), budget_tokens=1000, ) with pytest.raises(ValidationError): SearchRequest( query="audience", scope=Scope(projects=("dome-performance",)), node_types=(NodeType.PROBLEM, NodeType.PROBLEM), budget_tokens=1000, ) with pytest.raises(ValidationError): SearchRequest( query="audience", scope=Scope(projects=("dome-performance",)), tags=("attendance", "attendance"), budget_tokens=1000, ) with pytest.raises(ValidationError): SearchRequest( query="audience", scope=Scope(projects=("dome-performance",)), domain=(" production",), budget_tokens=1000, ) def test_search_allows_empty_query_only_for_a_typed_browse() -> None: request = SearchRequest( scope=Scope(projects=("dome-performance",)), node_types=(NodeType.PROBLEM,), budget_tokens=1000, ) assert request.query == "" with pytest.raises(ValidationError, match="search requires text"): SearchRequest( scope=Scope(projects=("dome-performance",)), budget_tokens=1000, ) with pytest.raises(ValidationError, match="search requires text"): SearchRequest( query=" ", scope=Scope(projects=("dome-performance",)), budget_tokens=1000, ) def test_pdf_locator_uses_one_based_page_and_nonempty_span() -> None: with pytest.raises(ValidationError): Locator(kind="pdf_page_span", block_id="p1-b1", start=8, end=8, page=0) def test_unarchived_source_requires_a_url_and_cannot_claim_artifact_bytes() -> None: base = { "source_id": "project:bookmark", "revision_id": "project:bookmark:r1", "media_type": "application/octet-stream", "byte_length": 0, "captured_at": datetime.now(UTC), "original_url": "https://example.org/paper", "title": "Referenced paper", "license": "unspecified", "source_type": "paper", "reliability_grade": ReliabilityGrade.MEDIUM, "ingested_for": ("dome-performance",), "archive_status": "reference-only", } record = SourceRevisionRecord.model_validate(base) assert record.artifact_sha256 is None with pytest.raises(ValidationError): SourceRevisionRecord.model_validate({**base, "byte_length": 10}) with pytest.raises(ValidationError): SourceRevisionRecord.model_validate({**base, "original_url": None}) def test_receipt_is_immutable_and_versioned() -> None: receipt = ProposalReceipt( proposal_id="proposal-1", revision=1, payload_digest="a" * 64, state=ProposalState.ACCEPTED, outcome="accepted", commit_id="b" * 64, decided_at=datetime.now(UTC), ) assert receipt.schema_version == 1 with pytest.raises(ValidationError): receipt.revision = 2 # type: ignore[misc] def test_pilot_registry_fixture_matches_project_and_policy_contracts() -> None: fixture_path = ( Path(__file__).resolve().parents[2] / "schema" / "fixtures" / "pilot-projects.json" ) def reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict[str, object]: result: dict[str, object] = {} for key, value in pairs: if key in result: raise ValueError(f"duplicate JSON key: {key}") result[key] = value return result fixture = json.loads( fixture_path.read_text(encoding="utf-8"), object_pairs_hook=reject_duplicate_keys ) projects = tuple( ProjectRecord.model_validate_json(json.dumps(record), strict=True) for record in fixture["projects"] ) policies = tuple( AutoAcceptPolicy.model_validate_json(json.dumps(record), strict=True) for record in fixture["test_policies"] ) assert {project.project_id for project in projects} == { "dome-performance", "interactive-video", } assert {policy.project_id for policy in policies} == { "dome-performance", "interactive-video", "shared", }