Map remaining coverage-gap hrefs, fix discover() false-gap bug, improve device naming
Maps the fridge/dishwasher hrefs flagged by last session's coverage-gap Repairs issue: defrost delay (switch) + block status (diagnostic binary sensor), master ice-maker enable, self-check trigger/status, dishwasher diagnosis trigger/status, and last-operation-source. Adds fallback capabilities for /doors, /temperatures, and /icemaker/status that only bind when the richer per-instance hrefs they duplicate are absent, so simpler devices without those hrefs still get the data. Everything else in the original gap list moves to capabilities.ignored (Bixby audio feedback, inert DR/energy-planner resources, redundant metadata). Fixes discover() reporting a known href as an unregistered gap whenever its match_fn declined to bind for that device (e.g. a filter capability on hardware without that filter) — it's now only a gap when no capability is registered for the href at all. Device naming now uses x.com.samsung.da.modelNum (not the OCF /oic/p,d metadata, which produced inconsistent names like "[dishwasher] Samsung") to build "Samsung <Type> (<model>)".
This commit is contained in:
@@ -202,15 +202,17 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self.bound = bound
|
||||
self._unbound_hrefs = unbound
|
||||
|
||||
serial = (resources.get('/information/vs/0', {})
|
||||
.get('x.com.samsung.da.serialNum', ''))
|
||||
info = resources.get('/information/vs/0', {})
|
||||
serial = info.get('x.com.samsung.da.serialNum', '')
|
||||
if not serial:
|
||||
serial = self._entry.data[CONF_HOST]
|
||||
self.device_serial = serial
|
||||
|
||||
ident = self._identity
|
||||
model = (ident.model if ident else '') or (reg.name.title() if reg else 'Appliance')
|
||||
name = (ident.name if ident else '') or f"Samsung {model}"
|
||||
device_type = reg.name.title() if reg else 'Appliance'
|
||||
model_num = info.get('x.com.samsung.da.modelNum', '')
|
||||
model = model_num.split('|', 1)[0] if model_num else (ident.model if ident else '')
|
||||
name = f"Samsung {device_type} ({model})" if model else f"Samsung {device_type}"
|
||||
mfr = (ident.manufacturer if ident else '') or 'Samsung'
|
||||
|
||||
self.device_info = DeviceInfo(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Dishwasher device registry."""
|
||||
from ..capabilities import common, dishwasher, fridge, ignored, laundry, operational
|
||||
from ..capabilities import common, dishwasher, dryer, fridge, ignored, laundry, operational
|
||||
from ._base import DeviceRegistry, _build
|
||||
|
||||
REGISTRY = DeviceRegistry(
|
||||
@@ -16,6 +16,9 @@ REGISTRY = DeviceRegistry(
|
||||
operational.OPERATIONAL_STATE,
|
||||
dishwasher.CYCLE_OPTIONS,
|
||||
dishwasher.DISHWASHER_SETTINGS,
|
||||
dishwasher.DIAGNOSIS,
|
||||
dishwasher.OPERATION_ORIGIN,
|
||||
dryer.JOB_BEGINNING_STATUS,
|
||||
laundry.DOOR_LED,
|
||||
laundry.SOUND_MODE,
|
||||
laundry.SOUND_VOLUME,
|
||||
|
||||
@@ -24,6 +24,12 @@ REGISTRY = DeviceRegistry(
|
||||
fridge.SABBATH,
|
||||
fridge.BEVERAGE_ZONE,
|
||||
fridge.FIRMWARE_UPDATE,
|
||||
fridge.DEFROST_DELAY,
|
||||
fridge.DEFROST_BLOCK_STATUS,
|
||||
fridge.SELF_CHECK,
|
||||
fridge.DOORS_FALLBACK,
|
||||
fridge.TEMPERATURES_FALLBACK,
|
||||
fridge.ICEMAKER_STATUS_FALLBACK,
|
||||
]),
|
||||
pattern_capabilities=[
|
||||
fridge.TEMP_CURRENT_GENERIC,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Resources verified against the live device dump at 10.0.0.129.
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import SelectDesc, SwitchDesc
|
||||
from ..entities import ButtonDesc, SelectDesc, SensorDesc, SwitchDesc
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /dishwasher/vs/0 — cycle wash/dry settings
|
||||
@@ -119,3 +119,32 @@ CYCLE_OPTIONS = Capability(
|
||||
write_fn=_auto_release_write),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Self-diagnostic trigger and last-operation-source sensor
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DIAGNOSIS = Capability(
|
||||
href='/diagnosis/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SensorDesc(key='diagnosis_status', field='x.com.samsung.da.diagnosisStart',
|
||||
name='Diagnosis status', icon='mdi:stethoscope',
|
||||
entity_category='diagnostic'),
|
||||
ButtonDesc(key='diagnosis_start', field='', name='Start diagnosis',
|
||||
payload='Start', icon='mdi:play-circle-outline',
|
||||
entity_category='diagnostic',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['diagnosis', 'vs', '0'], {'x.com.samsung.da.diagnosisStart': p})),
|
||||
),
|
||||
)
|
||||
|
||||
OPERATION_ORIGIN = Capability(
|
||||
href='/operation/origin/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SensorDesc(key='operation_origin', field='origin',
|
||||
name='Last operation source', icon='mdi:remote',
|
||||
entity_category='diagnostic'),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -15,7 +15,8 @@ import datetime
|
||||
|
||||
from ..capability import Capability
|
||||
from ..entities import (
|
||||
BinarySensorDesc, NumberDesc, SelectDesc, SensorDesc, SwitchDesc, TimeDesc,
|
||||
BinarySensorDesc, ButtonDesc, NumberDesc, SelectDesc, SensorDesc,
|
||||
SwitchDesc, TimeDesc,
|
||||
)
|
||||
|
||||
# Beverage zone flex modes.
|
||||
@@ -164,6 +165,62 @@ STATUS_LOCK = Capability(
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Defrost delay
|
||||
#
|
||||
# /defrost/delay/vs/0 is the writable toggle. /defrost/block/vs/0 just
|
||||
# reports whether the defrost cycle is currently blocked as a result (a
|
||||
# derived status, not an independent control) — exposed as a read-only
|
||||
# diagnostic binary sensor.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DEFROST_DELAY = Capability(
|
||||
href='/defrost/delay/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SwitchDesc(key='defrost_delay', field='x.com.samsung.da.delayDefrost',
|
||||
name='Defrost delay', icon='mdi:snowflake-off',
|
||||
entity_category='config',
|
||||
value_fn=lambda v: v == 'On',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['defrost', 'delay', 'vs', '0'],
|
||||
{'x.com.samsung.da.delayDefrost': 'On' if p else 'Off'})),
|
||||
),
|
||||
)
|
||||
|
||||
DEFROST_BLOCK_STATUS = Capability(
|
||||
href='/defrost/block/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
BinarySensorDesc(key='defrost_blocked', field='x.com.samsung.da.modes',
|
||||
name='Defrost blocked', icon='mdi:snowflake-off',
|
||||
entity_category='diagnostic',
|
||||
value_fn=lambda modes: bool(modes) and modes[0] == 'DEFROST_BLOCK_ON'),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Self-check diagnostic
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SELF_CHECK = Capability(
|
||||
href='/selfcheck/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SensorDesc(key='selfcheck_status', field='x.com.samsung.da.status',
|
||||
name='Self-check status', icon='mdi:stethoscope',
|
||||
entity_category='diagnostic'),
|
||||
SensorDesc(key='selfcheck_result', field='x.com.samsung.da.result',
|
||||
name='Self-check result', icon='mdi:clipboard-check-outline',
|
||||
entity_category='diagnostic'),
|
||||
ButtonDesc(key='selfcheck_start', field='', name='Start self-check',
|
||||
payload='Start', icon='mdi:play-circle-outline',
|
||||
entity_category='diagnostic',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['selfcheck', 'vs', '0'], {'x.com.samsung.da.status': p})),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Refrigeration modes (rapid cooling)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -484,3 +541,85 @@ DOOR_GENERIC = Capability(
|
||||
value_fn=lambda v: v == 'Open'),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Aggregate-resource fallbacks
|
||||
#
|
||||
# /doors/vs/0, /temperatures/vs/0, and /icemaker/status/vs/0 each duplicate
|
||||
# information exposed more precisely by per-instance hrefs (DOOR_GENERIC,
|
||||
# TEMP_CURRENT_GENERIC/TEMP_SETPOINT_GENERIC, ICEMAKER_GENERIC) on hardware
|
||||
# that has them. Not every fridge does — a simpler model may only ever
|
||||
# advertise the aggregate resource. Each fallback's match_fn checks the
|
||||
# full resource set for the richer sibling hrefs and only binds when
|
||||
# they're absent, so it's a no-op (not a gap — see discovery.py) wherever
|
||||
# the richer hrefs exist, and a real (if coarser) source of the same data
|
||||
# where they don't.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _any_door_generic(resources):
|
||||
return any(h.startswith('/door/') for h in resources)
|
||||
|
||||
|
||||
DOORS_FALLBACK = Capability(
|
||||
href='/doors/vs/0',
|
||||
match_fn=lambda rep, resources: not _any_door_generic(resources),
|
||||
poll_tier='hot',
|
||||
entities=(
|
||||
BinarySensorDesc(key='door_open', field='x.com.samsung.da.items',
|
||||
name='Door', device_class='door',
|
||||
value_fn=lambda items: any(
|
||||
i.get('x.com.samsung.da.openState') == 'Open'
|
||||
for i in (items or []))),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _any_temperature_generic(resources):
|
||||
return any(h.startswith('/temperature/') for h in resources)
|
||||
|
||||
|
||||
def _temp_item_value(items, keyword):
|
||||
for item in (items or []):
|
||||
if keyword.lower() in (item.get('x.com.samsung.da.description') or '').lower():
|
||||
return _int(item.get('x.com.samsung.da.current'))
|
||||
return None
|
||||
|
||||
|
||||
TEMPERATURES_FALLBACK = Capability(
|
||||
href='/temperatures/vs/0',
|
||||
match_fn=lambda rep, resources: not _any_temperature_generic(resources),
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SensorDesc(key='freezer_temperature', field='x.com.samsung.da.items',
|
||||
name='Freezer temperature', icon='mdi:thermometer',
|
||||
device_class='temperature', unit='°F', state_class='measurement',
|
||||
value_fn=lambda items: _temp_item_value(items, 'Freezer')),
|
||||
SensorDesc(key='fridge_temperature', field='x.com.samsung.da.items',
|
||||
name='Fridge temperature', icon='mdi:thermometer',
|
||||
device_class='temperature', unit='°F', state_class='measurement',
|
||||
value_fn=lambda items: _temp_item_value(items, 'Fridge')),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _any_icemaker_unit_generic(resources):
|
||||
return any(
|
||||
h.startswith('/icemaker/') and isinstance(r, dict)
|
||||
and 'x.com.samsung.da.iceMaker.state' in r
|
||||
for h, r in resources.items()
|
||||
)
|
||||
|
||||
|
||||
ICEMAKER_STATUS_FALLBACK = Capability(
|
||||
href='/icemaker/status/vs/0',
|
||||
match_fn=lambda rep, resources: not _any_icemaker_unit_generic(resources),
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SwitchDesc(key='ice_maker_enabled', field='x.com.samsung.da.iceMaker',
|
||||
name='Ice maker', icon='mdi:cube-outline',
|
||||
value_fn=lambda v: v == 'On',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['icemaker', 'status', 'vs', '0'],
|
||||
{'x.com.samsung.da.iceMaker': 'On' if p else 'Off'})),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -64,4 +64,34 @@ IGNORED: list[Capability] = [
|
||||
# /wm/editcourse/vs/0 is the raw encoded course-list byte string already
|
||||
# decoded and exposed via /course/vs/0 (dishwasher.CYCLE_OPTIONS).
|
||||
Capability(href='/wm/editcourse/vs/0'),
|
||||
|
||||
# Bixby audio feedback (chime + volume played when Bixby starts/stops
|
||||
# listening) — only meaningful with Bixby enabled, which this
|
||||
# integration has no local path to configure or use.
|
||||
Capability(href='/sec/networkaudio/audio/vs/0'),
|
||||
|
||||
# Static Bespoke-product-line flag, not appliance state.
|
||||
Capability(href='/bespoke/vs/0'),
|
||||
# Empty resource on every dump seen so far — nothing to expose.
|
||||
Capability(href='/defrost/prediction/vs/0'),
|
||||
# Seasonal defrost schedule (start/period/end per season). Automating
|
||||
# this cleanly would need a multi-field schedule editor; the practical
|
||||
# on/off control is fridge.DEFROST_DELAY.
|
||||
Capability(href='/defrost/reservation/vs/0'),
|
||||
# Warranty/service-plan enrollment status — every field reads "Unknown"
|
||||
# on hardware not enrolled in a Samsung Care+ style program.
|
||||
Capability(href='/dginformation/vs/0'),
|
||||
# AI energy-saving level; only one value ('1') is ever in
|
||||
# supportedAiLevel on hardware seen so far, so there's no real choice
|
||||
# to expose. Revisit if a device surfaces more than one supported level.
|
||||
Capability(href='/energy/ailevel/vs/0'),
|
||||
# Opaque integer with no supportedModes/options list to interpret it
|
||||
# against — meaning unclear from the raw resource alone.
|
||||
Capability(href='/runningmode/vs/0'),
|
||||
|
||||
# Demand-response energy planner — same utility-program dependency as
|
||||
# /drlc/vs/0 above; every dump seen so far is inert (plan: 'none').
|
||||
Capability(href='/energy/planner/vs/0'),
|
||||
# Temperature-unit display preference, redundant with HA's own units.
|
||||
Capability(href='/wm/submode/vs/0'),
|
||||
]
|
||||
|
||||
@@ -4,6 +4,12 @@ A device advertises a set of OCF resources keyed by href. For every href
|
||||
present in the registry, emit the capability's entities bound to that resource.
|
||||
Unknown hrefs are a coverage gap; each one is passed to the optional `log`
|
||||
callback (as the raw href, not a formatted message) and otherwise skipped.
|
||||
|
||||
An href with a registered capability whose rt_filter/match_fn declines for
|
||||
this particular device (e.g. a filter capability sitting on hardware that
|
||||
doesn't have that filter) is *not* a gap — a maintainer already looked at
|
||||
that href and decided how to handle it. Only hrefs absent from the registry
|
||||
entirely are reported.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -79,7 +85,7 @@ def discover(
|
||||
matched = True
|
||||
break
|
||||
|
||||
if not matched and log is not None:
|
||||
if not matched and not caps and log is not None:
|
||||
log(href)
|
||||
|
||||
return out
|
||||
|
||||
+7
@@ -6,16 +6,19 @@
|
||||
"cycle",
|
||||
"cycle_active",
|
||||
"delay_start_time",
|
||||
"diagnosis_status",
|
||||
"energy_kwh",
|
||||
"finish_time",
|
||||
"firmware_update",
|
||||
"heated_dry",
|
||||
"job_beginning_status",
|
||||
"led_brightness",
|
||||
"led_night_brightness",
|
||||
"led_night_end",
|
||||
"led_night_light",
|
||||
"led_night_start",
|
||||
"machine_state",
|
||||
"operation_origin",
|
||||
"power_switch",
|
||||
"power_watts",
|
||||
"progress",
|
||||
@@ -34,16 +37,20 @@
|
||||
"samsung_dishwasher_cycle",
|
||||
"samsung_dishwasher_cycle_active",
|
||||
"samsung_dishwasher_delay_start_time",
|
||||
"samsung_dishwasher_diagnosis_start",
|
||||
"samsung_dishwasher_diagnosis_status",
|
||||
"samsung_dishwasher_energy_kwh",
|
||||
"samsung_dishwasher_finish_time",
|
||||
"samsung_dishwasher_firmware_update",
|
||||
"samsung_dishwasher_heated_dry",
|
||||
"samsung_dishwasher_job_beginning_status",
|
||||
"samsung_dishwasher_led_brightness",
|
||||
"samsung_dishwasher_led_night_brightness",
|
||||
"samsung_dishwasher_led_night_end",
|
||||
"samsung_dishwasher_led_night_light",
|
||||
"samsung_dishwasher_led_night_start",
|
||||
"samsung_dishwasher_machine_state",
|
||||
"samsung_dishwasher_operation_origin",
|
||||
"samsung_dishwasher_pause",
|
||||
"samsung_dishwasher_power_switch",
|
||||
"samsung_dishwasher_power_watts",
|
||||
|
||||
+9
@@ -10,6 +10,8 @@
|
||||
"cooler_setpoint",
|
||||
"cooler_temperature",
|
||||
"day_brightness",
|
||||
"defrost_blocked",
|
||||
"defrost_delay",
|
||||
"door_alert",
|
||||
"door_cooler_open",
|
||||
"door_freezer_open",
|
||||
@@ -33,6 +35,8 @@
|
||||
"rapid_freezing",
|
||||
"rapid_fridge",
|
||||
"sabbath_mode",
|
||||
"selfcheck_result",
|
||||
"selfcheck_status",
|
||||
"welcome_lighting"
|
||||
],
|
||||
"discovery_unique_ids": [
|
||||
@@ -46,6 +50,8 @@
|
||||
"samsung_refrigerator_cooler_setpoint",
|
||||
"samsung_refrigerator_cooler_temperature",
|
||||
"samsung_refrigerator_day_brightness",
|
||||
"samsung_refrigerator_defrost_blocked",
|
||||
"samsung_refrigerator_defrost_delay",
|
||||
"samsung_refrigerator_door_alert",
|
||||
"samsung_refrigerator_door_cooler_open",
|
||||
"samsung_refrigerator_door_freezer_open",
|
||||
@@ -69,6 +75,9 @@
|
||||
"samsung_refrigerator_rapid_freezing",
|
||||
"samsung_refrigerator_rapid_fridge",
|
||||
"samsung_refrigerator_sabbath_mode",
|
||||
"samsung_refrigerator_selfcheck_result",
|
||||
"samsung_refrigerator_selfcheck_start",
|
||||
"samsung_refrigerator_selfcheck_status",
|
||||
"samsung_refrigerator_welcome_lighting"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -79,30 +79,34 @@ async def test_update_failed_on_persistent_poll_error(
|
||||
# Coverage-gap detection and the Repairs issue it raises
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def test_discovery_populates_device_type_and_unbound_hrefs(
|
||||
async def test_discovery_populates_device_type_with_no_unbound_hrefs(
|
||||
hass: HomeAssistant, mock_entry, mock_coordinator_session
|
||||
) -> None:
|
||||
"""The real refrigerator fixture is a recognized type with some gaps left."""
|
||||
"""The real refrigerator fixture is a recognized type, fully covered.
|
||||
|
||||
Every href in this fixture is either mapped to a capability or listed
|
||||
in capabilities.ignored — see tests/test_registry_ignored.py for the
|
||||
noise-suppression checks. If this starts failing because a genuinely
|
||||
new gap appeared, map it or ignore it rather than deleting the assertion.
|
||||
"""
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
||||
assert coordinator.device_type_name == 'refrigerator'
|
||||
assert coordinator.one_ui_version == '7.0 Refrigerator'
|
||||
# Real, currently-unmodeled hrefs on this fixture (not noise — see
|
||||
# tests/test_registry_ignored.py for the noise-suppression checks).
|
||||
assert '/bespoke/vs/0' in coordinator._unbound_hrefs
|
||||
assert coordinator._unbound_hrefs == []
|
||||
|
||||
|
||||
async def test_coverage_gap_issue_created_for_real_fixture_gaps(
|
||||
async def test_coverage_gap_issue_absent_for_fully_covered_real_fixture(
|
||||
hass: HomeAssistant, mock_entry, mock_coordinator_session
|
||||
) -> None:
|
||||
"""A recognized device with leftover unbound hrefs still raises the issue."""
|
||||
"""A recognized device with no unbound hrefs does not raise the issue."""
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue_id = f"device_gap_{mock_entry.entry_id}"
|
||||
assert ir.async_get(hass).async_get_issue(DOMAIN, issue_id) is not None
|
||||
assert ir.async_get(hass).async_get_issue(DOMAIN, issue_id) is None
|
||||
|
||||
|
||||
def test_update_coverage_gap_issue_creates_issue_for_unknown_type(
|
||||
|
||||
@@ -18,7 +18,7 @@ async def test_diagnostics_shape_and_redaction(
|
||||
|
||||
assert diagnostics["device_type"] == 'refrigerator'
|
||||
assert diagnostics["one_ui_version"] == '7.0 Refrigerator'
|
||||
assert '/bespoke/vs/0' in diagnostics["unbound_hrefs"]
|
||||
assert diagnostics["unbound_hrefs"] == []
|
||||
assert diagnostics["integration_version"] == '0.1.0'
|
||||
assert diagnostics["smartthings_local_version"]
|
||||
|
||||
|
||||
@@ -138,6 +138,26 @@ def test_discover_match_fn_filters_wrong_device():
|
||||
assert bound == []
|
||||
|
||||
|
||||
def test_discover_match_fn_decline_is_not_logged_as_gap():
|
||||
"""A registered href whose match_fn declines is known, not a gap.
|
||||
|
||||
Distinguishes "no capability registered for this href" (a real
|
||||
coverage gap) from "a capability is registered but chose not to bind
|
||||
for this device" (e.g. a filter capability on hardware that doesn't
|
||||
have that filter) — only the former should be reported via `log`.
|
||||
"""
|
||||
seen = []
|
||||
cap = Capability(
|
||||
href='/filter/vs/0',
|
||||
match_fn=lambda rep, resources: False,
|
||||
entities=(BinarySensorDesc(key='filter', field='x.com.samsung.da.filterStatus'),),
|
||||
)
|
||||
resources = {'/filter/vs/0': {'x.com.samsung.da.filterStatus': 'notused'}}
|
||||
bound = discover(resources, {'/filter/vs/0': [cap]}, log=seen.append)
|
||||
assert bound == []
|
||||
assert seen == []
|
||||
|
||||
|
||||
def test_discover_rt_filter_gates_binding():
|
||||
"""Cap with rt_filter must not bind a rep whose rt list does not match."""
|
||||
oven_mode_cap = Capability(
|
||||
|
||||
@@ -45,14 +45,15 @@ def test_ignored_hrefs_absent_from_refrigerator_gaps():
|
||||
assert not present_ignored, f"ignored hrefs leaked into gap report: {present_ignored}"
|
||||
|
||||
|
||||
def test_dishwasher_fixture_still_has_genuine_gaps():
|
||||
def test_dishwasher_fixture_has_no_remaining_gaps():
|
||||
"""Sanity check that the gap mechanism isn't silencing everything.
|
||||
|
||||
If this starts failing because the fixture is now fully covered, that's
|
||||
good news — just trim the expected set down (or to empty) rather than
|
||||
deleting the test.
|
||||
Every href in the dishwasher fixture is now either mapped to a
|
||||
capability or listed in capabilities.ignored — if this starts failing
|
||||
because a genuinely new gap appeared, that's the interesting case to
|
||||
look at (add the href to ignored.py or map it, don't just delete this
|
||||
assertion).
|
||||
"""
|
||||
resources = _load('dishwasher_device.json')
|
||||
unbound = set(_unbound_hrefs(resources, dishwasher.REGISTRY))
|
||||
assert '/wm/jobbeginingstatus/vs/0' in unbound
|
||||
assert '/diagnosis/vs/0' in unbound
|
||||
assert unbound == set()
|
||||
|
||||
Reference in New Issue
Block a user