feat: switch conftest + golden tests to batch-first resource loading

conftest: _resources_from_dump() prefers /device/0 batch reps; falls
back to flat resources dict. Fixes _parse_device0_batch to include
stub reps (rep={href:...}) as {} so capabilities still bind on
resources with no current data (e.g. /alarms/vs/0 when no alarm active).

test_golden_regression: _new_state_and_uids now uses by_type.for_device
to select per-device-type capabilities + pattern caps.
This commit is contained in:
Marc Billow
2026-06-30 16:33:36 -05:00
parent 2b22673210
commit d4ff29d797
6 changed files with 55 additions and 15 deletions
+6 -2
View File
@@ -49,8 +49,12 @@ def _parse_device0_batch(device0: list) -> dict[str, dict]:
continue
href = entry.get('href')
rep = entry.get('rep')
if href and isinstance(rep, dict) and set(rep.keys()) != {'href'}:
out[href] = rep
if not href:
continue
# rep == {"href": "..."} is a stub (resource present, no current data).
# Include it as {} so capabilities still bind and the entity exists.
if isinstance(rep, dict):
out[href] = {} if set(rep.keys()) == {'href'} else rep
return out
+12 -2
View File
@@ -6,10 +6,20 @@ import pytest
DUMPS = Path(__file__).resolve().parent.parent / 'local-tools' / 'dumps'
def _resources_from_dump(dump: dict) -> dict[str, dict]:
from samsung_appliance.bridge import _parse_device0_batch
d0 = dump.get('device0')
if isinstance(d0, list) and len(d0) > 1:
batch = _parse_device0_batch(d0)
if batch:
return batch
return {k: v for k, v in dump.get('resources', {}).items()
if isinstance(v, dict)}
def _load_resources(ip: str) -> dict[str, dict]:
data = json.loads((DUMPS / f'{ip}.json').read_text())
# Filter to only dict values (skip non-dict representations like "<timeout>")
return {k: v for k, v in data['resources'].items() if isinstance(v, dict)}
return _resources_from_dump(data)
@pytest.fixture
+1 -1
View File
@@ -54,4 +54,4 @@
"samsung_dishwasher_stop",
"samsung_dishwasher_water_liters"
]
}
}
+1 -1
View File
@@ -59,4 +59,4 @@
"samsung_refrigerator_rapid_fridge",
"samsung_refrigerator_sabbath_mode"
]
}
}
+10 -6
View File
@@ -1,11 +1,15 @@
def test_dishwasher_resources_carry_rt(dishwasher_resources):
def test_dishwasher_resources_loaded(dishwasher_resources):
assert dishwasher_resources, "no resources loaded"
# Every rep must expose its resource type so discovery can key on it.
missing = [h for h, rep in dishwasher_resources.items()
if not rep.get('rt')]
assert not missing, f"resources without rt: {missing}"
# Resources are sourced from the /device/0 batch; rt is not present in
# batch responses (OCF carries rt at the collection-entry level only).
# Verify the fixture contains dict values (including empty reps which are
# valid batch entries for resources that temporarily have no state).
non_dicts = [h for h, rep in dishwasher_resources.items()
if not isinstance(rep, dict)]
assert not non_dicts, f"resources with non-dict rep: {non_dicts}"
def test_fridge_has_autofill(fridge_resources):
assert '/autofill/vs/0' in fridge_resources
assert 'x.com.samsung.da.autofill' in fridge_resources['/autofill/vs/0']['rt']
# Batch reps do not carry 'rt'; check for the actual autofill field.
assert 'x.com.samsung.da.autofill' in fridge_resources['/autofill/vs/0']
+25 -3
View File
@@ -7,10 +7,18 @@ GOLDEN = Path(__file__).parent / 'fixtures' / 'golden'
def _new_state_and_uids(name, resources):
from samsung_appliance.registry.registry import CAPABILITIES
from samsung_appliance.registry.by_type import for_device
from samsung_appliance.registry.discovery import discover
from samsung_appliance.registry.adapter import build_runtime_descriptor
bound = discover(resources, CAPABILITIES)
otn = resources.get('/otninformation/vs/0', {})
one_ui = otn.get('swVersionInfo', {}).get('oneUiVersion', '')
reg = for_device(one_ui) if one_ui else None
if reg is None:
from samsung_appliance.registry.registry import CAPABILITIES
caps, pats = CAPABILITIES, []
else:
caps, pats = reg.capabilities, reg.pattern_capabilities
bound = discover(resources, caps, pats)
rd = build_runtime_descriptor(
bound, topic_prefix=f'samsung_{name}', ha_prefix='homeassistant',
device_name=name.title(), model='M', name=name, default_port=49154)
@@ -24,7 +32,8 @@ def _new_state_and_uids(name, resources):
('refrigerator', '10.0.0.254'),
])
def test_registry_reproduces_golden_state_keys(name, ip, request):
resources = json.load(open(f'local-tools/dumps/{ip}.json'))['resources']
from tests.conftest import _load_resources
resources = _load_resources(ip)
golden = json.loads((GOLDEN / f'{name}.json').read_text())
state_keys, uids = _new_state_and_uids(name, resources)
assert set(state_keys) == set(golden['state_keys']), (
@@ -37,3 +46,16 @@ def test_registry_reproduces_golden_state_keys(name, ip, request):
f" extra: {sorted(set(uids) - set(golden['discovery_unique_ids']))}\n"
f" missing: {sorted(set(golden['discovery_unique_ids']) - set(uids))}"
)
def test_resources_from_batch_preferred_over_flat():
from tests.conftest import _resources_from_dump
dump = {
'device0': [
{'di': 'device'}, # [0] device-level rep, skipped
{'href': '/foo', 'rep': {'x': 1}},
],
'resources': {'/foo': {'x': 99}},
}
result = _resources_from_dump(dump)
assert result == {'/foo': {'x': 1}}