feat: classify gear and weapon mods
This commit is contained in:
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from game_data_capture.domain.validation import validate_classification
|
||||
from game_data_capture.storage import CaptureCorpus, CaptureRepository, ContentAddressedBlobStore
|
||||
from game_data_capture.storage.blobs import BlobCorruptionError, BlobLockError
|
||||
from game_data_capture.storage.database import StorageDatabase
|
||||
@@ -24,6 +25,17 @@ def _label(*, view: str = "detail", records: list[str] | None = None) -> dict[st
|
||||
}
|
||||
|
||||
|
||||
def _mod_label(family: str) -> dict[str, object]:
|
||||
return {
|
||||
"family": family,
|
||||
"view": "detail",
|
||||
"gear_slot": None,
|
||||
"record_types": [family],
|
||||
"confidence": 1.0,
|
||||
"abstention_reason": None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def corpus(tmp_path: Path) -> tuple[StorageDatabase, CaptureRepository, CaptureCorpus]:
|
||||
root = (tmp_path / "utility").resolve()
|
||||
@@ -420,6 +432,91 @@ def test_label_transaction_rolls_back_on_evidence_failure(
|
||||
assert stored is not None and '"view":"detail"' in stored["classification_json"]
|
||||
|
||||
|
||||
def test_mod_labels_round_trip_through_restart_projections_and_shared_validation(
|
||||
corpus: tuple[StorageDatabase, CaptureRepository, CaptureCorpus],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
database, repository, port = corpus
|
||||
weapon_mod = _mod_label("weapon_mod")
|
||||
gear_mod = _mod_label("gear_mod")
|
||||
_receive(repository, "capture-0001", "receipt-0001")
|
||||
_receive(repository, "capture-0002", "receipt-0002")
|
||||
assert port.label("capture-0001", weapon_mod) == "updated"
|
||||
assert port.label("capture-0002", gear_mod) == "updated"
|
||||
|
||||
shared_validator = validate_classification
|
||||
validated: list[dict[str, object]] = []
|
||||
|
||||
def record_shared_validation(value: object) -> None:
|
||||
assert isinstance(value, dict)
|
||||
validated.append(value.copy())
|
||||
shared_validator(value)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"game_data_capture.storage.corpus.validate_classification", record_shared_validation
|
||||
)
|
||||
restarted = CaptureCorpus(
|
||||
database, ContentAddressedBlobStore(database.root), max_media_bytes=1024
|
||||
)
|
||||
listed = {
|
||||
record.capture_id: record.classification for record in restarted.list_captures(limit=2)
|
||||
}
|
||||
assert listed == {"capture-0001": weapon_mod, "capture-0002": gear_mod}
|
||||
weapon_detail = restarted.get_capture("capture-0001")
|
||||
gear_detail = restarted.get_capture("capture-0002")
|
||||
assert weapon_detail is not None and weapon_detail.classification == weapon_mod
|
||||
assert gear_detail is not None and gear_detail.classification == gear_mod
|
||||
assert validated == [gear_mod, weapon_mod, weapon_mod, gear_mod]
|
||||
|
||||
|
||||
def test_invalid_mod_label_leaves_current_label_and_evidence_unchanged(
|
||||
corpus: tuple[StorageDatabase, CaptureRepository, CaptureCorpus],
|
||||
) -> None:
|
||||
database, repository, port = corpus
|
||||
weapon_mod = _mod_label("weapon_mod")
|
||||
gear_mod = _mod_label("gear_mod")
|
||||
_receive(repository, "capture-0001", "receipt-0001")
|
||||
assert port.label("capture-0001", weapon_mod) == "updated"
|
||||
assert port.label("capture-0001", gear_mod) == "updated"
|
||||
with database.connect() as connection:
|
||||
current = connection.execute(
|
||||
"SELECT classification_json, revision FROM capture_labels WHERE capture_id = ?",
|
||||
("capture-0001",),
|
||||
).fetchone()
|
||||
assert current is not None
|
||||
before_current = tuple(current)
|
||||
before_evidence = [
|
||||
tuple(row)
|
||||
for row in connection.execute(
|
||||
"SELECT replaced_revision, classification_json FROM capture_label_evidence "
|
||||
"WHERE capture_id = ? ORDER BY replaced_revision",
|
||||
("capture-0001",),
|
||||
)
|
||||
]
|
||||
malformed = {**weapon_mod, "gear_slot": "Mask"}
|
||||
unsupported = {**weapon_mod, "view": "blueprint"}
|
||||
for invalid in (malformed, unsupported):
|
||||
with pytest.raises(ValueError, match="frozen taxonomy shape"):
|
||||
port.label("capture-0001", invalid)
|
||||
with database.connect() as connection:
|
||||
current = connection.execute(
|
||||
"SELECT classification_json, revision FROM capture_labels WHERE capture_id = ?",
|
||||
("capture-0001",),
|
||||
).fetchone()
|
||||
assert current is not None
|
||||
after_current = tuple(current)
|
||||
after_evidence = [
|
||||
tuple(row)
|
||||
for row in connection.execute(
|
||||
"SELECT replaced_revision, classification_json FROM capture_label_evidence "
|
||||
"WHERE capture_id = ? ORDER BY replaced_revision",
|
||||
("capture-0001",),
|
||||
)
|
||||
]
|
||||
assert after_current == before_current
|
||||
assert after_evidence == before_evidence
|
||||
|
||||
|
||||
def test_deletion_intent_recovers_after_unlink_failure(
|
||||
corpus: tuple[StorageDatabase, CaptureRepository, CaptureCorpus],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
Reference in New Issue
Block a user