From d4ff29d797bfaf0d4ea37400fbd5ba8417f1cb81 Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Mon, 29 Jun 2026 09:53:21 -0500 Subject: [PATCH] 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. --- samsung_appliance/bridge.py | 8 +++++-- tests/conftest.py | 14 +++++++++++-- tests/fixtures/golden/dishwasher.json | 2 +- tests/fixtures/golden/refrigerator.json | 2 +- tests/test_fixtures.py | 16 ++++++++------ tests/test_golden_regression.py | 28 ++++++++++++++++++++++--- 6 files changed, 55 insertions(+), 15 deletions(-) diff --git a/samsung_appliance/bridge.py b/samsung_appliance/bridge.py index 7968a29..c5f2973 100644 --- a/samsung_appliance/bridge.py +++ b/samsung_appliance/bridge.py @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index 9b30725..50a47b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 "") - return {k: v for k, v in data['resources'].items() if isinstance(v, dict)} + return _resources_from_dump(data) @pytest.fixture diff --git a/tests/fixtures/golden/dishwasher.json b/tests/fixtures/golden/dishwasher.json index 66d1e99..9c3c4d8 100644 --- a/tests/fixtures/golden/dishwasher.json +++ b/tests/fixtures/golden/dishwasher.json @@ -54,4 +54,4 @@ "samsung_dishwasher_stop", "samsung_dishwasher_water_liters" ] -} \ No newline at end of file +} diff --git a/tests/fixtures/golden/refrigerator.json b/tests/fixtures/golden/refrigerator.json index 459d98b..d7ac79e 100644 --- a/tests/fixtures/golden/refrigerator.json +++ b/tests/fixtures/golden/refrigerator.json @@ -59,4 +59,4 @@ "samsung_refrigerator_rapid_fridge", "samsung_refrigerator_sabbath_mode" ] -} \ No newline at end of file +} diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 4c1ec03..e035693 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -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'] diff --git a/tests/test_golden_regression.py b/tests/test_golden_regression.py index b9f1f7d..6a2d6c7 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -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}}