232 lines
7.6 KiB
Python
232 lines
7.6 KiB
Python
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",
|
|
}
|