"""AZ-965 — `C10ProvisioningConfig` coerces YAML-shaped backbones to dataclasses. YAML loaders (`config/loader.py::_replace_block` → `dataclasses.replace`) pass `backbones` as `list[dict]` because the generic loader path does not recursively construct nested dataclasses inside list/tuple fields. `C10ProvisioningConfig.__post_init__` must coerce each dict entry to a proper :class:`BackboneConfig` instance before downstream consumers iterate `backbone.model_name`. Similarly, `BackboneConfig.__post_init__` must coerce `expected_input_shape: list[int]` to `tuple[int, ...]` because PyYAML loads `[3, 480, 480]` as a list. """ from __future__ import annotations from gps_denied_onboard.components.c10_provisioning.config import ( BackboneConfig, C10ProvisioningConfig, ) def test_c10_provisioning_coerces_list_of_dicts_to_backbone_configs() -> None: # Arrange — what the YAML loader produces for c10_provisioning.backbones yaml_shaped_backbones = [ { "model_name": "net_vlad", "onnx_path": "/opt/models/net_vlad/net_vlad.pt", "expected_input_shape": [3, 480, 480], "input_name": "input", }, ] # Act config = C10ProvisioningConfig(backbones=yaml_shaped_backbones) # Assert assert len(config.backbones) == 1 assert isinstance(config.backbones, tuple) only = config.backbones[0] assert isinstance(only, BackboneConfig) assert only.model_name == "net_vlad" assert only.onnx_path == "/opt/models/net_vlad/net_vlad.pt" assert only.expected_input_shape == (3, 480, 480) assert isinstance(only.expected_input_shape, tuple) assert only.input_name == "input" def test_backbone_config_coerces_list_input_shape_to_tuple() -> None: # Act backbone = BackboneConfig( model_name="net_vlad", onnx_path="/opt/models/net_vlad/net_vlad.pt", expected_input_shape=[3, 480, 480], # type: ignore[arg-type] input_name="input", ) # Assert assert isinstance(backbone.expected_input_shape, tuple) assert backbone.expected_input_shape == (3, 480, 480) def test_c10_provisioning_passes_through_existing_backbone_configs() -> None: # Arrange existing = BackboneConfig( model_name="net_vlad", onnx_path="/opt/models/net_vlad/net_vlad.pt", expected_input_shape=(3, 480, 480), input_name="input", ) # Act — coercion path must be idempotent for already-typed inputs config = C10ProvisioningConfig(backbones=(existing,)) # Assert assert config.backbones == (existing,) assert config.backbones[0] is existing def test_c10_provisioning_coerces_mixed_dict_and_dataclass_entries() -> None: # Arrange — partial migration shape (defensive) existing = BackboneConfig( model_name="ultra_vpr", onnx_path="/opt/models/ultra_vpr/ultra_vpr.onnx", expected_input_shape=(3, 224, 224), input_name="input", ) yaml_shaped = { "model_name": "net_vlad", "onnx_path": "/opt/models/net_vlad/net_vlad.pt", "expected_input_shape": [3, 480, 480], "input_name": "input", } # Act config = C10ProvisioningConfig(backbones=[existing, yaml_shaped]) # Assert assert len(config.backbones) == 2 assert config.backbones[0] is existing assert isinstance(config.backbones[1], BackboneConfig) assert config.backbones[1].model_name == "net_vlad" def test_c10_provisioning_empty_backbones_remains_empty_tuple() -> None: # Act config = C10ProvisioningConfig() # Assert assert config.backbones == () assert isinstance(config.backbones, tuple)