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"),
|
||||
[
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,6 +25,7 @@ from game_data_capture.runtime import (
|
||||
_CaptureControl,
|
||||
)
|
||||
from game_data_capture.storage import CaptureCorpus, CaptureRepository, StorageDatabase
|
||||
from game_data_capture.web.app import WebConfig
|
||||
|
||||
_PNG = PNG_SIGNATURE + b"unit-png"
|
||||
|
||||
@@ -451,6 +452,37 @@ def test_serve_constructs_durable_corpus_without_auto_arming_and_uses_one_fragme
|
||||
reusable.release()
|
||||
|
||||
|
||||
def test_serve_loads_bounded_corpus_history_independently_of_intake_capacity(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
runtime = CaptureRuntime(
|
||||
AppConfig(
|
||||
limits=CaptureLimits(max_queue_items=64),
|
||||
lifecycle=LifecycleConfig(lock_timeout_seconds=0),
|
||||
),
|
||||
_paths(tmp_path),
|
||||
migrations_dir=(Path(__file__).parents[3] / "migrations").resolve(),
|
||||
)
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def factory(**kwargs: object) -> object:
|
||||
captured.update(kwargs)
|
||||
return object()
|
||||
|
||||
def runner(_: object, **__: object) -> None:
|
||||
control = cast(_CaptureControl, captured["capture_control"])
|
||||
assert control.status().queue_capacity == runtime.config.limits.max_queue_items
|
||||
|
||||
monkeypatch.setattr(runtime_module, "create_app", factory)
|
||||
monkeypatch.setattr(uvicorn, "run", runner)
|
||||
|
||||
runtime.serve()
|
||||
|
||||
web_config = cast(WebConfig, captured["config"])
|
||||
assert web_config.max_capture_items == 256
|
||||
assert web_config.max_capture_items > runtime.config.limits.max_queue_items
|
||||
|
||||
|
||||
def test_service_startup_reconciliation_failure_releases_the_instance_lock(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user