Files
NODEDC_MISSION_CORE/tests/test_e30_human_review.py

345 lines
10 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import pytest
from fastapi import APIRouter
from k1link.compute.e30_human_review import (
E30HumanReviewConflictError,
E30HumanReviewIntegrityError,
E30HumanReviewStore,
E30HumanReviewValidationError,
E30ReviewSubject,
E30ReviewSubstrate,
)
from k1link.web import e30_human_review_api
from k1link.web.e30_human_review_api import build_e30_human_review_router
def _endpoint(router: APIRouter, path: str, method: str) -> Any:
for route in router.routes:
if (
getattr(route, "path", None) == path
and method in getattr(route, "methods", set())
):
return route.endpoint
raise AssertionError(f"{method} endpoint {path} not found")
def _substrate() -> E30ReviewSubstrate:
return E30ReviewSubstrate(
materialization_id=f"e30-materialization-{'a' * 64}",
materialization_identity_sha256="a" * 64,
review_pack_id=f"e30-review-pack-{'b' * 64}",
review_items_sha256="c" * 64,
reason_taxonomy=(),
subjects=(
E30ReviewSubject(
item_id=f"e30-review-item-{'d' * 64}",
sequence=0,
source_stratum="geometry-only",
),
E30ReviewSubject(
item_id=f"e30-review-item-{'e' * 64}",
sequence=1,
source_stratum="unknown",
),
),
engineering_generation_id=f"e30-engineering-generation-{'f' * 64}",
)
def _store(tmp_path: Path) -> E30HumanReviewStore:
return E30HumanReviewStore(
draft_root=tmp_path / "drafts",
generation_root=tmp_path / "generations",
)
def test_exception_review_is_generation_bound_and_resumable(
tmp_path: Path,
) -> None:
substrate = _substrate()
store = _store(tmp_path)
created = store.create_or_resume(substrate=substrate, reviewer_id="DC")
resumed = store.create_or_resume(substrate=substrate, reviewer_id="DC")
assert resumed["draft_id"] == created["draft_id"]
assert resumed["engineering_generation_id"] == (
substrate.engineering_generation_id
)
assert resumed["item_count"] == 2
assert resumed["reviewed_item_count"] == 0
def test_decisions_are_append_only_idempotent_and_superseding(
tmp_path: Path,
) -> None:
substrate = _substrate()
store = _store(tmp_path)
draft_id = str(
store.create_or_resume(substrate=substrate, reviewer_id="DC")["draft_id"]
)
subject = substrate.subjects[0]
first = store.record_decision(
draft_id=draft_id,
substrate=substrate,
item_id=subject.item_id,
expected_revision=0,
idempotency_key="decision-001",
disposition="object-present",
notes=None,
)
replay = store.record_decision(
draft_id=draft_id,
substrate=substrate,
item_id=subject.item_id,
expected_revision=0,
idempotency_key="decision-001",
disposition="object-present",
notes=None,
)
changed = store.record_decision(
draft_id=draft_id,
substrate=substrate,
item_id=subject.item_id,
expected_revision=1,
idempotency_key="decision-002",
disposition="background-or-noise",
notes="Static façade points.",
)
assert first["revision"] == replay["revision"] == 1
assert changed["revision"] == 2
assert changed["reviewed_item_count"] == 1
assert changed["disposition_distribution"] == {
"background-or-noise": 1
}
assert len(
(
tmp_path / "drafts" / draft_id / "events.jsonl"
).read_text().splitlines()
) == 2
def test_decision_rejects_unknown_disposition(tmp_path: Path) -> None:
substrate = _substrate()
store = _store(tmp_path)
draft_id = str(
store.create_or_resume(substrate=substrate, reviewer_id="DC")["draft_id"]
)
with pytest.raises(E30HumanReviewValidationError):
store.record_decision(
draft_id=draft_id,
substrate=substrate,
item_id=substrate.subjects[0].item_id,
expected_revision=0,
idempotency_key="decision-invalid",
disposition="invented", # type: ignore[arg-type]
notes=None,
)
def test_finalization_requires_every_exception_and_freezes_generation(
tmp_path: Path,
) -> None:
substrate = _substrate()
store = _store(tmp_path)
draft_id = str(
store.create_or_resume(substrate=substrate, reviewer_id="DC")["draft_id"]
)
first = store.record_decision(
draft_id=draft_id,
substrate=substrate,
item_id=substrate.subjects[0].item_id,
expected_revision=0,
idempotency_key="decision-001",
disposition="object-present",
notes=None,
)
with pytest.raises(E30HumanReviewConflictError, match="coverage is incomplete"):
store.finalize(
draft_id=draft_id,
substrate=substrate,
expected_revision=int(first["revision"]),
)
complete = store.record_decision(
draft_id=draft_id,
substrate=substrate,
item_id=substrate.subjects[1].item_id,
expected_revision=1,
idempotency_key="decision-002",
disposition="insufficient-evidence",
notes="Occluded.",
)
generation = store.finalize(
draft_id=draft_id,
substrate=substrate,
expected_revision=int(complete["revision"]),
)
assert generation["human_review_complete"] is True
assert generation["lab_published"] is False
assert generation["engineering_generation_id"] == (
substrate.engineering_generation_id
)
assert generation["disposition_distribution"] == {
"insufficient-evidence": 1,
"object-present": 1,
}
generation_id = str(generation["generation_id"])
decision_rows = [
json.loads(line)
for line in (
tmp_path
/ "generations"
/ generation_id
/ "review-decisions.jsonl"
).read_text().splitlines()
]
assert [row["disposition"] for row in decision_rows] == [
"object-present",
"insufficient-evidence",
]
repeated = store.finalize(
draft_id=draft_id,
substrate=substrate,
expected_revision=2,
)
assert repeated["generation_id"] == generation_id
with pytest.raises(E30HumanReviewConflictError, match="finalized"):
store.record_decision(
draft_id=draft_id,
substrate=substrate,
item_id=substrate.subjects[0].item_id,
expected_revision=2,
idempotency_key="decision-after-finalize",
disposition="object-present",
notes=None,
)
def test_event_and_generation_tampering_fail_closed(tmp_path: Path) -> None:
substrate = _substrate()
store = _store(tmp_path)
draft_id = str(
store.create_or_resume(substrate=substrate, reviewer_id="DC")["draft_id"]
)
store.record_decision(
draft_id=draft_id,
substrate=substrate,
item_id=substrate.subjects[0].item_id,
expected_revision=0,
idempotency_key="decision-001",
disposition="object-present",
notes=None,
)
events_path = tmp_path / "drafts" / draft_id / "events.jsonl"
event = json.loads(events_path.read_text())
event["disposition"] = "background-or-noise"
events_path.write_text(json.dumps(event) + "\n")
with pytest.raises(E30HumanReviewIntegrityError):
store.get(draft_id=draft_id, substrate=substrate)
def test_http_lifecycle_is_bound_to_the_engineering_exception_queue(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
source_substrate = _substrate()
generation_id = str(source_substrate.engineering_generation_id)
monkeypatch.setattr(
e30_human_review_api,
"load_verified_e30_review",
lambda **_: ({}, (), source_substrate),
)
monkeypatch.setattr(
e30_human_review_api,
"load_verified_e30_engineering_generation",
lambda **_: (
{
"human_exceptions": [
{"item_id": subject.item_id}
for subject in source_substrate.subjects
]
},
(),
),
)
router = build_e30_human_review_router(
materialization_root_provider=lambda: tmp_path / "materializations",
review_pack_root_provider=lambda: tmp_path / "review-packs",
engineering_generation_root_provider=lambda: (
tmp_path / "engineering-generations"
),
draft_root_provider=lambda: tmp_path / "drafts",
generation_root_provider=lambda: tmp_path / "generations",
)
create = _endpoint(
router,
"/api/v1/laboratory/e30/reviews/{result_id}/human-review",
"POST",
)
decide = _endpoint(
router,
(
"/api/v1/laboratory/e30/reviews/{result_id}/human-review/"
"{draft_id}/decisions/{item_id}"
),
"PUT",
)
finalize = _endpoint(
router,
(
"/api/v1/laboratory/e30/reviews/{result_id}/human-review/"
"{draft_id}/finalize"
),
"POST",
)
created = create(
result_id=source_substrate.materialization_id,
request=e30_human_review_api.E30HumanReviewCreateRequest(
reviewer_id="DC",
engineering_generation_id=generation_id,
),
)
draft = created
for revision, subject in enumerate(source_substrate.subjects):
draft = decide(
result_id=source_substrate.materialization_id,
draft_id=draft["draft_id"],
item_id=subject.item_id,
engineering_generation_id=generation_id,
request=e30_human_review_api.E30HumanReviewDecisionRequest(
expected_revision=revision,
idempotency_key=f"http-{revision}",
disposition=(
"object-present"
if revision == 0
else "insufficient-evidence"
),
notes=None,
),
)
finalized = finalize(
result_id=source_substrate.materialization_id,
draft_id=draft["draft_id"],
engineering_generation_id=generation_id,
request=e30_human_review_api.E30HumanReviewFinalizeRequest(
expected_revision=draft["revision"],
confirm_generation=True,
),
)
assert finalized["draft"]["state"] == "finalized"