feat: classify gear and weapon mods
This commit is contained in:
@@ -17,7 +17,12 @@ from game_data_capture.domain.state_machines import (
|
||||
InvalidStateTransition,
|
||||
require_transition,
|
||||
)
|
||||
from game_data_capture.domain.taxonomy import RECORD_TYPES_BY_VIEW, SCREEN_VIEWS, Family
|
||||
from game_data_capture.domain.taxonomy import (
|
||||
RECORD_TYPES_BY_VIEW,
|
||||
SCREEN_VIEWS,
|
||||
Family,
|
||||
RecordType,
|
||||
)
|
||||
from game_data_capture.domain.validation import (
|
||||
ContractValidationError,
|
||||
validate_archive_document,
|
||||
@@ -28,6 +33,10 @@ from game_data_capture.domain.validation import (
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CONTRACTS = ROOT / "contracts"
|
||||
CLASSIFICATION_EXAMPLES = CONTRACTS / "examples" / "classification"
|
||||
MOD_FAMILY_CONTRACT = {
|
||||
Family.WEAPON_MOD: RecordType.WEAPON_MOD,
|
||||
Family.GEAR_MOD: RecordType.GEAR_MOD,
|
||||
}
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
@@ -82,6 +91,31 @@ def test_domain_contract_and_python_taxonomy_cover_every_family_view() -> None:
|
||||
assert families[family.value]["views"][view] == expected
|
||||
|
||||
|
||||
def test_mod_families_have_exact_frozen_detail_semantics() -> None:
|
||||
contract = load_json(CONTRACTS / "domain-contract-v1.json")
|
||||
assert contract["contract_version"] == 1
|
||||
assert set(MOD_FAMILY_CONTRACT) == {Family.WEAPON_MOD, Family.GEAR_MOD}
|
||||
for family, record_type in MOD_FAMILY_CONTRACT.items():
|
||||
assert SCREEN_VIEWS[family] == ("detail",)
|
||||
assert RECORD_TYPES_BY_VIEW[(family, "detail")] == (record_type,)
|
||||
assert contract["families"][family.value] == {
|
||||
"gear_slot": False,
|
||||
"views": {"detail": [record_type.value]},
|
||||
}
|
||||
|
||||
|
||||
def test_approved_taxonomy_change_remains_contract_version_one() -> None:
|
||||
archive_schema = load_json(CONTRACTS / "evidence-archive-manifest-v1.schema.json")
|
||||
handoff_schema = load_json(CONTRACTS / "isac-handoff-v1.schema.json")
|
||||
classification_schema = load_json(CONTRACTS / "screenshot-classification.schema.json")
|
||||
assert archive_schema["properties"]["schema_version"] == {"const": 1}
|
||||
assert handoff_schema["properties"]["schema_version"] == {"const": 1}
|
||||
assert handoff_schema["properties"]["source_archive"]["properties"]["schema_version"] == {
|
||||
"const": 1
|
||||
}
|
||||
assert classification_schema["$id"].endswith("screenshot-classification-v1.json")
|
||||
|
||||
|
||||
def test_domain_contract_and_python_state_machines_match() -> None:
|
||||
contract_states = load_json(CONTRACTS / "domain-contract-v1.json")["states"]
|
||||
assert set(contract_states) == set(STATE_MACHINES)
|
||||
@@ -106,6 +140,79 @@ def test_each_family_has_a_valid_and_invalid_example(family: str) -> None:
|
||||
validate_classification(invalid)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("family", "record_type"), MOD_FAMILY_CONTRACT.items())
|
||||
def test_mod_family_examples_validate_through_schema_and_semantics(
|
||||
family: Family, record_type: RecordType
|
||||
) -> None:
|
||||
validator = schema_validator("screenshot-classification.schema.json")
|
||||
valid_path = CLASSIFICATION_EXAMPLES / f"{family.value}.valid.json"
|
||||
invalid_path = CLASSIFICATION_EXAMPLES / f"{family.value}.invalid.json"
|
||||
assert valid_path.is_file()
|
||||
assert invalid_path.is_file()
|
||||
valid = load_json(valid_path)
|
||||
invalid = load_json(invalid_path)
|
||||
assert valid == {
|
||||
"family": family.value,
|
||||
"view": "detail",
|
||||
"gear_slot": None,
|
||||
"record_types": [record_type.value],
|
||||
"confidence": valid["confidence"],
|
||||
"abstention_reason": None,
|
||||
}
|
||||
validator.validate(valid)
|
||||
validate_classification(valid)
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(invalid)
|
||||
with pytest.raises(ContractValidationError):
|
||||
validate_classification(invalid)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("family", "record_type"), MOD_FAMILY_CONTRACT.items())
|
||||
def test_archive_and_handoff_schemas_accept_mod_record_types(
|
||||
family: Family, record_type: RecordType
|
||||
) -> None:
|
||||
archive = load_json(CONTRACTS / "examples" / "archive" / "manifest.valid.json")
|
||||
archive["screens"][0]["classification"] = {
|
||||
"family": family.value,
|
||||
"view": "detail",
|
||||
"gear_slot": None,
|
||||
"record_types": [record_type.value],
|
||||
"confidence": 0.96,
|
||||
"abstention_reason": None,
|
||||
}
|
||||
archive["observed_instances"][0]["family"] = family.value
|
||||
archive["canonical_candidates"][0]["record_type"] = record_type.value
|
||||
schema_validator("evidence-archive-manifest-v1.schema.json").validate(archive)
|
||||
validate_archive_document(archive)
|
||||
|
||||
handoff = load_json(CONTRACTS / "examples" / "handoff" / "isac-handoff.valid.json")
|
||||
handoff["records"][0]["record_type"] = record_type.value
|
||||
schema_validator("isac-handoff-v1.schema.json").validate(handoff)
|
||||
validate_handoff_document(handoff)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("family", "record_type"), MOD_FAMILY_CONTRACT.items())
|
||||
@pytest.mark.parametrize(
|
||||
("field", "invalid_value"),
|
||||
[
|
||||
("view", "quality_profile"),
|
||||
("record_types", ["weapon"]),
|
||||
("record_types", ["weapon_mod_effect"]),
|
||||
("gear_slot", "Mask"),
|
||||
],
|
||||
)
|
||||
def test_mod_classifications_fail_closed_on_unsupported_values(
|
||||
family: Family, record_type: RecordType, field: str, invalid_value: object
|
||||
) -> None:
|
||||
classification = load_json(CLASSIFICATION_EXAMPLES / f"{family.value}.valid.json")
|
||||
assert classification["record_types"] == [record_type.value]
|
||||
classification[field] = invalid_value
|
||||
with pytest.raises(ValidationError):
|
||||
schema_validator("screenshot-classification.schema.json").validate(classification)
|
||||
with pytest.raises(ContractValidationError):
|
||||
validate_classification(classification)
|
||||
|
||||
|
||||
def test_composite_classification_preserves_multiple_record_types() -> None:
|
||||
classification = load_json(CLASSIFICATION_EXAMPLES / "weapon.valid.json")
|
||||
validate_classification(classification)
|
||||
@@ -121,6 +228,15 @@ def test_unknown_enum_value_is_rejected() -> None:
|
||||
validate_classification(classification)
|
||||
|
||||
|
||||
def test_semantic_validator_enforces_the_bounded_record_type_collection() -> None:
|
||||
classification = load_json(CLASSIFICATION_EXAMPLES / "weapon.valid.json")
|
||||
classification["record_types"] = [f"future_record_{index}" for index in range(9)]
|
||||
with pytest.raises(ValidationError):
|
||||
schema_validator("screenshot-classification.schema.json").validate(classification)
|
||||
with pytest.raises(ContractValidationError, match="more than 8"):
|
||||
validate_classification(classification)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("schema_name", "valid_fixture", "invalid_fixture"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user