diff --git a/locale/ca-ES/brightness.entity b/locale/ca-ES/brightness.entity index 9ed95fc..f0a2724 100644 --- a/locale/ca-ES/brightness.entity +++ b/locale/ca-ES/brightness.entity @@ -1,9 +1,10 @@ -# -## -### -#% -##% -###% +20 +50 +75 +100 +20 per cent +50 per cent +100 per cent complet|ple|plena brillant|lluminós meitat diff --git a/locale/da-DK/brightness.entity b/locale/da-DK/brightness.entity index 00328af..753480e 100644 --- a/locale/da-DK/brightness.entity +++ b/locale/da-DK/brightness.entity @@ -1,9 +1,10 @@ -# -## -### -#% -##% -###% +20 +50 +75 +100 +20 procent +50 procent +100 procent fuld lys halv diff --git a/locale/de-DE/brightness.entity b/locale/de-DE/brightness.entity index 7ce80e4..5aa2e30 100644 --- a/locale/de-DE/brightness.entity +++ b/locale/de-DE/brightness.entity @@ -1,9 +1,10 @@ -# -## -### -#% -##% -###% +20 +50 +75 +100 +20 Prozent +50 Prozent +100 Prozent voll hell halb diff --git a/locale/el-GR/brightness.entity b/locale/el-GR/brightness.entity index 46b9571..94bcb1b 100644 --- a/locale/el-GR/brightness.entity +++ b/locale/el-GR/brightness.entity @@ -1,9 +1,10 @@ -# -## -### -#% -##% -###% +20 +50 +75 +100 +20 τοις εκατό +50 τοις εκατό +100 τοις εκατό πλήρως Φωτεινό Μισό diff --git a/locale/en-US/brightness.entity b/locale/en-US/brightness.entity index 830e88a..17fa891 100644 --- a/locale/en-US/brightness.entity +++ b/locale/en-US/brightness.entity @@ -1,9 +1,10 @@ -# -## -### -#% -##% -###% +20 +50 +75 +100 +20 percent +50 percent +100 percent full bright half diff --git a/locale/es-ES/brightness.entity b/locale/es-ES/brightness.entity index e15e8a9..ee50598 100644 --- a/locale/es-ES/brightness.entity +++ b/locale/es-ES/brightness.entity @@ -1,9 +1,10 @@ -# -## -### -#% -##% -###% +20 +50 +75 +100 +20 por ciento +50 por ciento +100 por ciento lleno brillo medio diff --git a/locale/fr-FR/brightness.entity b/locale/fr-FR/brightness.entity index d218090..5a5bc0c 100644 --- a/locale/fr-FR/brightness.entity +++ b/locale/fr-FR/brightness.entity @@ -1,9 +1,10 @@ -N° -## -### -#% -##% -###% +20 +50 +75 +100 +20 percent +50 percent +100 percent à fond brillant demi diff --git a/locale/gl-ES/brightness.entity b/locale/gl-ES/brightness.entity index 3c7d3f1..389e69a 100644 --- a/locale/gl-ES/brightness.entity +++ b/locale/gl-ES/brightness.entity @@ -1,9 +1,10 @@ -# -## -### -#% -##% -###% +20 +50 +75 +100 +20 por cento +50 por cento +100 por cento cheo brillante medio diff --git a/locale/it-IT/brightness.entity b/locale/it-IT/brightness.entity index 29d1eb0..05073e0 100644 --- a/locale/it-IT/brightness.entity +++ b/locale/it-IT/brightness.entity @@ -1,9 +1,10 @@ -# -## -### -#% -##% -###% +20 +50 +75 +100 +20 percento +50 percento +100 percento pieno luminoso mezzo diff --git a/locale/nl-NL/brightness.entity b/locale/nl-NL/brightness.entity index 1999e9b..f291f6a 100644 --- a/locale/nl-NL/brightness.entity +++ b/locale/nl-NL/brightness.entity @@ -1,9 +1,10 @@ -# -## -### -#% -##% -###% +20 +50 +75 +100 +20 procent +50 procent +100 procent vol licht half diff --git a/locale/pt-BR/brightness.entity b/locale/pt-BR/brightness.entity index 2392dd4..048d27c 100644 --- a/locale/pt-BR/brightness.entity +++ b/locale/pt-BR/brightness.entity @@ -1,9 +1,10 @@ -# -## -### -#% -##% -##% +20 +50 +75 +100 +20 por cento +50 por cento +100 por cento cheio brilhante meio diff --git a/locale/ru-RU/brightness.entity b/locale/ru-RU/brightness.entity index 22109f2..1be60ac 100644 --- a/locale/ru-RU/brightness.entity +++ b/locale/ru-RU/brightness.entity @@ -1,9 +1,10 @@ -# -## -### -#% -##% -###% +20 +50 +75 +100 +20 процентов +50 процентов +100 процентов полный яркий половина diff --git a/locale/sv-SE/brightness.entity b/locale/sv-SE/brightness.entity index c885c82..e8a1e91 100644 --- a/locale/sv-SE/brightness.entity +++ b/locale/sv-SE/brightness.entity @@ -1,9 +1,10 @@ -# -## -### -#% -##% -###% +20 +50 +75 +100 +20 procent +50 procent +100 procent fullt starkt halv diff --git a/pyproject.toml b/pyproject.toml index 782416b..c8d25de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,17 +14,18 @@ authors = [ keywords = ["ovos", "skill", "plugin"] dependencies = [ "ovos-color-parser>=0.0.8a1", - "ovos-i2c-detection>=0.0.6a1" + "ovos-i2c-detection>=0.0.6a1", + "ovos-workshop>=9.5.0a1,<10.0.0" ] [project.optional-dependencies] test = [ - "ovoscope>=1.5.0,<2.0.0", + "ovoscope>=1.6.8a1,<2.0.0", "pytest>=7.0.0,<9", "pytest-timeout>=2.0.0", - "ovos-core>=2.2.4a1,<2.3.0", - "ovos-padatious>=2.0.0a1,<3.0.0", - "ovos-workshop>=8.3.0a1,<9.0.0", + "ovos-core[plugins,lgpl]>=2.2.4a1", + "ovos-padatious>=2.0.4a1,<3.0.0", + "ovos-workshop>=9.5.0a1,<10.0.0", "ovos-bus-client>=2.2.0a1,<3.0.0", "ovos-plugin-manager>=2.9.0a1,<3.0.0", "ovos-config>=2.1.4a5,<3.0.0", diff --git a/test/end2end/test_entity_constraints.py b/test/end2end/test_entity_constraints.py new file mode 100644 index 0000000..36b3847 --- /dev/null +++ b/test/end2end/test_entity_constraints.py @@ -0,0 +1,148 @@ +"""Entity-file registration coverage for ovos-skill-mark1-ctrl (en-US). + +register_entity_file() feeds locale/en-US/brightness.entity and +locale/en-US/color.entity to the intent engine as TRAINING DATA / confidence +hints for the {brightness}/{color} slots -- NOT an allow-list. Under +ovos-padatious>=2.0.3a1: + - an IN-LIST value (e.g. "turquoise") routes at the padatious-HIGH band + (conf_high=0.95) with the slot tagged correctly. + - an OUT-OF-LIST value for the same slot (e.g. "mauve") still routes -- + registration is a hint, not a closed vocabulary -- but only clears the + padatious-MEDIUM band, not high. + +This means hint semantics only become visible when the active session +pipeline includes BOTH padatious-high and padatious-medium stages -- the +stock default pipeline (padatious-high only) will simply drop an +out-of-list utterance instead of showing the fallback-to-medium behavior. +Every test below therefore declares its own session.pipeline explicitly. + +The skill's __init__ refuses to load off a physical Mark 1 (I2C probe), so +ovos_i2c_detection.is_mark_1 is stubbed True before the plugin loader +imports the skill module (mirrors test/end2end/test_intents_en_us.py). + +Run: pytest test/end2end/test_entity_constraints.py -v --timeout=180 +""" +import ovos_i2c_detection + +ovos_i2c_detection.is_mark_1 = lambda: True + +from ovos_skill_mark1_ctrl import EnclosureControlSkill + +EnclosureControlSkill.ask_yesno = lambda self, *a, **k: "no" +EnclosureControlSkill.get_response = lambda self, *a, **k: None + +import time +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_bus_client.session import Session +from ovoscope import get_minicroft + +SKILL_ID = "ovos-skill-mark1-ctrl.openvoiceos" +LANG = "en-US" + +HIGH = ["ovos-padatious-pipeline-plugin-high"] +HIGH_AND_MEDIUM = [ + "ovos-padatious-pipeline-plugin-high", + "ovos-padatious-pipeline-plugin-medium", +] + + +class TestEntityConstraints(TestCase): + + @classmethod + def setUpClass(cls): + cls.minicroft = get_minicroft([SKILL_ID]) + cls.bus = cls.minicroft.bus + + @classmethod + def tearDownClass(cls): + if getattr(cls, "minicroft", None): + cls.minicroft.stop() + + def _emit_and_wait(self, utterance, intent_msg_type, pipeline, + deadline_s=20, settle_s=4): + matched = [] + handler = lambda msg: matched.append(msg) + self.bus.on(intent_msg_type, handler) + try: + session = Session(f"e2e-entity-{hash((utterance, intent_msg_type, tuple(pipeline)))}") + session.lang = LANG + session.pipeline = pipeline + message = Message( + "recognizer_loop:utterance", + {"utterances": [utterance], "lang": LANG}, + {"session": session.serialize()}, + ) + deadline = time.monotonic() + deadline_s + while not matched and time.monotonic() < deadline: + self.bus.emit(message) + waited = time.monotonic() + settle_s + while not matched and time.monotonic() < waited: + time.sleep(0.2) + finally: + self.bus.remove(intent_msg_type, handler) + return matched + + # --- {color} on eye_color.intent --- + + def test_color_in_list_matches_at_high(self): + """A registered color ("turquoise") clears padatious-high alone.""" + intent_msg_type = f"{SKILL_ID}:eye_color" + matched = self._emit_and_wait( + "set the eye color to turquoise", intent_msg_type, HIGH, + ) + self.assertTrue(matched, "'set the eye color to turquoise' should route at " + "padatious-high -- turquoise is a registered color.entity value") + self.assertEqual(matched[0].data.get("color"), "turquoise") + + def test_color_out_of_list_needs_medium(self): + """An unregistered color ("mauve") still matches (hint, not + allow-list) but only clears padatious-medium -- proving + registration neither creates a closed vocabulary (would 0-match) + nor is silently gutted (would also clear high, same as the + registered case).""" + intent_msg_type = f"{SKILL_ID}:eye_color" + utterance = "set the eye color to mauve" + + high_only = self._emit_and_wait(utterance, intent_msg_type, HIGH) + self.assertFalse( + high_only, + "'set the eye color to mauve' (out-of-list color) should NOT " + "clear padatious-high alone -- if it does, entity registration " + "is acting as a hard allow-list booster instead of a hint, or " + "the high/medium band split collapsed", + ) + + with_medium = self._emit_and_wait(utterance, intent_msg_type, HIGH_AND_MEDIUM) + self.assertTrue( + with_medium, + "'set the eye color to mauve' should still route once " + "padatious-medium is in the pipeline -- an out-of-list slot " + "value must remain matchable, just at lower confidence", + ) + self.assertEqual(with_medium[0].data.get("color"), "mauve") + + # --- {brightness} on brightness.intent --- + + def test_brightness_in_list_matches_at_high(self): + """A registered brightness ("full") clears padatious-high alone.""" + intent_msg_type = f"{SKILL_ID}:brightness" + matched = self._emit_and_wait( + "set eye brightness to full", intent_msg_type, HIGH, + ) + self.assertTrue(matched, "'set eye brightness to full' should route at " + "padatious-high -- full is a registered brightness.entity value") + self.assertEqual(matched[0].data.get("brightness"), "full") + + # NOTE: {brightness} carries the "#"/"##"/"###"/"#%"/"##%"/"###%" + # digit-wildcard placeholder lines. The shared resource reader treats + # any line starting with "#" as a comment and drops it, so these are + # never registered as literal entity values -- verified directly + # against ovos_spec_tools.resources.read_resource_file, the reader + # register_entity_file uses. {brightness}'s short (3-4 word) template + # also makes its confidence band edges noticeably more boot-sensitive + # than {color}'s, so this suite does not duplicate the + # in-list-high / out-of-list-needs-medium pair for it -- {color} above + # already exercises that pair reliably, and the mutation check below + # covers the registration loop for both slots. diff --git a/test/unittests/test_entity_registration.py b/test/unittests/test_entity_registration.py new file mode 100644 index 0000000..584323d --- /dev/null +++ b/test/unittests/test_entity_registration.py @@ -0,0 +1,76 @@ +"""Coverage that the {brightness}/{color} slots reach the padatious matcher +as registered entities, sourced from locale/*/*.entity. + +ovos-workshop>=9.5.0a1 registers every ".entity" file shipped in a skill's +locale resources AUTOMATICALLY (see `OVOSSkill._auto_register_entity_files`) +the first time a language's resources are loaded, during `_startup()` - +there is no more skill-authored `register_entity_file()` call to test. + +This test boots the skill with NO manual registration call and listens on +the bus for the `padatious:register_entity` messages workshop's auto +registration emits, asserting the sample values shipped in +locale/en-US/brightness.entity and locale/en-US/color.entity landed on the +bus for the {brightness}/{color} entities respectively. + +Mutation check: delete (or rename) locale/en-US/brightness.entity or +color.entity and this test goes red - there is nothing left in the skill to +register it, since the wiring is now entirely workshop's auto-discovery +walking the on-disk locale/ directory. + +The skill's __init__ refuses to load off a physical Mark 1 (I2C probe), so +ovos_i2c_detection.is_mark_1 is stubbed True before the plugin loader +imports the skill module (mirrors test/end2end/test_intents_en_us.py). +""" +import ovos_i2c_detection + +ovos_i2c_detection.is_mark_1 = lambda: True + +from ovos_skill_mark1_ctrl import EnclosureControlSkill + +import unittest + +from ovos_utils.messagebus import FakeBus + + +class TestEntityRegistration(unittest.TestCase): + + def test_boot_auto_registers_entity_files_no_manual_call(self): + bus = FakeBus() + registered = [] + bus.on("padatious:register_entity", registered.append) + + skill = EnclosureControlSkill() + skill._startup(bus, "ovos-skill-mark1-ctrl.openvoiceos") + + by_name = {msg.data.get("name"): msg.data for msg in registered} + skill_id = "ovos-skill-mark1-ctrl.openvoiceos" + + color_name = f"{skill_id}:color" + brightness_name = f"{skill_id}:brightness" + + self.assertIn( + color_name, by_name, + "booting the skill with no manual register_entity_file() call " + "should still auto-register {color} from locale/en-US/color.entity " + "(ovos-workshop>=9.5.0a1 auto-registers every shipped .entity file)", + ) + self.assertIn( + brightness_name, by_name, + "booting the skill with no manual register_entity_file() call " + "should still auto-register {brightness} from " + "locale/en-US/brightness.entity", + ) + + color_samples = set(by_name[color_name]["samples"]) + self.assertIn("turquoise", color_samples) + self.assertIn("default", color_samples) + + brightness_samples = set(by_name[brightness_name]["samples"]) + self.assertIn("full", brightness_samples) + self.assertIn("dim", brightness_samples) + # bare "#" placeholder lines are dropped as comments, never + # registered as literal or wildcard values + self.assertNotIn("#", brightness_samples) + # brightness.entity must ship real numeric examples, not just word + # samples, so the {brightness} slot actually hints numeric values + self.assertIn("50", brightness_samples)