This commit is contained in:
1 parent
4db3f45164
commit
d25c04cdd2
175 files changed
+56243
-15
No files matched your search
@@ -0,0 +1 @@
|
||||
"""Domain contract tests and fixtures."""
|
||||
@@ -0,0 +1,212 @@
|
||||
"""Golden pilot proposal fixtures for deterministic evidence and policy checks."""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from gewu.domain.contracts import (
|
||||
AutoAcceptPolicy,
|
||||
Claim,
|
||||
Confidence,
|
||||
EvidenceReference,
|
||||
ExtractionBlock,
|
||||
ExtractionRecord,
|
||||
KnowledgeNode,
|
||||
LensFit,
|
||||
Lifecycle,
|
||||
Locator,
|
||||
NodeType,
|
||||
ProjectRecord,
|
||||
ProjectStatus,
|
||||
ProposalOperation,
|
||||
ProposalRevision,
|
||||
ReliabilityGrade,
|
||||
ReviewState,
|
||||
Scope,
|
||||
SourceRevisionRecord,
|
||||
SupportAssessment,
|
||||
SupportStatus,
|
||||
)
|
||||
from gewu.domain.validation import (
|
||||
assertion_digest,
|
||||
evidence_digest,
|
||||
extraction_digest,
|
||||
proposal_payload_digest,
|
||||
)
|
||||
|
||||
BASE_COMMIT = "1" * 40
|
||||
NOW = datetime(2026, 10, 13, tzinfo=UTC)
|
||||
SOURCE_REVISION = SourceRevisionRecord(
|
||||
source_id="shared:pilot-report",
|
||||
revision_id="shared:pilot-report:r1",
|
||||
artifact_sha256="2" * 64,
|
||||
media_type="text/markdown",
|
||||
byte_length=100,
|
||||
captured_at=NOW,
|
||||
original_url=None,
|
||||
title="Pilot dome installation report",
|
||||
authors=("Pilot team",),
|
||||
publisher="GEWU fixtures",
|
||||
license="internal",
|
||||
source_type="internal",
|
||||
reliability_grade=ReliabilityGrade.MEDIUM,
|
||||
ingested_for=("dome-performance",),
|
||||
archive_status="archived",
|
||||
)
|
||||
BLOCK_TEXT = "The installation hosted 400 visitors during the 2026 season."
|
||||
_EXTRACTION_UNHASHED = ExtractionRecord(
|
||||
extraction_id="0" * 64,
|
||||
source_revision_id=SOURCE_REVISION.revision_id,
|
||||
parser_name="gewu-text",
|
||||
parser_version="0.1.0",
|
||||
parser_config_sha256="4" * 64,
|
||||
normalization_profile="unicode-nfc-lf-v1",
|
||||
status="complete",
|
||||
blocks=(
|
||||
ExtractionBlock(
|
||||
block_id="block-1",
|
||||
ordinal=0,
|
||||
original_text=BLOCK_TEXT,
|
||||
normalized_text=BLOCK_TEXT,
|
||||
normalization_map=tuple((index, index + 1) for index in range(len(BLOCK_TEXT))),
|
||||
page=None,
|
||||
),
|
||||
),
|
||||
)
|
||||
EXTRACTION = _EXTRACTION_UNHASHED.model_copy(
|
||||
update={"extraction_id": extraction_digest(_EXTRACTION_UNHASHED)}
|
||||
)
|
||||
CLAIM_TEXT = "The installation hosted 400 visitors during the 2026 season."
|
||||
CLAIM_DIGEST = assertion_digest(CLAIM_TEXT)
|
||||
START = 0
|
||||
END = len(BLOCK_TEXT)
|
||||
EVIDENCE = EvidenceReference(
|
||||
source_revision_id=SOURCE_REVISION.revision_id,
|
||||
extraction_id=EXTRACTION.extraction_id,
|
||||
assertion_path="claims[0].text",
|
||||
assertion_digest=CLAIM_DIGEST,
|
||||
quote=BLOCK_TEXT,
|
||||
locator=Locator(kind="text_span", block_id="block-1", start=START, end=END),
|
||||
normalization_profile="unicode-nfc-lf-v1",
|
||||
)
|
||||
CLAIM = Claim(
|
||||
claim_id="dome:claim-visitors",
|
||||
text=CLAIM_TEXT,
|
||||
evidence_type="reported",
|
||||
evidence=(EVIDENCE,),
|
||||
assessment=SupportAssessment(
|
||||
assessment_id="assessment-1",
|
||||
assertion_digest=CLAIM_DIGEST,
|
||||
evidence_digests=(evidence_digest(EVIDENCE),),
|
||||
assessor_id="agent-pilot",
|
||||
run_id="run-pilot-1",
|
||||
method="paired source claim check",
|
||||
status=SupportStatus.SUPPORTED,
|
||||
rationale="The quoted sentence states the count and period.",
|
||||
),
|
||||
)
|
||||
NODE = KnowledgeNode(
|
||||
node_id="dome:season-visitors",
|
||||
project_id="dome-performance",
|
||||
node_type=NodeType.DATAPOINT,
|
||||
title="Dome season visitors",
|
||||
summary="The installation hosted 400 visitors in the 2026 season.",
|
||||
created=NOW,
|
||||
updated=NOW,
|
||||
review_state=ReviewState.DRAFT,
|
||||
lifecycle=Lifecycle.ACTIVE,
|
||||
confidence=Confidence.MEDIUM,
|
||||
claim_support="supported",
|
||||
tags=("attendance",),
|
||||
audience=("product",),
|
||||
domain=("production",),
|
||||
use_for=("comparison",),
|
||||
lens_fit={
|
||||
"dome-performance": LensFit(
|
||||
score=0.9,
|
||||
horizon="H1",
|
||||
key_question="How do audiences engage?",
|
||||
reason="Reports a representative audience measure.",
|
||||
),
|
||||
},
|
||||
run_id="run-pilot-1",
|
||||
agent_id="agent-pilot",
|
||||
model_id="fixture-model",
|
||||
claims=(CLAIM,),
|
||||
as_of=NOW,
|
||||
stale_after=datetime(2027, 1, 1, tzinfo=UTC),
|
||||
value=400,
|
||||
unit="visitors",
|
||||
sample_size=None,
|
||||
reporting_year=2026,
|
||||
)
|
||||
PROJECTS = {
|
||||
"dome-performance": ProjectRecord(
|
||||
project_id="dome-performance",
|
||||
title="Dome interactive content",
|
||||
status=ProjectStatus.ACTIVE,
|
||||
north_star="Create compelling interactive experiences for dome audiences.",
|
||||
key_questions=("How do audiences engage?",),
|
||||
autonomy_policy_id="dome-policy",
|
||||
),
|
||||
"interactive-video": ProjectRecord(
|
||||
project_id="interactive-video",
|
||||
title="Interactive video",
|
||||
status=ProjectStatus.ACTIVE,
|
||||
north_star="Understand useful interactive video experiences.",
|
||||
key_questions=("How do teams control branching?",),
|
||||
autonomy_policy_id="video-policy",
|
||||
),
|
||||
}
|
||||
POLICIES = {
|
||||
"dome-policy": AutoAcceptPolicy(
|
||||
policy_id="dome-policy",
|
||||
policy_version=1,
|
||||
project_id="dome-performance",
|
||||
automatic_node_types=("datapoint", "problem", "technique", "case"),
|
||||
automatic_source_registration=True,
|
||||
require_supported_assessment=True,
|
||||
),
|
||||
"video-policy": AutoAcceptPolicy(
|
||||
policy_id="video-policy",
|
||||
policy_version=1,
|
||||
project_id="interactive-video",
|
||||
automatic_node_types=("datapoint", "problem", "technique", "case"),
|
||||
automatic_source_registration=True,
|
||||
require_supported_assessment=True,
|
||||
),
|
||||
"shared": AutoAcceptPolicy(
|
||||
policy_id="shared",
|
||||
policy_version=1,
|
||||
project_id="shared",
|
||||
automatic_source_registration=True,
|
||||
require_supported_assessment=True,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def proposal(
|
||||
*,
|
||||
node: KnowledgeNode | None = NODE,
|
||||
source: SourceRevisionRecord | None = SOURCE_REVISION,
|
||||
scope: Scope | None = None,
|
||||
base_commit: str = BASE_COMMIT,
|
||||
) -> ProposalRevision:
|
||||
operations = tuple(
|
||||
operation
|
||||
for operation in (
|
||||
ProposalOperation(operation="register_source", source=source) if source else None,
|
||||
ProposalOperation(operation="upsert_node", node=node) if node else None,
|
||||
)
|
||||
if operation is not None
|
||||
)
|
||||
candidate = ProposalRevision(
|
||||
proposal_id="proposal-pilot-1",
|
||||
revision=1,
|
||||
initiating_project_id="dome-performance",
|
||||
scope=scope or Scope(projects=("dome-performance",), include_shared=True),
|
||||
base_commit=base_commit,
|
||||
payload_digest="0" * 64,
|
||||
proposer_id="agent-pilot",
|
||||
run_id="run-pilot-1",
|
||||
operations=operations,
|
||||
)
|
||||
return candidate.model_copy(update={"payload_digest": proposal_payload_digest(candidate)})
|
||||
@@ -0,0 +1,14 @@
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from scripts.export_agent_contract import PACK
|
||||
|
||||
|
||||
def test_pack_manifest_and_nested_contract_hashes_verify() -> None:
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(PACK / "scripts" / "manifest_tool.py"), "verify"],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
@@ -0,0 +1,40 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from gewu.domain.operations import OPERATIONS, Transport, catalog_bytes, operations_for
|
||||
|
||||
|
||||
def test_read_http_catalog_contains_no_mutation_operations() -> None:
|
||||
http_operations = operations_for(Transport.HTTP)
|
||||
assert all(not operation.mutates_knowledge for operation in http_operations)
|
||||
assert not {"proposal_submit", "owner_decide"} & {item.name for item in http_operations}
|
||||
|
||||
|
||||
def test_owner_decision_is_separate_from_agent_mcp() -> None:
|
||||
agent_operations = {item.name for item in operations_for(Transport.MCP)}
|
||||
owner_operations = {item.name for item in operations_for(Transport.OWNER_MCP)}
|
||||
assert "owner_decide" not in agent_operations
|
||||
assert owner_operations == {"owner_challenge_get", "owner_diff_get", "owner_decide"}
|
||||
|
||||
|
||||
def test_stdio_connector_has_only_selected_file_operations() -> None:
|
||||
stdio_operations = {item.name for item in operations_for(Transport.STDIO)}
|
||||
assert stdio_operations == {"list_selected_files", "upload_selected_file"}
|
||||
|
||||
|
||||
def test_exported_catalog_matches_the_application_allowlist() -> None:
|
||||
path = Path(__file__).resolve().parents[2] / "schema" / "v1" / "catalog.json"
|
||||
catalog = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert path.read_bytes() == catalog_bytes()
|
||||
names_by_transport = {
|
||||
transport.value: {operation.name for operation in operations_for(transport)}
|
||||
for transport in Transport
|
||||
}
|
||||
entries = {operation["name"]: operation for operation in catalog["operations"]}
|
||||
assert set(entries) == {operation.name for operation in OPERATIONS}
|
||||
for name, entry in entries.items():
|
||||
assert set(entry["transports"]) == {
|
||||
transport for transport, names in names_by_transport.items() if name in names
|
||||
}
|
||||
expected_mutates = next(item.mutates_knowledge for item in OPERATIONS if item.name == name)
|
||||
assert entry["mutates_knowledge"] is expected_mutates
|
||||
@@ -0,0 +1,231 @@
|
||||
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",
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import stat
|
||||
from pathlib import Path
|
||||
|
||||
import agents.gewu.scripts.run_checkpoint as checkpoint_module
|
||||
import pytest
|
||||
from agents.gewu.scripts.run_checkpoint import (
|
||||
create_checkpoint,
|
||||
load_checkpoint,
|
||||
update_checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def test_run_checkpoint_records_scope_coverage_receipt_and_pins(tmp_path: Path) -> None:
|
||||
workspace = tmp_path / "research-workspace"
|
||||
checkpoint = create_checkpoint(
|
||||
workspace,
|
||||
run_id="run-2026-01",
|
||||
objective="Compare audience response",
|
||||
projects=["dome-performance"],
|
||||
include_shared=True,
|
||||
lens="Audience behavior",
|
||||
budgets={"sources": 8},
|
||||
)
|
||||
path = workspace / "Runs" / "run-2026-01.json"
|
||||
assert stat.S_IMODE(path.stat().st_mode) == 0o600
|
||||
assert checkpoint["pack"]["snapshot_digest"]
|
||||
|
||||
updated = update_checkpoint(
|
||||
workspace,
|
||||
"run-2026-01",
|
||||
{
|
||||
"stage": "report",
|
||||
"sources": [
|
||||
{
|
||||
"source_revision_id": "source:r1",
|
||||
"extraction_id": "a" * 64,
|
||||
"status": "extracted",
|
||||
}
|
||||
],
|
||||
"coverage": {
|
||||
"selected": ["section-1", "section-2"],
|
||||
"included": ["section-1"],
|
||||
"excluded": [{"item": "section-2", "reason": "outside project lens"}],
|
||||
"skipped": [],
|
||||
},
|
||||
"pending_operations": [
|
||||
{
|
||||
"operation_id": "proposal-op-1",
|
||||
"tool_name": "proposal_submit",
|
||||
"input_sha256": "b" * 64,
|
||||
"status": "complete",
|
||||
}
|
||||
],
|
||||
"proposals": [
|
||||
{
|
||||
"proposal_id": "proposal-1",
|
||||
"revision": 1,
|
||||
"status": "accepted",
|
||||
"receipt": {
|
||||
"proposal_id": "proposal-1",
|
||||
"revision": 1,
|
||||
"state": "accepted",
|
||||
"commit_id": "c" * 40,
|
||||
},
|
||||
}
|
||||
],
|
||||
"outputs": [{"sha256": "d" * 64}],
|
||||
},
|
||||
)
|
||||
resumed = load_checkpoint(workspace, "run-2026-01")
|
||||
assert resumed == updated
|
||||
assert resumed["coverage"]["excluded"][0]["reason"] == "outside project lens"
|
||||
assert resumed["proposals"][0]["receipt"]["commit_id"] == "c" * 40
|
||||
with pytest.raises(ValueError, match="does not match"):
|
||||
update_checkpoint(
|
||||
workspace,
|
||||
"run-2026-01",
|
||||
{
|
||||
"proposals": [
|
||||
{
|
||||
"proposal_id": "proposal-1",
|
||||
"revision": 1,
|
||||
"status": "accepted",
|
||||
"receipt": {
|
||||
"proposal_id": "proposal-other",
|
||||
"revision": 1,
|
||||
"state": "accepted",
|
||||
"commit_id": "c" * 40,
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_run_checkpoint_rejects_secrets_bad_progress_and_snapshot_drift(tmp_path: Path) -> None:
|
||||
workspace = tmp_path / "research-workspace"
|
||||
create_checkpoint(
|
||||
workspace,
|
||||
run_id="run-drift-01",
|
||||
objective="Test resume pinning",
|
||||
projects=["dome-performance"],
|
||||
include_shared=False,
|
||||
lens="A bounded lens",
|
||||
)
|
||||
with pytest.raises(ValueError, match="credentials or source bodies"):
|
||||
update_checkpoint(workspace, "run-drift-01", {"last_failure": {"api_key": "secret"}})
|
||||
with pytest.raises(ValueError, match="SHA-256"):
|
||||
update_checkpoint(
|
||||
workspace,
|
||||
"run-drift-01",
|
||||
{
|
||||
"outputs": [{"sha256": "not-a-digest"}],
|
||||
"stage": "report",
|
||||
},
|
||||
)
|
||||
|
||||
path = workspace / "Runs" / "run-drift-01.json"
|
||||
checkpoint = json.loads(path.read_text(encoding="utf-8"))
|
||||
checkpoint["pack"]["snapshot_digest"] = "0" * 64
|
||||
path.write_text(json.dumps(checkpoint), encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="pack changed"):
|
||||
load_checkpoint(workspace, "run-drift-01")
|
||||
|
||||
|
||||
def test_existing_run_cannot_resume_after_verified_pack_upgrade(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
workspace = tmp_path / "research-workspace"
|
||||
create_checkpoint(
|
||||
workspace,
|
||||
run_id="run-pack-update-01",
|
||||
objective="Resume with pinned modules",
|
||||
projects=["dome-performance"],
|
||||
include_shared=True,
|
||||
lens="Keep the original extraction and evidence rules",
|
||||
)
|
||||
checkpoint_path = workspace / "Runs" / "run-pack-update-01.json"
|
||||
original_checkpoint = checkpoint_path.read_bytes()
|
||||
|
||||
updated_manifest = copy.deepcopy(checkpoint_module.load_manifest())
|
||||
updated_manifest["pack_version"] = "0.7.0"
|
||||
updated_manifest["modules"][0]["implementation_version"] += ".updated"
|
||||
modules_json = json.dumps(
|
||||
updated_manifest["modules"], ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
||||
)
|
||||
updated_manifest["snapshot"]["digest"] = hashlib.sha256(modules_json.encode()).hexdigest()
|
||||
|
||||
# This fixture stands in for a newer pack whose own manifest verification
|
||||
# succeeded; the behavior under test is the active run's compatibility pin.
|
||||
monkeypatch.setattr(checkpoint_module, "load_manifest", lambda: updated_manifest)
|
||||
monkeypatch.setattr(checkpoint_module, "verify", lambda _manifest: None)
|
||||
|
||||
with pytest.raises(ValueError, match="pack changed since this run"):
|
||||
load_checkpoint(workspace, "run-pack-update-01")
|
||||
assert checkpoint_path.read_bytes() == original_checkpoint
|
||||
@@ -0,0 +1,698 @@
|
||||
from gewu.adapters.text_extractor import TextMarkdownExtractor
|
||||
from gewu.domain.contracts import (
|
||||
EdgeEndpoint,
|
||||
KnowledgeNode,
|
||||
Locator,
|
||||
NodeType,
|
||||
ProjectStatus,
|
||||
ProposalOperation,
|
||||
ProposalRevision,
|
||||
Scope,
|
||||
SupportStatus,
|
||||
TypedEdge,
|
||||
)
|
||||
from gewu.domain.validation import (
|
||||
ValidationReport,
|
||||
assertion_digest,
|
||||
edge_assertion_digest,
|
||||
evidence_digest,
|
||||
proposal_payload_digest,
|
||||
validate_proposal,
|
||||
)
|
||||
from tests.domain.proposal_fixtures import (
|
||||
BASE_COMMIT,
|
||||
CLAIM,
|
||||
EVIDENCE,
|
||||
EXTRACTION,
|
||||
NODE,
|
||||
POLICIES,
|
||||
PROJECTS,
|
||||
SOURCE_REVISION,
|
||||
proposal,
|
||||
)
|
||||
|
||||
|
||||
def _validate(candidate: ProposalRevision) -> ValidationReport:
|
||||
return validate_proposal(
|
||||
candidate,
|
||||
projects=PROJECTS,
|
||||
policies=POLICIES,
|
||||
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
|
||||
|
||||
def test_supported_exact_citation_and_both_bundle_policies_are_eligible() -> None:
|
||||
report = _validate(proposal())
|
||||
assert report.status == "eligible", report.findings
|
||||
assert report.eligible_for_auto_accept
|
||||
assert set(report.affected_policy_ids) == {"dome-policy", "shared"}
|
||||
|
||||
|
||||
def test_transcript_timestamp_locator_must_match_the_cited_cue() -> None:
|
||||
transcript_bytes = (
|
||||
b"WEBVTT\n\n"
|
||||
b"cue-1\n00:14:32.000 --> 00:14:36.500\n"
|
||||
b"The installation hosted 400 visitors during the 2026 season.\n"
|
||||
)
|
||||
source = SOURCE_REVISION.model_copy(
|
||||
update={
|
||||
"media_type": "text/vtt",
|
||||
"byte_length": len(transcript_bytes),
|
||||
"artifact_sha256": __import__("hashlib").sha256(transcript_bytes).hexdigest(),
|
||||
}
|
||||
)
|
||||
extraction = TextMarkdownExtractor().extract(source.revision_id, transcript_bytes, "text/vtt")
|
||||
cue = next(block for block in extraction.blocks if block.time_start_ms is not None)
|
||||
quote = CLAIM.text
|
||||
start = cue.normalized_text.index(quote)
|
||||
evidence = EVIDENCE.model_copy(
|
||||
update={
|
||||
"extraction_id": extraction.extraction_id,
|
||||
"quote": quote,
|
||||
"locator": Locator(
|
||||
kind="transcript_timestamp_span",
|
||||
block_id=cue.block_id,
|
||||
start=start,
|
||||
end=start + len(quote),
|
||||
time_start_ms=cue.time_start_ms,
|
||||
time_end_ms=cue.time_end_ms,
|
||||
),
|
||||
}
|
||||
)
|
||||
assessment = CLAIM.assessment.model_copy(
|
||||
update={"evidence_digests": (evidence_digest(evidence),)}
|
||||
)
|
||||
claim = CLAIM.model_copy(update={"evidence": (evidence,), "assessment": assessment})
|
||||
node = NODE.model_copy(update={"claims": (claim,)})
|
||||
eligible = validate_proposal(
|
||||
proposal(node=node, source=source),
|
||||
projects=PROJECTS,
|
||||
policies=POLICIES,
|
||||
sources={},
|
||||
extractions={extraction.extraction_id: extraction},
|
||||
existing_nodes={},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert eligible.status == "eligible", eligible.findings
|
||||
|
||||
wrong_time = evidence.model_copy(
|
||||
update={
|
||||
"locator": evidence.locator.model_copy(
|
||||
update={"time_start_ms": evidence.locator.time_start_ms + 1}
|
||||
)
|
||||
}
|
||||
)
|
||||
wrong_assessment = assessment.model_copy(
|
||||
update={"evidence_digests": (evidence_digest(wrong_time),)}
|
||||
)
|
||||
wrong_claim = claim.model_copy(
|
||||
update={"evidence": (wrong_time,), "assessment": wrong_assessment}
|
||||
)
|
||||
rejected = validate_proposal(
|
||||
proposal(node=NODE.model_copy(update={"claims": (wrong_claim,)}), source=None),
|
||||
projects=PROJECTS,
|
||||
policies=POLICIES,
|
||||
sources={source.revision_id: source},
|
||||
extractions={extraction.extraction_id: extraction},
|
||||
existing_nodes={},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert rejected.status == "invalid"
|
||||
assert "EVIDENCE_LOCATION_MISMATCH" in {finding.code for finding in rejected.findings}
|
||||
|
||||
|
||||
def test_paused_or_archived_projects_reject_new_acceptance() -> None:
|
||||
for status in (ProjectStatus.PAUSED, ProjectStatus.ARCHIVED):
|
||||
project = PROJECTS["dome-performance"].model_copy(update={"status": status})
|
||||
report = validate_proposal(
|
||||
proposal(),
|
||||
projects={**PROJECTS, "dome-performance": project},
|
||||
policies=POLICIES,
|
||||
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert not report.eligible_for_auto_accept
|
||||
assert any(item.code == "PROJECT_NOT_ACTIVE" for item in report.findings)
|
||||
|
||||
|
||||
def test_cross_project_source_bundle_requires_every_affected_policy() -> None:
|
||||
shared_source = SOURCE_REVISION.model_copy(
|
||||
update={"ingested_for": ("dome-performance", "interactive-video")}
|
||||
)
|
||||
candidate = proposal(
|
||||
source=shared_source,
|
||||
scope=Scope(projects=("dome-performance", "interactive-video"), include_shared=True),
|
||||
)
|
||||
eligible = validate_proposal(
|
||||
candidate,
|
||||
projects=PROJECTS,
|
||||
policies=POLICIES,
|
||||
sources={},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert eligible.status == "eligible", eligible.findings
|
||||
assert set(eligible.affected_policy_ids) == {
|
||||
"dome-policy",
|
||||
"video-policy",
|
||||
"shared",
|
||||
}
|
||||
|
||||
restrictive_video_policy = POLICIES["video-policy"].model_copy(
|
||||
update={"automatic_source_registration": False}
|
||||
)
|
||||
blocked = validate_proposal(
|
||||
candidate,
|
||||
projects=PROJECTS,
|
||||
policies={**POLICIES, "video-policy": restrictive_video_policy},
|
||||
sources={},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert blocked.status == "needs-review"
|
||||
assert not blocked.eligible_for_auto_accept
|
||||
assert "AUTO_ACCEPT_UNSPECIFIED" in {finding.code for finding in blocked.findings}
|
||||
|
||||
|
||||
def test_matching_quote_at_wrong_location_is_rejected() -> None:
|
||||
evidence = EVIDENCE.model_copy(
|
||||
update={
|
||||
"quote": "400 visitors",
|
||||
"locator": EVIDENCE.locator.model_copy(update={"start": 0, "end": 13}),
|
||||
}
|
||||
)
|
||||
changed_claim = CLAIM.model_copy(update={"evidence": (evidence,)})
|
||||
changed_node = NODE.model_copy(update={"claims": (changed_claim,)})
|
||||
report = _validate(proposal(node=changed_node, source=None))
|
||||
assert report.status == "invalid"
|
||||
assert "EVIDENCE_MISMATCH" in {finding.code for finding in report.findings}
|
||||
|
||||
|
||||
def test_reference_only_source_cannot_support_accepted_evidence() -> None:
|
||||
reference = SOURCE_REVISION.model_copy(
|
||||
update={
|
||||
"artifact_sha256": None,
|
||||
"media_type": "application/octet-stream",
|
||||
"byte_length": 0,
|
||||
"original_url": "https://research.example/missing",
|
||||
"archive_status": "reference-only",
|
||||
}
|
||||
)
|
||||
report = validate_proposal(
|
||||
proposal(source=None),
|
||||
projects=PROJECTS,
|
||||
policies=POLICIES,
|
||||
sources={reference.revision_id: reference},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert report.status == "invalid"
|
||||
assert "SOURCE_NOT_ARCHIVED" in {finding.code for finding in report.findings}
|
||||
|
||||
|
||||
def test_correct_quote_with_unsupported_assessment_is_rejected() -> None:
|
||||
assessment = CLAIM.assessment.model_copy(update={"status": SupportStatus.UNSUPPORTED})
|
||||
changed_claim = CLAIM.model_copy(update={"assessment": assessment})
|
||||
changed_node = NODE.model_copy(update={"claims": (changed_claim,)})
|
||||
report = _validate(proposal(node=changed_node, source=None))
|
||||
assert report.status == "invalid"
|
||||
assert "SUPPORT_UNAVAILABLE" in {finding.code for finding in report.findings}
|
||||
|
||||
|
||||
def test_node_and_assessor_attribution_cannot_impersonate_another_agent() -> None:
|
||||
other_assessment = CLAIM.assessment.model_copy(update={"assessor_id": "agent-other"})
|
||||
assessment_claim = CLAIM.model_copy(update={"assessment": other_assessment})
|
||||
assessment_node = NODE.model_copy(update={"claims": (assessment_claim,)})
|
||||
assessment_report = _validate(proposal(node=assessment_node, source=None))
|
||||
assert "ASSESSOR_ATTRIBUTION_MISMATCH" in {
|
||||
finding.code for finding in assessment_report.findings
|
||||
}
|
||||
|
||||
other_node = NODE.model_copy(update={"agent_id": "agent-other"})
|
||||
node_report = _validate(proposal(node=other_node, source=None))
|
||||
assert "AGENT_ATTRIBUTION_MISMATCH" in {finding.code for finding in node_report.findings}
|
||||
|
||||
|
||||
def test_entity_name_collision_requires_review_without_creating_a_link() -> None:
|
||||
entity = NODE.model_copy(
|
||||
update={"node_type": NodeType.ENTITY, "title": "The Dome", "aliases": ("Planetarium",)}
|
||||
)
|
||||
existing = NODE.model_copy(
|
||||
update={
|
||||
"node_type": NodeType.ENTITY,
|
||||
"node_id": "dome:existing-entity",
|
||||
"title": " planetarium ",
|
||||
}
|
||||
)
|
||||
report = validate_proposal(
|
||||
proposal(
|
||||
node=entity,
|
||||
source=None,
|
||||
scope=Scope(projects=("dome-performance",), include_shared=False),
|
||||
),
|
||||
projects=PROJECTS,
|
||||
policies=POLICIES,
|
||||
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={existing.node_id: existing},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert report.status == "needs-review"
|
||||
assert not report.eligible_for_auto_accept
|
||||
assert "POSSIBLE_DUPLICATE_ENTITY" in {finding.code for finding in report.findings}
|
||||
|
||||
|
||||
def test_entity_duplicate_detection_respects_project_scope() -> None:
|
||||
entity = NODE.model_copy(
|
||||
update={"node_type": NodeType.ENTITY, "title": "The Dome", "aliases": ("Planetarium",)}
|
||||
)
|
||||
unrelated = NODE.model_copy(
|
||||
update={
|
||||
"node_type": NodeType.ENTITY,
|
||||
"node_id": "video:existing-entity",
|
||||
"project_id": "interactive-video",
|
||||
"title": "Planetarium",
|
||||
}
|
||||
)
|
||||
report = validate_proposal(
|
||||
proposal(node=entity, source=None),
|
||||
projects=PROJECTS,
|
||||
policies=POLICIES,
|
||||
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={unrelated.node_id: unrelated},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert "POSSIBLE_DUPLICATE_ENTITY" not in {finding.code for finding in report.findings}
|
||||
|
||||
|
||||
def test_entity_duplicate_review_uses_only_readable_projects_in_explicit_scope() -> None:
|
||||
entity = NODE.model_copy(
|
||||
update={"node_type": NodeType.ENTITY, "title": "The Dome", "aliases": ("Planetarium",)}
|
||||
)
|
||||
other_entity = NODE.model_copy(
|
||||
update={
|
||||
"node_type": NodeType.ENTITY,
|
||||
"node_id": "video:existing-entity",
|
||||
"project_id": "interactive-video",
|
||||
"title": "Planetarium",
|
||||
}
|
||||
)
|
||||
existing = {other_entity.node_id: other_entity}
|
||||
candidate = proposal(
|
||||
node=entity,
|
||||
source=None,
|
||||
scope=Scope(projects=("dome-performance", "interactive-video")),
|
||||
)
|
||||
readable = validate_proposal(
|
||||
candidate,
|
||||
projects=PROJECTS,
|
||||
policies=POLICIES,
|
||||
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes=existing,
|
||||
current_base_commit=BASE_COMMIT,
|
||||
readable_project_ids=frozenset({"interactive-video"}),
|
||||
)
|
||||
unreadable = validate_proposal(
|
||||
candidate,
|
||||
projects=PROJECTS,
|
||||
policies=POLICIES,
|
||||
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes=existing,
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert "POSSIBLE_DUPLICATE_ENTITY" in {finding.code for finding in readable.findings}
|
||||
assert "POSSIBLE_DUPLICATE_ENTITY" not in {finding.code for finding in unreadable.findings}
|
||||
|
||||
|
||||
def test_entity_duplicate_review_does_not_expand_beyond_proposal_scope() -> None:
|
||||
entity = NODE.model_copy(
|
||||
update={"node_type": NodeType.ENTITY, "title": "The Dome", "aliases": ("Planetarium",)}
|
||||
)
|
||||
shared_entity = NODE.model_copy(
|
||||
update={
|
||||
"node_type": NodeType.ENTITY,
|
||||
"node_id": "shared:planetarium-entity",
|
||||
"project_id": "shared",
|
||||
"title": "Planetarium",
|
||||
}
|
||||
)
|
||||
report = validate_proposal(
|
||||
proposal(
|
||||
node=entity,
|
||||
source=None,
|
||||
scope=Scope(projects=("dome-performance",), include_shared=False),
|
||||
),
|
||||
projects=PROJECTS,
|
||||
policies=POLICIES,
|
||||
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={shared_entity.node_id: shared_entity},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
readable_project_ids=frozenset({"shared"}),
|
||||
)
|
||||
assert "POSSIBLE_DUPLICATE_ENTITY" not in {finding.code for finding in report.findings}
|
||||
|
||||
|
||||
def test_requested_shared_scope_does_not_reveal_unreadable_shared_entity_names() -> None:
|
||||
entity = NODE.model_copy(
|
||||
update={"node_type": NodeType.ENTITY, "title": "The Dome", "aliases": ("Planetarium",)}
|
||||
)
|
||||
shared_entity = NODE.model_copy(
|
||||
update={
|
||||
"node_type": NodeType.ENTITY,
|
||||
"node_id": "shared:planetarium-entity",
|
||||
"project_id": "shared",
|
||||
"title": "Planetarium",
|
||||
}
|
||||
)
|
||||
report = validate_proposal(
|
||||
proposal(node=entity, source=None),
|
||||
projects=PROJECTS,
|
||||
policies=POLICIES,
|
||||
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={shared_entity.node_id: shared_entity},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert "POSSIBLE_DUPLICATE_ENTITY" not in {finding.code for finding in report.findings}
|
||||
|
||||
explicitly_readable = validate_proposal(
|
||||
proposal(node=entity, source=None, scope=Scope(projects=("dome-performance", "shared"))),
|
||||
projects=PROJECTS,
|
||||
policies=POLICIES,
|
||||
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={shared_entity.node_id: shared_entity},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
readable_project_ids=frozenset({"shared"}),
|
||||
)
|
||||
assert "POSSIBLE_DUPLICATE_ENTITY" in {finding.code for finding in explicitly_readable.findings}
|
||||
|
||||
|
||||
def test_changed_assertion_and_old_assessment_are_rejected() -> None:
|
||||
changed_text = "The installation hosted 401 visitors during the 2026 season."
|
||||
stale_assessment = CLAIM.assessment
|
||||
evidence = CLAIM.evidence[0].model_copy(
|
||||
update={"assertion_digest": assertion_digest(changed_text)}
|
||||
)
|
||||
changed_claim = CLAIM.model_copy(
|
||||
update={"text": changed_text, "evidence": (evidence,), "assessment": stale_assessment}
|
||||
)
|
||||
changed_node = NODE.model_copy(update={"claims": (changed_claim,)})
|
||||
report = _validate(proposal(node=changed_node, source=None))
|
||||
assert report.status == "invalid"
|
||||
assert "ASSESSMENT_STALE" in {finding.code for finding in report.findings}
|
||||
|
||||
|
||||
def test_assumption_and_limitation_claims_use_the_same_evidence_contract() -> None:
|
||||
assumption_text = CLAIM.text
|
||||
assumption_digest = assertion_digest(assumption_text)
|
||||
assumption_evidence = EVIDENCE.model_copy(
|
||||
update={
|
||||
"assertion_path": "assumptions[0].text",
|
||||
"assertion_digest": assumption_digest,
|
||||
}
|
||||
)
|
||||
assumption = CLAIM.model_copy(
|
||||
update={
|
||||
"claim_id": "dome:assumption-seasonality",
|
||||
"text": assumption_text,
|
||||
"evidence": (assumption_evidence,),
|
||||
"assessment": CLAIM.assessment.model_copy(
|
||||
update={
|
||||
"assertion_digest": assumption_digest,
|
||||
"evidence_digests": (evidence_digest(assumption_evidence),),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
limitation_evidence = EVIDENCE.model_copy(
|
||||
update={
|
||||
"assertion_path": "limitations[0].text",
|
||||
"assertion_digest": assumption_digest,
|
||||
}
|
||||
)
|
||||
limitation = assumption.model_copy(
|
||||
update={
|
||||
"claim_id": "dome:limitation-seasonality",
|
||||
"evidence": (limitation_evidence,),
|
||||
"assessment": assumption.assessment.model_copy(
|
||||
update={"evidence_digests": (evidence_digest(limitation_evidence),)}
|
||||
),
|
||||
}
|
||||
)
|
||||
node = NODE.model_copy(update={"assumptions": (assumption,), "limitations": (limitation,)})
|
||||
report = _validate(proposal(node=node, source=None))
|
||||
assert report.eligible_for_auto_accept, report.findings
|
||||
|
||||
wrong_location = assumption_evidence.model_copy(
|
||||
update={
|
||||
"quote": "400 visitors",
|
||||
"locator": assumption_evidence.locator.model_copy(update={"start": 0, "end": 13}),
|
||||
}
|
||||
)
|
||||
invalid_assumption = assumption.model_copy(
|
||||
update={
|
||||
"evidence": (wrong_location,),
|
||||
"assessment": assumption.assessment.model_copy(
|
||||
update={"evidence_digests": (evidence_digest(wrong_location),)}
|
||||
),
|
||||
}
|
||||
)
|
||||
invalid = _validate(
|
||||
proposal(node=NODE.model_copy(update={"assumptions": (invalid_assumption,)}), source=None)
|
||||
)
|
||||
assert "EVIDENCE_MISMATCH" in {finding.code for finding in invalid.findings}
|
||||
assert any(
|
||||
finding.path == "operations[0].node.assumptions[0].evidence[0]"
|
||||
for finding in invalid.findings
|
||||
)
|
||||
|
||||
|
||||
def test_missing_policy_and_explicit_scope_escape_do_not_accept() -> None:
|
||||
no_shared_policy = {key: value for key, value in POLICIES.items() if key != "shared"}
|
||||
candidate = proposal()
|
||||
report = validate_proposal(
|
||||
candidate,
|
||||
projects=PROJECTS,
|
||||
policies=no_shared_policy,
|
||||
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert report.status == "needs-review"
|
||||
assert "POLICY_MISSING" in {finding.code for finding in report.findings}
|
||||
|
||||
outside_scope = proposal(
|
||||
node=NODE.model_copy(update={"project_id": "interactive-video"}),
|
||||
source=None,
|
||||
scope=Scope(projects=("dome-performance",)),
|
||||
)
|
||||
report = _validate(outside_scope)
|
||||
assert report.status == "invalid"
|
||||
assert "OUT_OF_SCOPE" in {finding.code for finding in report.findings}
|
||||
|
||||
|
||||
def test_policy_cannot_waive_required_support_review() -> None:
|
||||
weakened_policy = POLICIES["dome-policy"].model_copy(
|
||||
update={"require_supported_assessment": False}
|
||||
)
|
||||
report = validate_proposal(
|
||||
proposal(),
|
||||
projects=PROJECTS,
|
||||
policies={**POLICIES, "dome-policy": weakened_policy},
|
||||
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert report.status == "invalid"
|
||||
assert not report.eligible_for_auto_accept
|
||||
assert "POLICY_WEAKENING" in {finding.code for finding in report.findings}
|
||||
|
||||
|
||||
def test_changed_base_is_a_conflict_and_edge_policy_is_explicit() -> None:
|
||||
report = _validate(proposal(base_commit="f" * 64))
|
||||
assert report.status == "conflict"
|
||||
assert "BASE_CONFLICT" in {finding.code for finding in report.findings}
|
||||
|
||||
|
||||
def test_relationship_evidence_is_bound_to_the_edge_and_exact_source_span() -> None:
|
||||
policies = {
|
||||
**POLICIES,
|
||||
"dome-policy": POLICIES["dome-policy"].model_copy(
|
||||
update={"automatic_edge_types": ("causes",)}
|
||||
),
|
||||
}
|
||||
edge = TypedEdge(
|
||||
edge_id="edge-pilot",
|
||||
edge_type="causes",
|
||||
source=EdgeEndpoint(kind="node", id=NODE.node_id),
|
||||
target=EdgeEndpoint(kind="node", id="dome:other-node"),
|
||||
evidence=(EVIDENCE,),
|
||||
)
|
||||
bound_evidence = EVIDENCE.model_copy(update={"assertion_digest": edge_assertion_digest(edge)})
|
||||
edge = edge.model_copy(update={"evidence": (bound_evidence,)})
|
||||
base = proposal(source=None)
|
||||
candidate = base.model_copy(
|
||||
update={
|
||||
"operations": (*base.operations, ProposalOperation(operation="add_edge", edge=edge)),
|
||||
"payload_digest": "0" * 64,
|
||||
}
|
||||
)
|
||||
candidate = candidate.model_copy(update={"payload_digest": proposal_payload_digest(candidate)})
|
||||
report = validate_proposal(
|
||||
candidate,
|
||||
projects=PROJECTS,
|
||||
policies=policies,
|
||||
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={
|
||||
"dome:other-node": NODE.model_copy(update={"node_id": "dome:other-node"}),
|
||||
},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert report.status == "eligible", report.findings
|
||||
|
||||
tampered = bound_evidence.model_copy(
|
||||
update={"assertion_digest": assertion_digest("unrelated claim")}
|
||||
)
|
||||
invalid_edge = edge.model_copy(update={"evidence": (tampered,)})
|
||||
invalid = candidate.model_copy(
|
||||
update={
|
||||
"operations": (
|
||||
*base.operations,
|
||||
ProposalOperation(operation="add_edge", edge=invalid_edge),
|
||||
),
|
||||
"payload_digest": "0" * 64,
|
||||
}
|
||||
)
|
||||
invalid = invalid.model_copy(update={"payload_digest": proposal_payload_digest(invalid)})
|
||||
invalid_report = validate_proposal(
|
||||
invalid,
|
||||
projects=PROJECTS,
|
||||
policies=policies,
|
||||
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
|
||||
extractions={EXTRACTION.extraction_id: EXTRACTION},
|
||||
existing_nodes={},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
assert invalid_report.status == "invalid"
|
||||
assert "EVIDENCE_ASSERTION_MISMATCH" in {finding.code for finding in invalid_report.findings}
|
||||
|
||||
|
||||
def test_contradiction_bundle_preserves_both_supported_claims() -> None:
|
||||
import hashlib
|
||||
|
||||
from gewu.domain.contracts import EdgeEndpoint
|
||||
|
||||
first_quote = "The seasonal audience count was 400."
|
||||
second_quote = "The seasonal audience count was 450."
|
||||
content = f"Field report: {first_quote}\n\nVendor report: {second_quote}\n".encode()
|
||||
source = SOURCE_REVISION.model_copy(
|
||||
update={
|
||||
"artifact_sha256": hashlib.sha256(content).hexdigest(),
|
||||
"byte_length": len(content),
|
||||
}
|
||||
)
|
||||
extraction = TextMarkdownExtractor().extract(source.revision_id, content, "text/markdown")
|
||||
|
||||
def node_with_claim(node_id: str, claim_id: str, quote: str, title: str) -> KnowledgeNode:
|
||||
block = next(item for item in extraction.blocks if quote in item.normalized_text)
|
||||
start = block.normalized_text.index(quote)
|
||||
evidence = EVIDENCE.model_copy(
|
||||
update={
|
||||
"source_revision_id": source.revision_id,
|
||||
"extraction_id": extraction.extraction_id,
|
||||
"quote": quote,
|
||||
"locator": Locator(
|
||||
kind="text_span", block_id=block.block_id, start=start, end=start + len(quote)
|
||||
),
|
||||
"assertion_digest": assertion_digest(quote),
|
||||
}
|
||||
)
|
||||
assessment = CLAIM.assessment.model_copy(
|
||||
update={
|
||||
"assertion_digest": assertion_digest(quote),
|
||||
"evidence_digests": (evidence_digest(evidence),),
|
||||
}
|
||||
)
|
||||
claim = CLAIM.model_copy(
|
||||
update={
|
||||
"claim_id": claim_id,
|
||||
"text": quote,
|
||||
"evidence": (evidence,),
|
||||
"assessment": assessment,
|
||||
}
|
||||
)
|
||||
return NODE.model_copy(
|
||||
update={
|
||||
"node_id": node_id,
|
||||
"title": title,
|
||||
"summary": quote,
|
||||
"claims": (claim,),
|
||||
"value": 400 if "400" in quote else 450,
|
||||
}
|
||||
)
|
||||
|
||||
first = node_with_claim(
|
||||
"dome:count-field-report", "dome:claim-count-400", first_quote, "Field count"
|
||||
)
|
||||
second = node_with_claim(
|
||||
"dome:count-vendor-report", "dome:claim-count-450", second_quote, "Vendor count"
|
||||
)
|
||||
contradiction = TypedEdge(
|
||||
edge_id="dome:counts-conflict",
|
||||
edge_type="contradicts",
|
||||
source=EdgeEndpoint(kind="node", id=first.node_id),
|
||||
target=EdgeEndpoint(kind="node", id=second.node_id),
|
||||
conditions="Both reports refer to the same seasonal audience count.",
|
||||
interpretation_rationale="The source-backed counts differ for the same reporting period.",
|
||||
)
|
||||
base = proposal(node=None, source=source)
|
||||
candidate = base.model_copy(
|
||||
update={
|
||||
"operations": (
|
||||
*base.operations,
|
||||
ProposalOperation(operation="upsert_node", node=first),
|
||||
ProposalOperation(operation="upsert_node", node=second),
|
||||
ProposalOperation(operation="add_edge", edge=contradiction),
|
||||
),
|
||||
"payload_digest": "0" * 64,
|
||||
}
|
||||
)
|
||||
candidate = candidate.model_copy(update={"payload_digest": proposal_payload_digest(candidate)})
|
||||
report = validate_proposal(
|
||||
candidate,
|
||||
projects=PROJECTS,
|
||||
policies={
|
||||
**POLICIES,
|
||||
"dome-policy": POLICIES["dome-policy"].model_copy(
|
||||
update={"automatic_edge_types": ("contradicts",)}
|
||||
),
|
||||
},
|
||||
sources={},
|
||||
extractions={extraction.extraction_id: extraction},
|
||||
existing_nodes={},
|
||||
current_base_commit=BASE_COMMIT,
|
||||
)
|
||||
|
||||
assert report.status == "eligible", report.findings
|
||||
assert tuple(
|
||||
operation.node.node_id for operation in candidate.operations if operation.node is not None
|
||||
) == (first.node_id, second.node_id)
|
||||
assert first.claims[0].text == first_quote
|
||||
assert second.claims[0].text == second_quote
|
||||
assert any(operation.edge == contradiction for operation in candidate.operations)
|
||||
Reference in new issue
Block a user