feat: classify gear and weapon mods
Validate and deploy production / validate (push) Successful in 24s
Validate and deploy staging / validate (push) Successful in 44s
Validate and deploy production / deploy (push) Failing after 29s
Validate and deploy staging / deploy (push) Failing after 6s

This commit is contained in:
jb
2026-07-31 21:54:47 -04:00
parent 1bd9facd3f
commit f48cc878aa
21 changed files with 595 additions and 39 deletions
@@ -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,
@@ -122,6 +122,44 @@ def test_private_capture_labeling_flow_preserves_rejection_before_retention_dele
assert corpus.deleted is True
def test_mod_labels_round_trip_through_the_reviewed_label_route() -> None:
token = new_bootstrap_token()
client = TestClient(
create_app(
config=WebConfig(ServerConfig()),
capture_control=CaptureControl(),
corpus=Corpus(),
bootstrap_token=token,
),
base_url="http://127.0.0.1:8765",
)
origin = "http://127.0.0.1:8765"
csrf = str(
client.post("/api/bootstrap", headers={"Origin": origin}, json={"token": token}).json()[
"csrf_token"
]
)
for family, record_type in (("weapon_mod", "weapon_mod"), ("gear_mod", "gear_mod")):
label = {
"family": family,
"view": "detail",
"gear_slot": None,
"record_types": [record_type],
"confidence": 1.0,
"abstention_reason": None,
}
response = client.post(
f"/api/captures/{_ID}/label",
headers={"Origin": origin, "X-CSRF-Token": csrf},
json=label,
)
assert response.status_code == 200
csrf = str(response.json()["csrf_token"])
assert client.get(f"/api/captures/{_ID}").json()["capture"]["classification"] == label
def test_review_ui_uses_local_external_assets_and_keeps_guardrails_in_static_source() -> None:
web_root = Path(__file__).parents[2] / "src" / "game_data_capture" / "web"
template = (web_root / "templates" / "index.html").read_text(encoding="utf-8")
@@ -146,12 +184,15 @@ def test_review_ui_uses_local_external_assets_and_keeps_guardrails_in_static_sou
assert 'credentials: "same-origin"' in script
assert "localStorage" not in script
assert "sessionStorage" not in script
assert "document.cookie" not in script
for expected in (
"weapon",
"gear",
"skill_chip",
"os_protocol",
"specialization",
"weapon_mod",
"gear_mod",
"unknown",
"Mask",
"Gloves",
@@ -193,3 +234,4 @@ def test_review_page_and_local_static_assets_serve_with_security_headers() -> No
assert "csrfPattern" in script.text
assert "setCheckedMembers" in script.text
assert "announce: false" in script.text
assert 'id="hide-reviewed"' in page.text
@@ -85,6 +85,92 @@ def test_review_script_guards_session_taxonomy_pending_and_selection_state() ->
assert "fallbackFocus?.focus();" in script
assert "function clearActiveDetail()" in script
assert 'if (action === "delete") clearActiveDetail();' in script
assert "captures.some((capture) => capture.capture_id === captureId)" in script
assert (
"const savedCapture = captures.find((capture) => capture.capture_id === captureId);"
in script
)
assert "if (savedCapture && !savedCaptureIsHidden) await selectCapture" in script
assert "else { clearActiveDetail(); renderCaptures(); }" in script
assert "refreshCaptures({ fallbackFocus: els.refresh })" in script
assert "focusIfAvailable(els.save);" in script
def test_queue_filter_keeps_reviewed_captures_local_and_focus_safe() -> None:
web_root = Path(__file__).parents[2] / "src" / "game_data_capture" / "web"
template = (web_root / "templates" / "index.html").read_text(encoding="utf-8")
script = (web_root / "static" / "review.js").read_text(encoding="utf-8")
stylesheet = (web_root / "static" / "review.css").read_text(encoding="utf-8")
assert '<input id="hide-reviewed" type="checkbox" />' in template
assert "Hide reviewed captures" in template
assert (
"checked"
not in template[
template.index('id="hide-reviewed"') - 20 : template.index('id="hide-reviewed"') + 50
]
)
assert "let hideReviewed = false;" in script
assert "els.filter.checked = false; hideReviewed = false;" in script
assert "function isReviewedCapture(capture)" in script
assert "capture?.classification !== null" in script
assert "function visibleCaptures()" in script
assert "captures.filter((capture) => !isReviewedCapture(capture))" in script
assert "`${visible.length} shown / ${captures.length} loaded`" in script
assert "ALL LOADED CAPTURES HIDDEN" in script
assert 'button.setAttribute("aria-current", "true")' in script
assert "els.filter.disabled = pending || locked;" in script
assert 'els.filter.addEventListener("change", applyReviewedFilter);' in script
filter_start = script.index("function applyReviewedFilter()")
filter_end = script.index("async function refreshCaptures", filter_start)
filter_source = script[filter_start:filter_end]
assert "const activeProjection = activeCaptureProjection();" in filter_source
assert (
"const activeIsHidden = hideReviewed && isReviewedCapture(activeProjection);"
in filter_source
)
assert "isReviewedCapture(activeCapture)" not in filter_source
assert "function activeCaptureProjection()" in script
assert (
"captures.find((capture) => capture.capture_id === activeCapture.capture_id)"
" || activeCapture" in script
)
assert "if (activeIsHidden) clearActiveDetail();" in script
assert "function focusVisibleCaptureOrFilter()" in script
assert 'els.list.querySelector("button:not(:disabled)") || els.filter' in script
assert "const savedCaptureIsHidden" in script
assert "if (savedCapture && !savedCaptureIsHidden) await selectCapture" in script
assert "if (savedCaptureIsHidden) focusVisibleCaptureOrFilter();" in script
assert ".capture-scroll { min-height: 0; overflow-x: hidden; overflow-y: auto;" in stylesheet
assert ".capture-panel { height: auto; min-height: 0;" in stylesheet
assert ".capture-scroll { overflow: visible;" in stylesheet
assert "body { margin: 0; min-width:" not in stylesheet
assert "function abbreviateCaptureId(value)" in script
assert "value.slice(0, 11)" in script
assert "function formatCaptureTime(value)" in script
assert '"TIME UNAVAILABLE"' in script
assert 'typeof value !== "string" || value.length > 64' in script
assert 'const reviewState = isReviewedCapture(capture) ? "REVIEWED" : "UNREVIEWED";' in script
assert 'button.setAttribute("aria-label", `Capture ${capture.capture_id}.' in script
assert "id.textContent = abbreviateCaptureId(capture.capture_id);" in script
assert "selectCapture(capture.capture_id)" in script
assert "els.reviewTitle.textContent = activeCapture.capture_id;" in script
assert ".capture-list .capture-id { min-width: 0; overflow: hidden;" in stylesheet
assert "text-overflow: ellipsis" in stylesheet
assert ".capture-list .capture-review" in stylesheet
def test_mod_taxonomy_maps_exact_labels_without_gear_slots_and_restores_them() -> None:
script = (
Path(__file__).parents[2] / "src" / "game_data_capture" / "web" / "static" / "review.js"
).read_text(encoding="utf-8")
for family, record_type in (("weapon_mod", "weapon_mod"), ("gear_mod", "gear_mod")):
assert (
f'{family}: {{ views: ["detail"], records: {{ detail: ["{record_type}"] }} }}' in script
)
assert 'const isGear = family === "gear";' in script
assert "if (isGear) gearSlots.forEach" in script
assert 'gear_slot: family === "gear" ? selected("gear_slot") : null' in script
assert 'setChecked("family", classification.family);' in script
assert 'setChecked("view", classification.view);' in script
assert 'setCheckedMembers("record_type", classification.record_types || []);' in script