From 60bee334e22960cc013a5744a9236ec9218777fb Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sat, 1 Aug 2026 01:07:30 +0100 Subject: [PATCH 1/8] feat: FakeBus mirrors the .intent-suffixed twin for aliased intents Old ovos-workshop built the per-intent dispatch topic from the padatious resource filename, so the `.intent` extension leaked onto the wire. The real MessageBusClient now mirrors an intent dispatch onto that suffixed twin when emit_legacy is on. A test double that skipped the mirror would hide the compat path from every harness built on it. FakeBus and AsyncFakeBus now share a `_LegacyIntentBridge` that calls `ovos_spec_tools.intent_topics.legacy_reemit_targets` after the namespace counterpart dispatch, with an `IntentAliasRegistry` filled from the bus's own on() / once() calls. Blanket mode (`intent_reemit_blanket`, off by default) mirrors every intent dispatch for pure-bus listeners that never register. The spec-tools import is guarded: against a release without the intent-topic helpers both fake buses behave exactly as before. Co-Authored-By: Claude Fable 5 --- ovos_utils/fakebus.py | 115 ++++++++- test/unittests/log_test/configured.log | 222 +++++++--------- test/unittests/log_test/rotate.log | 2 +- test/unittests/log_test/rotate.log.1 | 2 +- .../test_fakebus_intent_legacy_reemit.py | 239 ++++++++++++++++++ 5 files changed, 450 insertions(+), 130 deletions(-) create mode 100644 test/unittests/test_fakebus_intent_legacy_reemit.py diff --git a/ovos_utils/fakebus.py b/ovos_utils/fakebus.py index cc80c98f..1d48a50a 100644 --- a/ovos_utils/fakebus.py +++ b/ovos_utils/fakebus.py @@ -57,7 +57,98 @@ def _resolve_bus_flags(kwargs): return NamespaceTranslator(modernize=modernize, emit_legacy=emit_legacy) -class FakeBus: +# --- legacy intent-topic compat (non-normative migration tooling) ---------- +# +# Old ovos-workshop releases built the per-intent dispatch topic from the +# padatious resource FILENAME, so the ``.intent`` extension leaked onto the +# wire: a skill with ``food.order.intent`` listened on +# ``:food.order.intent``. Current workshop is spec-pure and +# registers the canonical ``:food.order`` (OVOS-MSG-1 §2.1.1). +# +# ``ovos_spec_tools.intent_topics`` is the whole compat surface for that gap. +# It is newer than the spec-tools floor declared here, so the import is +# guarded: against an older spec-tools the fake bus behaves as before. +try: + from ovos_spec_tools.intent_topics import (IntentAliasRegistry, + legacy_reemit_targets) + _HAS_INTENT_TOPICS = True +except ImportError: # spec-tools without OVOS-INTENT-4 compat helpers + IntentAliasRegistry = None + legacy_reemit_targets = None + _HAS_INTENT_TOPICS = False + +#: Context flag stamped on a mirrored intent dispatch. A message carrying it is +#: already a twin, so it is never mirrored again. Same key the real +#: ``MessageBusClient`` uses. +INTENT_REEMIT_CONTEXT_KEY = "__legacy_intent_reemit__" + + +class _LegacyIntentBridge: + """Mirror an intent dispatch onto its legacy ``.intent``-suffixed twin. + + Shared by :class:`FakeBus` and :class:`AsyncFakeBus` so both test doubles + behave like ``ovos_bus_client.MessageBusClient``, which runs the same + bridge next to its namespace bridge. A test double that skipped it would + hide the compat path from every harness built on it. + + The alias table is owned by the bus instance and filled from its own + ``on()`` / ``once()`` calls: a bus mirrors only the intents one of its own + handlers asked for by the suffixed name, so no topic nobody listens on is + invented. + """ + + def _init_intent_bridge(self, kwargs): + self._intent_aliases = IntentAliasRegistry() if _HAS_INTENT_TOPICS else None + blanket = kwargs.get("intent_reemit_blanket", _UNSET) + if blanket is _UNSET: + blanket = _bus_flag("OVOS_BUS_INTENT_REEMIT_BLANKET", + "intent_reemit_blanket", default=False) + # blanket mode mirrors EVERY intent dispatch, registered alias or not, + # for pure-bus listeners that subscribe without registering. It doubles + # intent traffic, so it is off unless asked for. + self._intent_reemit_blanket = blanket + + def _record_intent_alias(self, msg_type): + """Note that a handler subscribed to ``msg_type``. + + Only per-intent dispatch topics are recorded; the registry ignores + everything else. A subscription written with the legacy ``.intent`` + suffix is what marks the canonical intent as needing the mirror. + """ + if self._intent_aliases is not None: + self._intent_aliases.register(msg_type) + + def _forget_intent_alias(self, msg_type): + """Drop the alias of ``msg_type`` once nothing listens on it.""" + if self._intent_aliases is None: + return + alias = self._intent_aliases.legacy_alias(msg_type) + if alias is None: + return + if not self.ee.listeners(alias): + self._intent_aliases.deregister(msg_type) + + def _reemit_legacy_intent(self, message): + """Dispatch the suffixed twin of ``message``, if one is called for. + + The twin carries the same data and context plus + :data:`INTENT_REEMIT_CONTEXT_KEY`, and fires at most once: it is + already the suffixed spelling, which ``legacy_reemit_targets`` never + mirrors again. + """ + if self._intent_aliases is None or not self._translator.emit_legacy: + return + if message.context.get(INTENT_REEMIT_CONTEXT_KEY): + return + for topic in legacy_reemit_targets(message.msg_type, + registry=self._intent_aliases, + blanket=self._intent_reemit_blanket): + twin = message.forward(topic, message.data) + twin.context[INTENT_REEMIT_CONTEXT_KEY] = True + self.ee.emit(topic, twin) + + +class FakeBus(_LegacyIntentBridge): def __init__(self, *args, **kwargs): self.started_running = False self.session_id = "default" @@ -70,6 +161,8 @@ def __init__(self, *args, **kwargs): self._translator = _resolve_bus_flags(kwargs) self._handler_guards = {} # handler -> shared mirror-guard self._dedup_registrations = {} # handler -> [(msg_type, wrapped), ...] + # legacy intent-topic bridge, gated by the SAME emit_legacy flag + self._init_intent_bridge(kwargs) self.on_open() try: self.session_id = kwargs["session"].session_id @@ -80,6 +173,7 @@ def __init__(self, *args, **kwargs): self.on_default_session_update) def on(self, msg_type, handler): + self._record_intent_alias(msg_type) # wrap handlers on migrated topics so a handler subscribed to both the # legacy and ovos.* topic fires once (the mirror is dropped) if self._translator.is_migrated(msg_type): @@ -99,6 +193,7 @@ def wrapped(message=None): self.ee.on(msg_type, handler) def once(self, msg_type, handler): + self._record_intent_alias(msg_type) self.ee.once(msg_type, handler) def emit(self, message): @@ -140,6 +235,9 @@ def emit(self, message): self.ee.emit(topic, message.forward(topic, translated)) except Exception as e: LOG.exception(f"Error in counterpart dispatch for '{topic}': {e}") + # legacy intent-topic bridge: mirror an intent dispatch onto its + # ``.intent``-suffixed twin for handlers written against old workshop. + self._reemit_legacy_intent(message) def on_message(self, *args): """ @@ -239,14 +337,17 @@ def remove(self, msg_type, handler): if not regs: self._dedup_registrations.pop(handler, None) self._handler_guards.pop(handler, None) + self._forget_intent_alias(msg_type) return try: self.ee.remove_listener(msg_type, handler) except Exception: pass + self._forget_intent_alias(msg_type) def remove_all_listeners(self, event_name): self.ee.remove_all_listeners(event_name) + self._forget_intent_alias(event_name) def create_client(self): return self @@ -357,7 +458,7 @@ def __new__(cls, *args, **kwargs): return FakeMessage(*args, **kwargs) -class AsyncFakeBus: +class AsyncFakeBus(_LegacyIntentBridge): """In-process stand-in for ``AsyncMessageBusClient``. Mirrors the same surface as the real async bus client: ``connect`` / @@ -385,6 +486,8 @@ def __init__(self, *args, **kwargs): self._translator = _resolve_bus_flags(kwargs) self._handler_guards = {} # handler -> shared mirror-guard self._dedup_registrations = {} # handler -> [(msg_type, wrapped), ...] + # legacy intent-topic bridge, gated by the SAME emit_legacy flag + self._init_intent_bridge(kwargs) self.connected_event = asyncio.Event() self.connected_event.set() self.on_open() @@ -401,6 +504,7 @@ def __init__(self, *args, **kwargs): # ------------------------------------------------------------------ def on(self, msg_type, handler): + self._record_intent_alias(msg_type) # wrap handlers on migrated topics so a handler subscribed to both the # legacy and ovos.* topic fires once (the mirror is dropped) -- same as # FakeBus.on / MessageBusClient.on. @@ -421,6 +525,7 @@ def wrapped(message=None): self.ee.on(msg_type, handler) def once(self, msg_type, handler): + self._record_intent_alias(msg_type) self.ee.once(msg_type, handler) def remove(self, msg_type, handler): @@ -435,14 +540,17 @@ def remove(self, msg_type, handler): if not regs: self._dedup_registrations.pop(handler, None) self._handler_guards.pop(handler, None) + self._forget_intent_alias(msg_type) return try: self.ee.remove_listener(msg_type, handler) except Exception: pass + self._forget_intent_alias(msg_type) def remove_all_listeners(self, event_name): self.ee.remove_all_listeners(event_name) + self._forget_intent_alias(event_name) # ------------------------------------------------------------------ # Lifecycle (async) @@ -493,6 +601,9 @@ async def emit(self, message): self.ee.emit(topic, message.forward(topic, translated)) except Exception as e: LOG.exception(f"Error in counterpart dispatch for '{topic}': {e}") + # legacy intent-topic bridge: mirror an intent dispatch onto its + # ``.intent``-suffixed twin for handlers written against old workshop. + self._reemit_legacy_intent(message) # ------------------------------------------------------------------ # Sync helpers used internally — same as FakeBus diff --git a/test/unittests/log_test/configured.log b/test/unittests/log_test/configured.log index c889d3fa..b6e14f13 100644 --- a/test/unittests/log_test/configured.log +++ b/test/unittests/log_test/configured.log @@ -1,126 +1,96 @@ -2026-03-11 04:12:11.787 - configured - ovos_utils.network_utils:get_external_ip:78 - ERROR - Got resp=503: -2026-03-11 04:12:11.789 - configured - ovos_utils.network_utils:get_external_ip:80 - ERROR - Unable to get external IP Address: network error -2026-03-11 04:12:11.796 - configured - ovos_utils.network_utils:check_captive_portal:162 - ERROR - Error checking for captive portal -Traceback (most recent call last): - File "/home/miro/PycharmProjects/OpenVoiceOS Workspace/ovos-utils/ovos_utils/network_utils.py", line 156, in check_captive_portal - html_doc = requests.get(host).text - ~~~~~~~~~~~~^^^^^^ - File "/home/miro/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/unittest/mock.py", line 1169, in __call__ - return self._mock_call(*args, **kwargs) - ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^ - File "/home/miro/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/unittest/mock.py", line 1173, in _mock_call - return self._execute_mock_call(*args, **kwargs) - ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^ - File "/home/miro/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/unittest/mock.py", line 1228, in _execute_mock_call - raise effect -Exception: timeout -2026-03-11 04:12:11.823 - configured - ovos_utils.ocp:from_dict:254 - ERROR - track dictionary does not contain 'uri', it is not a valid MediaEntry -2026-03-11 04:12:11.824 - configured - ovos_utils.ocp:from_dict:256 - WARNING - DEPRECATED: use dict2entry() for Playlists and PluginStreams, MediaEntry.from_dict is only for regular media, will start throwing ValueError in 0.1.0 -2026-03-11 04:12:11.837 - configured - ovos_utils.ocp:available_extractors:165 - ERROR - please install/update ovos_plugin_manager -2026-03-11 04:12:11.841 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 1 -2026-03-11 04:12:11.842 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 0 -2026-03-11 04:12:11.842 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 0 -2026-03-11 04:12:11.843 - configured - ovos_utils.ocp:goto_track:588 - ERROR - requested track not in the playlist: MediaEntry(uri='http://missing.com/x.mp3', title='', artist='', match_confidence=0, skill_id='ovos.common_play', playback=, status=, media_type=, length=0, image='', skill_icon='', javascript='') -2026-03-11 04:12:11.852 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 2 -2026-03-11 04:12:11.853 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 1 -2026-03-11 04:12:11.854 - configured - ovos_utils.ocp:goto_track:588 - ERROR - requested track not in the playlist: MediaEntry(uri='nonexistent', title='nonexistent', artist='', match_confidence=0, skill_id='ovos.common_play', playback=, status=, media_type=, length=0, image='', skill_icon='', javascript='') -2026-03-11 04:12:11.857 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (3! Going to start of playlist -2026-03-11 04:12:11.858 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (-1! Going to start of playlist -2026-03-11 04:12:11.859 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (-1! Going to start of playlist -2026-03-11 04:12:11.860 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (100! Going to start of playlist -2026-03-11 04:12:11.877 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (0! Going to start of playlist -2026-03-11 04:12:11.878 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (-1! Going to start of playlist -2026-03-11 04:12:11.878 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (1! Going to start of playlist -2026-03-11 04:12:11.879 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (1! Going to start of playlist -2026-03-11 04:12:11.880 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (10! Going to start of playlist -2026-03-11 04:12:11.882 - configured - ovos_utils.parse:_validate_matching_strategy:27 - ERROR - rapidfuzz is not installed, falling back to SequenceMatcher for all match strategies -2026-03-11 04:12:11.882 - configured - ovos_utils.parse:_validate_matching_strategy:29 - WARNING - pip install rapidfuzz -2026-03-11 04:12:11.904 - configured - ovos_utils.security:decrypt:93 - ERROR - run pip install pycryptodomex -2026-03-11 04:12:11.906 - configured - ovos_utils.security:decrypt:103 - ERROR - decryption failed, invalid key? -2026-03-11 04:12:11.910 - configured - ovos_utils.security:encrypt:80 - ERROR - run pip install pycryptodomex -2026-03-11 04:12:11.912 - configured - ovos_utils.security:create_self_signed_cert:34 - ERROR - run pip install pyopenssl -2026-03-11 04:12:11.917 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.918 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'my_service' -2026-03-11 04:12:11.918 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.919 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.920 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.921 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.923 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.924 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.924 - configured - ovos_utils.skill_installer:handle_install_python:391 - ERROR - pip disabled in mycroft.conf -2026-03-11 04:12:11.925 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.926 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.927 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.928 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.929 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.931 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.932 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.932 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.933 - configured - ovos_utils.skill_installer:handle_uninstall_python:427 - ERROR - pip disabled in mycroft.conf -2026-03-11 04:12:11.934 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.934 - configured - ovos_utils.skill_installer:pip_install:230 - ERROR - no package list provided to install -2026-03-11 04:12:11.935 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.937 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.938 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg -2026-03-11 04:12:11.938 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://example.com/c.txt my-pkg -2026-03-11 04:12:11.939 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.940 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg -2026-03-11 04:12:11.940 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/PycharmProjects/OpenVoiceOS Workspace/ovos-utils/.venv/bin/python3 -m pip install -c http://example.com/c.txt my-pkg -2026-03-11 04:12:11.941 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.942 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing bad-pkg -2026-03-11 04:12:11.943 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c https://raw.githubusercontent.com/OpenVoiceOS/ovos-releases/refs/heads/main/constraints-stable.txt bad-pkg -2026-03-11 04:12:11.944 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.945 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing pkg -2026-03-11 04:12:11.945 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt pkg -2026-03-11 04:12:11.947 - configured - ovos_utils.skill_installer:validate_constraints:205 - ERROR - Couldn't find the constraints file -2026-03-11 04:12:11.948 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt -2026-03-11 04:12:11.949 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/missing.txt -2026-03-11 04:12:11.949 - configured - ovos_utils.skill_installer:validate_constraints:195 - ERROR - Remote constraints file not accessible: 404 -2026-03-11 04:12:11.950 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt -2026-03-11 04:12:11.951 - configured - ovos_utils.skill_installer:validate_constraints:201 - ERROR - Error accessing remote constraints: timeout -2026-03-11 04:12:11.952 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.953 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.954 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.954 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-03-11 04:12:11.955 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt pkg -2026-03-11 04:12:11.956 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.956 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-03-11 04:12:11.957 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt --break-system-packages pkg -2026-03-11 04:12:11.958 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.959 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-03-11 04:12:11.959 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt --pre pkg -2026-03-11 04:12:11.960 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.961 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-03-11 04:12:11.961 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt pkg -2026-03-11 04:12:11.962 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.963 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-03-11 04:12:11.963 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt pkg -2026-03-11 04:12:11.964 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.965 - configured - ovos_utils.skill_installer:pip_uninstall:295 - ERROR - no package list provided to uninstall -2026-03-11 04:12:11.966 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.967 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:12.298 - configured - ovos_utils.skill_installer:pip_uninstall:330 - ERROR - tried to uninstall a protected package: ['ovos-core'] -2026-03-11 04:12:12.299 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:12.461 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:12.462 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg -2026-03-11 04:12:12.464 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:12.925 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:12.926 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/PycharmProjects/OpenVoiceOS Workspace/ovos-utils/.venv/bin/python3 -m pip uninstall -y custom-pkg -2026-03-11 04:12:12.929 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:13.262 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:13.263 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y custom-pkg -2026-03-11 04:12:13.267 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:13.269 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:13.294 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y custom-pkg -2026-03-11 04:12:13.296 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:13.296 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:13.297 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y custom-pkg -2026-03-11 04:12:13.298 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:13.569 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:13.570 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y --break-system-packages custom-pkg -2026-03-11 04:12:13.571 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:13.859 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:13.859 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y custom-pkg -2026-03-11 04:12:13.913 - configured - ovos_utils.system:system_reboot:83 - DEBUG - sudo systemctl reboot -i -2026-03-11 04:12:13.915 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - sudo systemctl poweroff -i -2026-03-11 04:12:13.916 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - systemctl poweroff -i +2026-08-01 01:07:01.284 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - systemctl poweroff -i +2026-08-01 01:07:01.296 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - sudo systemctl poweroff -i +2026-08-01 01:07:01.308 - configured - ovos_utils.system:system_reboot:83 - DEBUG - sudo systemctl reboot -i +2026-08-01 01:07:01.423 - configured - ovos_utils.security:decrypt:105 - ERROR - run pip install pycryptodomex +2026-08-01 01:07:01.434 - configured - ovos_utils.security:encrypt:92 - ERROR - run pip install pycryptodomex +2026-08-01 01:07:01.444 - configured - ovos_utils.security:decrypt:115 - ERROR - decryption failed, invalid key? +2026-08-01 01:07:01.463 - configured - ovos_utils.security:create_self_signed_cert:46 - ERROR - run pip install pyopenssl +2026-08-01 01:07:11.840 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:11.847 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:11.855 - configured - ovos_utils.skill_installer:handle_install_python:391 - ERROR - pip disabled in mycroft.conf +2026-08-01 01:07:11.862 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:11.871 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:11.878 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:11.885 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:11.892 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:11.900 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:11.908 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:11.918 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt +2026-08-01 01:07:11.925 - configured - ovos_utils.skill_installer:validate_constraints:205 - ERROR - Couldn't find the constraints file +2026-08-01 01:07:11.931 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt +2026-08-01 01:07:11.936 - configured - ovos_utils.skill_installer:validate_constraints:201 - ERROR - Error accessing remote constraints: timeout +2026-08-01 01:07:11.943 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/missing.txt +2026-08-01 01:07:11.948 - configured - ovos_utils.skill_installer:validate_constraints:195 - ERROR - Remote constraints file not accessible: 404 +2026-08-01 01:07:11.955 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:12.220 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 01:07:12.226 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.venvs/ovos/bin/python -m pip uninstall -y custom-pkg +2026-08-01 01:07:12.232 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:12.238 - configured - ovos_utils.skill_installer:pip_uninstall:295 - ERROR - no package list provided to uninstall +2026-08-01 01:07:12.246 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:12.254 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:12.490 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 01:07:12.495 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y --break-system-packages custom-pkg +2026-08-01 01:07:12.502 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:12.508 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 01:07:12.513 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg +2026-08-01 01:07:12.520 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:12.591 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 01:07:12.596 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg +2026-08-01 01:07:12.604 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:12.667 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 01:07:12.673 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg +2026-08-01 01:07:12.683 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:12.764 - configured - ovos_utils.skill_installer:pip_uninstall:330 - ERROR - tried to uninstall a protected package: ['ovos-core'] +2026-08-01 01:07:12.779 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:12.788 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 01:07:12.795 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg +2026-08-01 01:07:12.803 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:12.874 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 01:07:12.880 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg +2026-08-01 01:07:12.889 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:12.896 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing pkg +2026-08-01 01:07:12.903 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg +2026-08-01 01:07:12.912 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:12.919 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing bad-pkg +2026-08-01 01:07:12.924 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c https://raw.githubusercontent.com/OpenVoiceOS/ovos-releases/refs/heads/main/constraints-stable.txt bad-pkg +2026-08-01 01:07:12.934 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:12.943 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:12.950 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg +2026-08-01 01:07:12.957 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://example.com/c.txt my-pkg +2026-08-01 01:07:12.966 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:12.973 - configured - ovos_utils.skill_installer:pip_install:230 - ERROR - no package list provided to install +2026-08-01 01:07:12.984 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:12.990 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg +2026-08-01 01:07:12.995 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.venvs/ovos/bin/python -m pip install -c http://example.com/c.txt my-pkg +2026-08-01 01:07:13.005 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:13.024 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:13.036 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:13.043 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-08-01 01:07:13.049 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg +2026-08-01 01:07:13.056 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:13.065 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:13.074 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:13.078 - configured - ovos_utils.skill_installer:handle_uninstall_python:427 - ERROR - pip disabled in mycroft.conf +2026-08-01 01:07:13.087 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 01:07:13.095 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'my_service' +2026-08-01 01:07:13.102 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:13.109 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:13.116 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:13.122 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-08-01 01:07:13.127 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg +2026-08-01 01:07:13.135 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:13.140 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-08-01 01:07:13.146 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt --pre pkg +2026-08-01 01:07:13.154 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:13.159 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-08-01 01:07:13.164 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt --break-system-packages pkg +2026-08-01 01:07:13.171 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 01:07:13.176 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-08-01 01:07:13.181 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg +2026-08-01 01:07:13.194 - configured - ovos_utils.events:add:165 - DEBUG - Added event: id:f +2026-08-01 01:07:13.205 - configured - ovos_utils.events:remove:173 - DEBUG - Removing event id:f +2026-08-01 01:07:13.347 - configured - ovos_utils.gui:get_ui_directories:90 - DEBUG - Skill supports GUI framework: qt5 from folder: /home/miro/tmp/tmpilwf5jno/gui/qt5 +2026-08-01 01:07:13.352 - configured - ovos_utils.gui:get_ui_directories:90 - DEBUG - Skill supports GUI framework: kivy from folder: /home/miro/tmp/tmpilwf5jno/gui/kivy +2026-08-01 01:07:13.360 - configured - ovos_utils.gui:get_ui_directories:85 - DEBUG - legacy UI directory found - Handling `ui` directory as `qt5` +2026-08-01 01:07:13.421 - configured - ovos_utils.parse:_validate_matching_strategy:27 - ERROR - rapidfuzz is not installed, falling back to SequenceMatcher for all match strategies +2026-08-01 01:07:13.426 - configured - ovos_utils.parse:_validate_matching_strategy:29 - WARNING - pip install rapidfuzz diff --git a/test/unittests/log_test/rotate.log b/test/unittests/log_test/rotate.log index dd7d75aa..85156fa2 100644 --- a/test/unittests/log_test/rotate.log +++ b/test/unittests/log_test/rotate.log @@ -1 +1 @@ -2026-03-11 04:12:13.924 - rotate - ovos_utils.system:ssh_enable - WARNING - Deprecation version=0.2.0. Caller=test_system:165. DEPRECATED: use ovos-PHAL-plugin-system +2026-08-01 01:07:13.189 - rotate - ovos_utils.events:EventSchedulerInterface.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_event_scheduler:110. EventSchedulerInterface moved to ovos_bus_client. 'from ovos_bus_client.apis.events import EventSchedulerInterface' diff --git a/test/unittests/log_test/rotate.log.1 b/test/unittests/log_test/rotate.log.1 index 8f4286ae..d353a90e 100644 --- a/test/unittests/log_test/rotate.log.1 +++ b/test/unittests/log_test/rotate.log.1 @@ -1 +1 @@ -2026-03-11 04:12:13.923 - rotate - ovos_utils.system:ssh_disable - WARNING - Deprecation version=0.2.0. Caller=test_system:175. DEPRECATED: use ovos-PHAL-plugin-system +2026-08-01 01:07:02.718 - rotate - ovos_utils.sound:_find_player:66 - ERROR - Can't find player for: test.xyz diff --git a/test/unittests/test_fakebus_intent_legacy_reemit.py b/test/unittests/test_fakebus_intent_legacy_reemit.py new file mode 100644 index 00000000..75b89f9b --- /dev/null +++ b/test/unittests/test_fakebus_intent_legacy_reemit.py @@ -0,0 +1,239 @@ +"""FakeBus mirrors MessageBusClient's legacy intent-topic bridge. + +Old ovos-workshop built the per-intent dispatch topic from the resource +filename, so ``:food.order.intent`` reached the wire. Current +workshop registers the canonical ``:food.order``. When emit_legacy +is on, a bus that has a handler bound to the suffixed spelling also gets the +dispatch mirrored onto that spelling. + +Both fake buses must behave like the real client, otherwise every harness +built on them hides the compat path. +""" +import asyncio +import unittest + +from ovos_spec_tools import Message + +from ovos_utils.fakebus import (INTENT_REEMIT_CONTEXT_KEY, AsyncFakeBus, + FakeBus) + +CANONICAL = "skill-food.jarbas:food.order" +LEGACY = "skill-food.jarbas:food.order.intent" + + +def _run(coro): + return asyncio.run(coro) + + +class TestAliasDrivenReemit(unittest.TestCase): + def test_suffixed_subscription_receives_canonical_dispatch(self): + bus = FakeBus() + got = [] + bus.on(LEGACY, got.append) + bus.emit(Message(CANONICAL, {"utterance": "one pizza"})) + self.assertEqual([m.msg_type for m in got], [LEGACY]) + self.assertEqual(got[0].data, {"utterance": "one pizza"}) + + def test_mirror_keeps_data_and_context(self): + bus = FakeBus() + got = [] + bus.on(LEGACY, got.append) + bus.emit(Message(CANONICAL, {"a": 1}, {"source": ["me"]})) + self.assertEqual(got[0].data, {"a": 1}) + self.assertEqual(got[0].context["source"], ["me"]) + + def test_mirror_is_marked_in_context(self): + bus = FakeBus() + got = [] + bus.on(LEGACY, got.append) + bus.emit(Message(CANONICAL)) + self.assertTrue(got[0].context[INTENT_REEMIT_CONTEXT_KEY]) + + def test_no_mirror_without_a_suffixed_subscription(self): + bus = FakeBus() + got = [] + bus.on(CANONICAL, got.append) + bus.emit(Message(CANONICAL)) + self.assertEqual([m.msg_type for m in got], [CANONICAL]) + + def test_once_subscription_also_registers_the_alias(self): + bus = FakeBus() + got = [] + bus.once(LEGACY, got.append) + bus.emit(Message(CANONICAL)) + self.assertEqual([m.msg_type for m in got], [LEGACY]) + + def test_non_intent_topics_are_never_mirrored(self): + bus = FakeBus() + got = [] + bus.on("ovos.utterance.handled.intent", got.append) + bus.emit(Message("ovos.utterance.handled")) + self.assertEqual(got, []) + + +class TestExactlyOnce(unittest.TestCase): + def test_one_dispatch_yields_one_mirror(self): + bus = FakeBus() + got = [] + bus.on(LEGACY, got.append) + bus.emit(Message(CANONICAL)) + self.assertEqual(len(got), 1) + + def test_two_suffixed_handlers_each_run_once(self): + bus = FakeBus() + a, b = [], [] + bus.on(LEGACY, a.append) + bus.on(LEGACY, b.append) + bus.emit(Message(CANONICAL)) + self.assertEqual((len(a), len(b)), (1, 1)) + + def test_handler_on_both_spellings_gets_both_topics_once_each(self): + # the intent bridge does not dedupe across spellings - a handler bound + # to both asked for both. Workshop collapses aliases at registration. + bus = FakeBus() + got = [] + bus.on(CANONICAL, got.append) + bus.on(LEGACY, got.append) + bus.emit(Message(CANONICAL)) + self.assertEqual([m.msg_type for m in got], [CANONICAL, LEGACY]) + + +class TestLoopPrevention(unittest.TestCase): + def test_a_legacy_dispatch_is_not_mirrored_again(self): + bus = FakeBus() + got = [] + bus.on(LEGACY, got.append) + bus.on(CANONICAL, got.append) + bus.emit(Message(LEGACY)) + self.assertEqual([m.msg_type for m in got], [LEGACY]) + + def test_a_marked_message_is_not_mirrored(self): + bus = FakeBus() + got = [] + bus.on(LEGACY, got.append) + bus.emit(Message(CANONICAL, {}, {INTENT_REEMIT_CONTEXT_KEY: True})) + self.assertEqual(got, []) + + def test_reemitting_a_mirror_terminates(self): + bus = FakeBus(intent_reemit_blanket=True) + got = [] + bus.on(LEGACY, got.append) + bus.emit(Message(CANONICAL)) + bus.emit(got[0]) # feed the twin back in + self.assertEqual(len(got), 2) + + +class TestBlanketMode(unittest.TestCase): + def test_blanket_mirrors_without_any_registration(self): + bus = FakeBus(intent_reemit_blanket=True) + got = [] + bus.ee.on(LEGACY, got.append) # subscribe behind the bus's back + bus.emit(Message(CANONICAL)) + self.assertEqual([m.msg_type for m in got], [LEGACY]) + + def test_blanket_off_by_default(self): + bus = FakeBus() + got = [] + bus.ee.on(LEGACY, got.append) + bus.emit(Message(CANONICAL)) + self.assertEqual(got, []) + + def test_blanket_still_skips_non_intent_topics(self): + bus = FakeBus(intent_reemit_blanket=True) + got = [] + bus.ee.on("ovos.utterance.handled.intent", got.append) + bus.emit(Message("ovos.utterance.handled")) + self.assertEqual(got, []) + + +class TestDisabled(unittest.TestCase): + def test_no_mirror_when_emit_legacy_is_off(self): + bus = FakeBus(emit_legacy=False) + got = [] + bus.on(LEGACY, got.append) + bus.emit(Message(CANONICAL)) + self.assertEqual(got, []) + + def test_no_mirror_when_emit_legacy_is_off_even_in_blanket(self): + bus = FakeBus(emit_legacy=False, intent_reemit_blanket=True) + got = [] + bus.ee.on(LEGACY, got.append) + bus.emit(Message(CANONICAL)) + self.assertEqual(got, []) + + def test_no_mirror_without_spec_tools_intent_support(self): + bus = FakeBus() + bus._intent_aliases = None # older spec-tools: helpers not importable + got = [] + bus.ee.on(LEGACY, got.append) + bus.emit(Message(CANONICAL)) + self.assertEqual(got, []) + + +class TestAliasLifecycle(unittest.TestCase): + def test_removing_the_last_suffixed_handler_stops_the_mirror(self): + bus = FakeBus() + got = [] + bus.on(LEGACY, got.append) + bus.remove(LEGACY, got.append) + bus.emit(Message(CANONICAL)) + self.assertEqual(got, []) + + def test_remove_all_listeners_stops_the_mirror(self): + bus = FakeBus() + got = [] + bus.on(LEGACY, got.append) + bus.remove_all_listeners(LEGACY) + bus.emit(Message(CANONICAL)) + self.assertEqual(got, []) + + def test_one_removal_of_two_handlers_keeps_the_mirror(self): + bus = FakeBus() + a, b = [], [] + bus.on(LEGACY, a.append) + bus.on(LEGACY, b.append) + bus.remove(LEGACY, a.append) + bus.emit(Message(CANONICAL)) + self.assertEqual(len(b), 1) + + +class TestAsyncFakeBusParity(unittest.TestCase): + def test_suffixed_subscription_receives_canonical_dispatch(self): + bus = AsyncFakeBus() + got = [] + bus.on(LEGACY, got.append) + _run(bus.emit(Message(CANONICAL, {"utterance": "one pizza"}))) + self.assertEqual([m.msg_type for m in got], [LEGACY]) + self.assertEqual(got[0].data, {"utterance": "one pizza"}) + + def test_no_mirror_without_a_suffixed_subscription(self): + bus = AsyncFakeBus() + got = [] + bus.on(CANONICAL, got.append) + _run(bus.emit(Message(CANONICAL))) + self.assertEqual([m.msg_type for m in got], [CANONICAL]) + + def test_legacy_dispatch_is_not_mirrored_again(self): + bus = AsyncFakeBus() + got = [] + bus.on(LEGACY, got.append) + _run(bus.emit(Message(LEGACY))) + self.assertEqual(len(got), 1) + + def test_blanket_mode(self): + bus = AsyncFakeBus(intent_reemit_blanket=True) + got = [] + bus.ee.on(LEGACY, got.append) + _run(bus.emit(Message(CANONICAL))) + self.assertEqual([m.msg_type for m in got], [LEGACY]) + + def test_no_mirror_when_emit_legacy_is_off(self): + bus = AsyncFakeBus(emit_legacy=False) + got = [] + bus.on(LEGACY, got.append) + _run(bus.emit(Message(CANONICAL))) + self.assertEqual(got, []) + + +if __name__ == "__main__": + unittest.main() From 5c98237c7a07801d85fddf22cb2d0438279513d0 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sat, 1 Aug 2026 14:10:46 +0100 Subject: [PATCH 2/8] build: floor ovos-spec-tools at 1.6.0a1 for the intent_topics helpers Co-Authored-By: Claude Fable 5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 597e101a..02714e0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "rich-click~=1.7", "rich~=13.7", "python-dateutil", - "ovos-spec-tools>=0.16.1a2", + "ovos-spec-tools>=1.6.0a1", # intent_topics helpers for the FakeBus legacy re-emit ] [project.urls] From 5af0c0a9acff5495c474c9b84005af583d80f9c2 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sat, 1 Aug 2026 14:14:45 +0100 Subject: [PATCH 3/8] feat!: drop the legacy wire compat bridge from FakeBus FakeBus and AsyncFakeBus mirrored the two migration bridges that MessageBusClient ran. Both are removed there, so both are removed here. Gone from the fake bus: * the OVOS-MSG-1 namespace bridge and its payload reshaping; * the handler mirror-guard that made dual-namespace subscriptions safe; * the intent-topic twin, which mirrored a canonical : dispatch onto the old :.intent spelling. A test double that kept the bridge would be worse than useless: every harness built on it would pass against behaviour the fleet no longer has. The emit_legacy, modernize and intent_reemit_blanket flags are removed in both spellings the fake bus accepted - constructor kwarg and env/config - and raise RuntimeError when explicitly enabled. Passing them as False is still accepted, so callers that already turned the bridge off need no edit. test_fakebus_namespace_migration.py and test_fakebus_intent_legacy_reemit.py proved the bridge worked; test_fakebus_no_legacy_compat.py proves it is absent, sweeping the whole MIGRATION_MAP in both directions. The AsyncFakeBus namespace class is inverted in place. BREAKING CHANGE: FakeBus no longer bridges legacy bus topics. A test that emitted a legacy topic and listened on the spec one (or the reverse) must pick one namespace. emit_legacy / modernize / intent_reemit_blanket raise RuntimeError when enabled. Co-Authored-By: Claude Fable 5 --- ovos_utils/fakebus.py | 283 ++++-------------- test/unittests/log_test/configured.log | 155 ++++------ test/unittests/log_test/rotate.log | 1 - test/unittests/log_test/rotate.log.1 | 1 - test/unittests/test_async_fakebus.py | 51 ++-- .../test_fakebus_intent_legacy_reemit.py | 239 --------------- .../test_fakebus_namespace_migration.py | 150 ---------- .../test_fakebus_no_legacy_compat.py | 143 +++++++++ 8 files changed, 283 insertions(+), 740 deletions(-) delete mode 100644 test/unittests/log_test/rotate.log delete mode 100644 test/unittests/log_test/rotate.log.1 delete mode 100644 test/unittests/test_fakebus_intent_legacy_reemit.py delete mode 100644 test/unittests/test_fakebus_namespace_migration.py create mode 100644 test/unittests/test_fakebus_no_legacy_compat.py diff --git a/ovos_utils/fakebus.py b/ovos_utils/fakebus.py index 1d48a50a..44899a03 100644 --- a/ovos_utils/fakebus.py +++ b/ovos_utils/fakebus.py @@ -4,7 +4,6 @@ from threading import Event from ovos_utils.log import LOG, log_deprecation -from ovos_spec_tools import NamespaceTranslator from pyee import EventEmitter @@ -17,16 +16,11 @@ def dig_for_message(): return None -# sentinel: lets us tell "kwarg not passed" apart from "kwarg passed True/False" -_UNSET = object() - - -def _bus_flag(env_var, config_key, default=True): +def _bus_flag(env_var, config_key, default=False): """Resolve a boolean bus flag the way ``MessageBusClient._bus_flag`` does. Precedence: env var (when set) > ``websocket.`` in ovos_config - > ``default``. The env var wins when set to a truthy/falsy string; ovos_config - is optional, so any failure to read it falls back to ``default``. + > ``default``. Kept layering-clean: mirrors ``ovos_bus_client.client.client._bus_flag`` without importing from bus-client (bus-client depends on utils, not vice-versa). @@ -41,128 +35,64 @@ def _bus_flag(env_var, config_key, default=True): return default -def _resolve_bus_flags(kwargs): - """Build the namespace ``NamespaceTranslator`` for a fake bus instance. - - An explicitly-passed ``modernize``/``emit_legacy`` kwarg wins (back-compat for - callers passing ``emit_legacy=True/False``); otherwise the flag is resolved via - env var -> ``websocket.*`` config -> default ``True``, matching the real client. - """ - modernize = kwargs.get("modernize", _UNSET) - if modernize is _UNSET: - modernize = _bus_flag("OVOS_BUS_MODERNIZE", "modernize", default=True) - emit_legacy = kwargs.get("emit_legacy", _UNSET) - if emit_legacy is _UNSET: - emit_legacy = _bus_flag("OVOS_BUS_EMIT_LEGACY", "emit_legacy", default=True) - return NamespaceTranslator(modernize=modernize, emit_legacy=emit_legacy) - - -# --- legacy intent-topic compat (non-normative migration tooling) ---------- -# -# Old ovos-workshop releases built the per-intent dispatch topic from the -# padatious resource FILENAME, so the ``.intent`` extension leaked onto the -# wire: a skill with ``food.order.intent`` listened on -# ``:food.order.intent``. Current workshop is spec-pure and -# registers the canonical ``:food.order`` (OVOS-MSG-1 §2.1.1). +# --- the legacy wire bridge is GONE ----------------------------------------- # -# ``ovos_spec_tools.intent_topics`` is the whole compat surface for that gap. -# It is newer than the spec-tools floor declared here, so the import is -# guarded: against an older spec-tools the fake bus behaves as before. -try: - from ovos_spec_tools.intent_topics import (IntentAliasRegistry, - legacy_reemit_targets) - _HAS_INTENT_TOPICS = True -except ImportError: # spec-tools without OVOS-INTENT-4 compat helpers - IntentAliasRegistry = None - legacy_reemit_targets = None - _HAS_INTENT_TOPICS = False - -#: Context flag stamped on a mirrored intent dispatch. A message carrying it is -#: already a twin, so it is never mirrored again. Same key the real -#: ``MessageBusClient`` uses. -INTENT_REEMIT_CONTEXT_KEY = "__legacy_intent_reemit__" - - -class _LegacyIntentBridge: - """Mirror an intent dispatch onto its legacy ``.intent``-suffixed twin. - - Shared by :class:`FakeBus` and :class:`AsyncFakeBus` so both test doubles - behave like ``ovos_bus_client.MessageBusClient``, which runs the same - bridge next to its namespace bridge. A test double that skipped it would - hide the compat path from every harness built on it. - - The alias table is owned by the bus instance and filled from its own - ``on()`` / ``once()`` calls: a bus mirrors only the intents one of its own - handlers asked for by the suffixed name, so no topic nobody listens on is - invented. +# The fake bus used to mirror ``MessageBusClient``'s two migration bridges: the +# OVOS-MSG-1 namespace bridge (a spec topic also reached listeners on the +# legacy topic it replaced, and the reverse) and the OVOS-INTENT-4 intent-topic +# bridge (a canonical ``:`` dispatch also reached the old +# ``:.intent`` spelling). Both were removed from the real +# client, so both are removed here — a test double that kept them would let a +# harness pass against behaviour the fleet no longer has. + +#: Bus flags that used to steer the bridge. +_REMOVED_BRIDGE_FLAGS = ( + ("OVOS_BUS_EMIT_LEGACY", "emit_legacy"), + ("OVOS_BUS_MODERNIZE", "modernize"), + ("OVOS_BUS_INTENT_REEMIT_BLANKET", "intent_reemit_blanket"), +) + + +def _reject_removed_bridge_flags(kwargs): + """Handle a caller that still asks for the removed bridge. + + The two spellings get different treatment on purpose. + + An **env var or config entry** is an operator asking a live deployment to + keep the legacy topics on the wire. That belief is now wrong, and silence + would hand them a fleet that drops messages, so it raises — the same error + the real ``MessageBusClient`` raises. + + A **constructor kwarg** is a test harness, not a deployment. Harnesses + across the ecosystem pass ``emit_legacy=True`` unconditionally (ovoscope's + ``MiniCroft`` is one), and raising there would break every one of them at + construction without telling anybody anything useful. The kwarg is + accepted, ignored, and warned about instead. """ - - def _init_intent_bridge(self, kwargs): - self._intent_aliases = IntentAliasRegistry() if _HAS_INTENT_TOPICS else None - blanket = kwargs.get("intent_reemit_blanket", _UNSET) - if blanket is _UNSET: - blanket = _bus_flag("OVOS_BUS_INTENT_REEMIT_BLANKET", - "intent_reemit_blanket", default=False) - # blanket mode mirrors EVERY intent dispatch, registered alias or not, - # for pure-bus listeners that subscribe without registering. It doubles - # intent traffic, so it is off unless asked for. - self._intent_reemit_blanket = blanket - - def _record_intent_alias(self, msg_type): - """Note that a handler subscribed to ``msg_type``. - - Only per-intent dispatch topics are recorded; the registry ignores - everything else. A subscription written with the legacy ``.intent`` - suffix is what marks the canonical intent as needing the mirror. - """ - if self._intent_aliases is not None: - self._intent_aliases.register(msg_type) - - def _forget_intent_alias(self, msg_type): - """Drop the alias of ``msg_type`` once nothing listens on it.""" - if self._intent_aliases is None: - return - alias = self._intent_aliases.legacy_alias(msg_type) - if alias is None: - return - if not self.ee.listeners(alias): - self._intent_aliases.deregister(msg_type) - - def _reemit_legacy_intent(self, message): - """Dispatch the suffixed twin of ``message``, if one is called for. - - The twin carries the same data and context plus - :data:`INTENT_REEMIT_CONTEXT_KEY`, and fires at most once: it is - already the suffixed spelling, which ``legacy_reemit_targets`` never - mirrors again. - """ - if self._intent_aliases is None or not self._translator.emit_legacy: - return - if message.context.get(INTENT_REEMIT_CONTEXT_KEY): - return - for topic in legacy_reemit_targets(message.msg_type, - registry=self._intent_aliases, - blanket=self._intent_reemit_blanket): - twin = message.forward(topic, message.data) - twin.context[INTENT_REEMIT_CONTEXT_KEY] = True - self.ee.emit(topic, twin) - - -class FakeBus(_LegacyIntentBridge): + for env_var, config_key in _REMOVED_BRIDGE_FLAGS: + if _bus_flag(env_var, config_key, default=False): + raise RuntimeError( + f"'{config_key}' (env {env_var}) is enabled, but the legacy " + "wire bridge was removed from the fake bus, matching " + "ovos-bus-client. Legacy bus topics are no longer emitted or " + "mirrored. Migrate the producers and consumers to the " + f"OVOS-MSG-1 spec topics, then unset '{config_key}'.") + if kwargs.get(config_key): + log_deprecation( + f"the '{config_key}' kwarg no longer does anything: the " + "legacy wire bridge was removed from the fake bus. Drop it " + "from the call.", "1.0.0") + + +class FakeBus: def __init__(self, *args, **kwargs): self.started_running = False self.session_id = "default" self.ee = kwargs.get("emitter") or EventEmitter() self.ee.on("error", self.on_error) - # mirror MessageBusClient's namespace migration so the test/satellite - # double bridges legacy<->ovos.* topics identically. Flags resolve the - # same way the real client does: explicit modernize=/emit_legacy= kwarg - # wins, else env var -> websocket.* config -> default on. - self._translator = _resolve_bus_flags(kwargs) - self._handler_guards = {} # handler -> shared mirror-guard - self._dedup_registrations = {} # handler -> [(msg_type, wrapped), ...] - # legacy intent-topic bridge, gated by the SAME emit_legacy flag - self._init_intent_bridge(kwargs) + # the migration window is over: no namespace bridge, no intent-topic + # twin, no mirror-guard — matching MessageBusClient. + _reject_removed_bridge_flags(kwargs) self.on_open() try: self.session_id = kwargs["session"].session_id @@ -173,27 +103,9 @@ def __init__(self, *args, **kwargs): self.on_default_session_update) def on(self, msg_type, handler): - self._record_intent_alias(msg_type) - # wrap handlers on migrated topics so a handler subscribed to both the - # legacy and ovos.* topic fires once (the mirror is dropped) - if self._translator.is_migrated(msg_type): - guard = self._handler_guards.get(handler) - if guard is None: - guard = self._translator.new_mirror_guard() - self._handler_guards[handler] = guard - - def wrapped(message=None): - if guard(message): - return - return handler(message) - - self.ee.on(msg_type, wrapped) - self._dedup_registrations.setdefault(handler, []).append((msg_type, wrapped)) - return self.ee.on(msg_type, handler) def once(self, msg_type, handler): - self._record_intent_alias(msg_type) self.ee.once(msg_type, handler) def emit(self, message): @@ -221,23 +133,6 @@ def emit(self, message): self.ee.emit(message.msg_type, message) except Exception as e: LOG.exception(f"Error in event handler for '{message.msg_type}': {e}") - # namespace migration: also dispatch the counterpart topic(s) so a - # listener on either namespace receives the event (consumers dedupe). - # the mirrored payload is reshaped into the counterpart topic's shape - # (identity for payload-compatible renames, a per-topic transform for - # shape-changing ones) so a listener on it receives the payload in *its* - # shape -- matching MessageBusClient's bridge. - for topic in self._translator.counterpart_topics(message.msg_type): - try: - translated = self._translator.translate_payload( - from_topic=message.msg_type, to_topic=topic, - data=message.data) - self.ee.emit(topic, message.forward(topic, translated)) - except Exception as e: - LOG.exception(f"Error in counterpart dispatch for '{topic}': {e}") - # legacy intent-topic bridge: mirror an intent dispatch onto its - # ``.intent``-suffixed twin for handlers written against old workshop. - self._reemit_legacy_intent(message) def on_message(self, *args): """ @@ -326,28 +221,13 @@ def rcv(m): return msg def remove(self, msg_type, handler): - regs = self._dedup_registrations.get(handler) - if regs: - for ev, wrapped in [r for r in regs if r[0] == msg_type]: - try: - self.ee.remove_listener(ev, wrapped) - except Exception: - pass - regs.remove((ev, wrapped)) - if not regs: - self._dedup_registrations.pop(handler, None) - self._handler_guards.pop(handler, None) - self._forget_intent_alias(msg_type) - return try: self.ee.remove_listener(msg_type, handler) except Exception: pass - self._forget_intent_alias(msg_type) def remove_all_listeners(self, event_name): self.ee.remove_all_listeners(event_name) - self._forget_intent_alias(event_name) def create_client(self): return self @@ -458,7 +338,7 @@ def __new__(cls, *args, **kwargs): return FakeMessage(*args, **kwargs) -class AsyncFakeBus(_LegacyIntentBridge): +class AsyncFakeBus(): """In-process stand-in for ``AsyncMessageBusClient``. Mirrors the same surface as the real async bus client: ``connect`` / @@ -482,12 +362,8 @@ def __init__(self, *args, **kwargs): self.session_id = "default" self.ee = kwargs.get("emitter") or EventEmitter() self.ee.on("error", self.on_error) - # mirror MessageBusClient's namespace migration (see FakeBus.__init__). - self._translator = _resolve_bus_flags(kwargs) - self._handler_guards = {} # handler -> shared mirror-guard - self._dedup_registrations = {} # handler -> [(msg_type, wrapped), ...] - # legacy intent-topic bridge, gated by the SAME emit_legacy flag - self._init_intent_bridge(kwargs) + # no legacy wire bridge (see FakeBus.__init__). + _reject_removed_bridge_flags(kwargs) self.connected_event = asyncio.Event() self.connected_event.set() self.on_open() @@ -504,53 +380,19 @@ def __init__(self, *args, **kwargs): # ------------------------------------------------------------------ def on(self, msg_type, handler): - self._record_intent_alias(msg_type) - # wrap handlers on migrated topics so a handler subscribed to both the - # legacy and ovos.* topic fires once (the mirror is dropped) -- same as - # FakeBus.on / MessageBusClient.on. - if self._translator.is_migrated(msg_type): - guard = self._handler_guards.get(handler) - if guard is None: - guard = self._translator.new_mirror_guard() - self._handler_guards[handler] = guard - - def wrapped(message=None): - if guard(message): - return - return handler(message) - - self.ee.on(msg_type, wrapped) - self._dedup_registrations.setdefault(handler, []).append((msg_type, wrapped)) - return self.ee.on(msg_type, handler) def once(self, msg_type, handler): - self._record_intent_alias(msg_type) self.ee.once(msg_type, handler) def remove(self, msg_type, handler): - regs = self._dedup_registrations.get(handler) - if regs: - for ev, wrapped in [r for r in regs if r[0] == msg_type]: - try: - self.ee.remove_listener(ev, wrapped) - except Exception: - pass - regs.remove((ev, wrapped)) - if not regs: - self._dedup_registrations.pop(handler, None) - self._handler_guards.pop(handler, None) - self._forget_intent_alias(msg_type) - return try: self.ee.remove_listener(msg_type, handler) except Exception: pass - self._forget_intent_alias(msg_type) def remove_all_listeners(self, event_name): self.ee.remove_all_listeners(event_name) - self._forget_intent_alias(event_name) # ------------------------------------------------------------------ # Lifecycle (async) @@ -591,19 +433,6 @@ async def emit(self, message): self.ee.emit(message.msg_type, message) except Exception as e: LOG.exception(f"Error in event handler for '{message.msg_type}': {e}") - # namespace migration: also dispatch the counterpart topic(s) with the - # payload reshaped into each counterpart's shape -- same as FakeBus.emit. - for topic in self._translator.counterpart_topics(message.msg_type): - try: - translated = self._translator.translate_payload( - from_topic=message.msg_type, to_topic=topic, - data=message.data) - self.ee.emit(topic, message.forward(topic, translated)) - except Exception as e: - LOG.exception(f"Error in counterpart dispatch for '{topic}': {e}") - # legacy intent-topic bridge: mirror an intent dispatch onto its - # ``.intent``-suffixed twin for handlers written against old workshop. - self._reemit_legacy_intent(message) # ------------------------------------------------------------------ # Sync helpers used internally — same as FakeBus diff --git a/test/unittests/log_test/configured.log b/test/unittests/log_test/configured.log index b6e14f13..66ca1d13 100644 --- a/test/unittests/log_test/configured.log +++ b/test/unittests/log_test/configured.log @@ -1,96 +1,59 @@ -2026-08-01 01:07:01.284 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - systemctl poweroff -i -2026-08-01 01:07:01.296 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - sudo systemctl poweroff -i -2026-08-01 01:07:01.308 - configured - ovos_utils.system:system_reboot:83 - DEBUG - sudo systemctl reboot -i -2026-08-01 01:07:01.423 - configured - ovos_utils.security:decrypt:105 - ERROR - run pip install pycryptodomex -2026-08-01 01:07:01.434 - configured - ovos_utils.security:encrypt:92 - ERROR - run pip install pycryptodomex -2026-08-01 01:07:01.444 - configured - ovos_utils.security:decrypt:115 - ERROR - decryption failed, invalid key? -2026-08-01 01:07:01.463 - configured - ovos_utils.security:create_self_signed_cert:46 - ERROR - run pip install pyopenssl -2026-08-01 01:07:11.840 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:11.847 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:11.855 - configured - ovos_utils.skill_installer:handle_install_python:391 - ERROR - pip disabled in mycroft.conf -2026-08-01 01:07:11.862 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:11.871 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:11.878 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:11.885 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:11.892 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:11.900 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:11.908 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:11.918 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt -2026-08-01 01:07:11.925 - configured - ovos_utils.skill_installer:validate_constraints:205 - ERROR - Couldn't find the constraints file -2026-08-01 01:07:11.931 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt -2026-08-01 01:07:11.936 - configured - ovos_utils.skill_installer:validate_constraints:201 - ERROR - Error accessing remote constraints: timeout -2026-08-01 01:07:11.943 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/missing.txt -2026-08-01 01:07:11.948 - configured - ovos_utils.skill_installer:validate_constraints:195 - ERROR - Remote constraints file not accessible: 404 -2026-08-01 01:07:11.955 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.220 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.226 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.venvs/ovos/bin/python -m pip uninstall -y custom-pkg -2026-08-01 01:07:12.232 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.238 - configured - ovos_utils.skill_installer:pip_uninstall:295 - ERROR - no package list provided to uninstall -2026-08-01 01:07:12.246 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.254 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.490 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.495 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y --break-system-packages custom-pkg -2026-08-01 01:07:12.502 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.508 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.513 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg -2026-08-01 01:07:12.520 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.591 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.596 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg -2026-08-01 01:07:12.604 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.667 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.673 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg -2026-08-01 01:07:12.683 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.764 - configured - ovos_utils.skill_installer:pip_uninstall:330 - ERROR - tried to uninstall a protected package: ['ovos-core'] -2026-08-01 01:07:12.779 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.788 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.795 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg -2026-08-01 01:07:12.803 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.874 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.880 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg -2026-08-01 01:07:12.889 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.896 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing pkg -2026-08-01 01:07:12.903 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg -2026-08-01 01:07:12.912 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.919 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing bad-pkg -2026-08-01 01:07:12.924 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c https://raw.githubusercontent.com/OpenVoiceOS/ovos-releases/refs/heads/main/constraints-stable.txt bad-pkg -2026-08-01 01:07:12.934 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.943 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.950 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg -2026-08-01 01:07:12.957 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://example.com/c.txt my-pkg -2026-08-01 01:07:12.966 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.973 - configured - ovos_utils.skill_installer:pip_install:230 - ERROR - no package list provided to install -2026-08-01 01:07:12.984 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.990 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg -2026-08-01 01:07:12.995 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.venvs/ovos/bin/python -m pip install -c http://example.com/c.txt my-pkg -2026-08-01 01:07:13.005 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:13.024 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:13.036 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.043 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-08-01 01:07:13.049 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg -2026-08-01 01:07:13.056 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:13.065 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:13.074 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.078 - configured - ovos_utils.skill_installer:handle_uninstall_python:427 - ERROR - pip disabled in mycroft.conf -2026-08-01 01:07:13.087 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:13.095 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'my_service' -2026-08-01 01:07:13.102 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.109 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.116 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.122 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-08-01 01:07:13.127 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg -2026-08-01 01:07:13.135 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.140 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-08-01 01:07:13.146 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt --pre pkg -2026-08-01 01:07:13.154 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.159 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-08-01 01:07:13.164 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt --break-system-packages pkg -2026-08-01 01:07:13.171 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.176 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-08-01 01:07:13.181 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg -2026-08-01 01:07:13.194 - configured - ovos_utils.events:add:165 - DEBUG - Added event: id:f -2026-08-01 01:07:13.205 - configured - ovos_utils.events:remove:173 - DEBUG - Removing event id:f -2026-08-01 01:07:13.347 - configured - ovos_utils.gui:get_ui_directories:90 - DEBUG - Skill supports GUI framework: qt5 from folder: /home/miro/tmp/tmpilwf5jno/gui/qt5 -2026-08-01 01:07:13.352 - configured - ovos_utils.gui:get_ui_directories:90 - DEBUG - Skill supports GUI framework: kivy from folder: /home/miro/tmp/tmpilwf5jno/gui/kivy -2026-08-01 01:07:13.360 - configured - ovos_utils.gui:get_ui_directories:85 - DEBUG - legacy UI directory found - Handling `ui` directory as `qt5` -2026-08-01 01:07:13.421 - configured - ovos_utils.parse:_validate_matching_strategy:27 - ERROR - rapidfuzz is not installed, falling back to SequenceMatcher for all match strategies -2026-08-01 01:07:13.426 - configured - ovos_utils.parse:_validate_matching_strategy:29 - WARNING - pip install rapidfuzz +2026-08-01 14:14:22.438 - configured - ovos_utils.dialog:load_dialogs - WARNING - Deprecation version=1.0.0. Caller=test_dialog:191. use 'ovos_spec_tools.LocaleResources' to load .dialog resources +2026-08-01 14:14:22.443 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_dialog:191. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.455 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_dialog:202. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.460 - configured - ovos_utils.dialog:load_dialogs - WARNING - Deprecation version=1.0.0. Caller=test_dialog:203. use 'ovos_spec_tools.LocaleResources' to load .dialog resources +2026-08-01 14:14:22.468 - configured - ovos_utils.dialog:load_dialogs - WARNING - Deprecation version=1.0.0. Caller=test_dialog:184. use 'ovos_spec_tools.LocaleResources' to load .dialog resources +2026-08-01 14:14:22.472 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_dialog:184. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.481 - configured - ovos_utils.dialog:load_dialogs - WARNING - Deprecation version=1.0.0. Caller=test_dialog:213. use 'ovos_spec_tools.LocaleResources' to load .dialog resources +2026-08-01 14:14:22.486 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_dialog:213. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.493 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_dialog:70. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.501 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_dialog:41. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.507 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_dialog:151. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.515 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_dialog:135. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.523 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_dialog:100. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.530 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_dialog:29. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.538 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_dialog:56. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.546 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_dialog:85. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.561 - configured - ovos_utils.dialog:get_dialog - WARNING - Deprecation version=1.0.0. Caller=test_dialog:248. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.574 - configured - ovos_utils.dialog:get_dialog - WARNING - Deprecation version=1.0.0. Caller=test_dialog:224. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.586 - configured - ovos_utils.dialog:get_dialog - WARNING - Deprecation version=1.0.0. Caller=test_dialog:263. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.599 - configured - ovos_utils.dialog:get_dialog - WARNING - Deprecation version=1.0.0. Caller=test_dialog:237. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.604 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_dialog:237. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:22.629 - configured - ovos_utils.messagebus::5 - WARNING - Deprecation version=1.0.0. Caller=test_messagebus:37. ovos_utils.messagebus has been deprecated since version 0.1.0!! please import from ovos_utils.fakebus or ovos_bus_client directly +2026-08-01 14:14:22.654 - configured - ovos_utils.messagebus::5 - WARNING - Deprecation version=1.0.0. Caller=test_messagebus:30. ovos_utils.messagebus has been deprecated since version 0.1.0!! please import from ovos_utils.fakebus or ovos_bus_client directly +2026-08-01 14:14:22.783 - configured - ovos_utils.ocp:available_extractors - WARNING - Deprecation version=0.1.0. Caller=test_ocp:402. import ovos_utils.available_extractors from ovos_plugin_manager.ocp instead +2026-08-01 14:14:23.002 - configured - ovos_utils.events:EventSchedulerInterface.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_events:530. EventSchedulerInterface moved to ovos_bus_client. 'from ovos_bus_client.apis.events import EventSchedulerInterface' +2026-08-01 14:14:26.083 - configured - ovos_utils.events:EventSchedulerInterface.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_events:200. EventSchedulerInterface moved to ovos_bus_client. 'from ovos_bus_client.apis.events import EventSchedulerInterface' +2026-08-01 14:14:26.138 - configured - ovos_utils.lang:get_language_dir - WARNING - Deprecation version=1.0.0. Caller=test_lang:89. use 'closest_lang' from 'ovos_spec_tools' (or 'ovos_spec_tools.LocaleResources') +2026-08-01 14:14:26.144 - configured - ovos_utils.lang:get_language_dir - WARNING - Deprecation version=1.0.0. Caller=test_lang:71. use 'closest_lang' from 'ovos_spec_tools' (or 'ovos_spec_tools.LocaleResources') +2026-08-01 14:14:26.160 - configured - ovos_utils.lang:standardize_lang_tag - WARNING - Deprecation version=1.0.0. Caller=test_lang:59. use 'standardize_lang' from 'ovos_spec_tools' instead +2026-08-01 14:14:26.164 - configured - ovos_utils.lang:standardize_lang_tag - WARNING - Deprecation version=1.0.0. Caller=test_lang:61. use 'standardize_lang' from 'ovos_spec_tools' instead +2026-08-01 14:14:26.171 - configured - ovos_utils.lang:standardize_lang_tag - WARNING - Deprecation version=1.0.0. Caller=test_lang:34. use 'standardize_lang' from 'ovos_spec_tools' instead +2026-08-01 14:14:26.175 - configured - ovos_utils.lang:standardize_lang_tag - WARNING - Deprecation version=1.0.0. Caller=test_lang:35. use 'standardize_lang' from 'ovos_spec_tools' instead +2026-08-01 14:14:26.181 - configured - ovos_utils.lang:standardize_lang_tag - WARNING - Deprecation version=1.0.0. Caller=test_lang:49. use 'standardize_lang' from 'ovos_spec_tools' instead +2026-08-01 14:14:26.186 - configured - ovos_utils.lang:standardize_lang_tag - WARNING - Deprecation version=1.0.0. Caller=test_lang:50. use 'standardize_lang' from 'ovos_spec_tools' instead +2026-08-01 14:14:26.192 - configured - ovos_utils.lang:standardize_lang_tag - WARNING - Deprecation version=1.0.0. Caller=test_lang:42. use 'standardize_lang' from 'ovos_spec_tools' instead +2026-08-01 14:14:26.241 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=dialog.test_dialog:27. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:26.260 - configured - ovos_utils.dialog:load_dialogs - WARNING - Deprecation version=1.0.0. Caller=dialog.test_dialog:100. use 'ovos_spec_tools.LocaleResources' to load .dialog resources +2026-08-01 14:14:26.264 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=dialog.test_dialog:100. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:26.293 - configured - ovos_utils.dialog:load_dialogs - WARNING - Deprecation version=1.0.0. Caller=dialog.test_dialog:94. use 'ovos_spec_tools.LocaleResources' to load .dialog resources +2026-08-01 14:14:26.298 - configured - ovos_utils.dialog:MustacheDialogRenderer.__init__ - WARNING - Deprecation version=1.0.0. Caller=dialog.test_dialog:94. use the OVOS-INTENT-2 §4.2 dialog renderer in 'ovos_spec_tools' ('render' / 'DialogRenderer') +2026-08-01 14:14:26.560 - configured - ovos_utils.sound:_find_player:66 - ERROR - Can't find player for: test.xyz +2026-08-01 14:14:26.575 - configured - ovos_utils.sound:play_audio:107 - ERROR - Failed to play: No playback functionality available +2026-08-01 14:14:26.583 - configured - ovos_utils.sound:play_audio:119 - ERROR - Failed to play: ['broken', 'test.mp3'] +2026-08-01 14:14:26.587 - configured - ovos_utils.sound:play_audio:120 - ERROR - no such file +Traceback (most recent call last): + File "/home/miro/tmp/ovos-utils-compat-drop/ovos_utils/sound.py", line 117, in play_audio + return subprocess.Popen(play_cmd, env=environment) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/miro/.local/share/uv/python/cpython-3.12.13-linux-x86_64-gnu/lib/python3.12/unittest/mock.py", line 1139, in __call__ + return self._mock_call(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/miro/.local/share/uv/python/cpython-3.12.13-linux-x86_64-gnu/lib/python3.12/unittest/mock.py", line 1143, in _mock_call + return self._execute_mock_call(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/miro/.local/share/uv/python/cpython-3.12.13-linux-x86_64-gnu/lib/python3.12/unittest/mock.py", line 1198, in _execute_mock_call + raise effect +OSError: no such file +2026-08-01 14:14:26.598 - configured - ovos_utils.security:create_self_signed_cert - WARNING - Deprecation version=1.0.0. Caller=test_security_extra:68. create_self_signed_cert is unmaintained and generates a 1024-bit RSA / SHA-1 certificate that modern OpenSSL (SECLEVEL=2, the Debian/Ubuntu/Fedora default) refuses to load ('EE_KEY_TOO_SMALL'). Callers should bundle their own self-signed cert generation (RSA >= 2048, SHA-256, with a Subject Alternative Name) instead of relying on this helper. +2026-08-01 14:14:26.604 - configured - ovos_utils.security:create_self_signed_cert - WARNING - Deprecation version=1.0.0. Caller=test_security_extra:49. create_self_signed_cert is unmaintained and generates a 1024-bit RSA / SHA-1 certificate that modern OpenSSL (SECLEVEL=2, the Debian/Ubuntu/Fedora default) refuses to load ('EE_KEY_TOO_SMALL'). Callers should bundle their own self-signed cert generation (RSA >= 2048, SHA-256, with a Subject Alternative Name) instead of relying on this helper. diff --git a/test/unittests/log_test/rotate.log b/test/unittests/log_test/rotate.log deleted file mode 100644 index 85156fa2..00000000 --- a/test/unittests/log_test/rotate.log +++ /dev/null @@ -1 +0,0 @@ -2026-08-01 01:07:13.189 - rotate - ovos_utils.events:EventSchedulerInterface.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_event_scheduler:110. EventSchedulerInterface moved to ovos_bus_client. 'from ovos_bus_client.apis.events import EventSchedulerInterface' diff --git a/test/unittests/log_test/rotate.log.1 b/test/unittests/log_test/rotate.log.1 deleted file mode 100644 index d353a90e..00000000 --- a/test/unittests/log_test/rotate.log.1 +++ /dev/null @@ -1 +0,0 @@ -2026-08-01 01:07:02.718 - rotate - ovos_utils.sound:_find_player:66 - ERROR - Can't find player for: test.xyz diff --git a/test/unittests/test_async_fakebus.py b/test/unittests/test_async_fakebus.py index a3a1f79d..a4babe51 100644 --- a/test/unittests/test_async_fakebus.py +++ b/test/unittests/test_async_fakebus.py @@ -198,56 +198,58 @@ def test_run_in_thread_alias(self): self.assertTrue(bus.started_running) -class TestAsyncFakeBusNamespaceMigration(unittest.TestCase): - """AsyncFakeBus mirrors FakeBus / MessageBusClient namespace migration.""" +class TestAsyncFakeBusNoLegacyBridge(unittest.TestCase): + """AsyncFakeBus mirrors MessageBusClient: the legacy bridge is gone.""" - def test_legacy_emit_reaches_spec_listener(self): - bus = AsyncFakeBus() # both flags default on + def test_legacy_emit_does_not_reach_spec_listener(self): + bus = AsyncFakeBus() got = [] bus.on("ovos.utterance.speak", lambda m: got.append(m.msg_type)) _run(bus.emit(FakeMessage("speak", {"utterance": "hi"}))) - self.assertEqual(got, ["ovos.utterance.speak"]) # modernize bridged it + self.assertEqual(got, []) - def test_spec_emit_reaches_legacy_listener(self): + def test_spec_emit_does_not_reach_legacy_listener(self): bus = AsyncFakeBus() got = [] bus.on("speak", lambda m: got.append(m.msg_type)) _run(bus.emit(FakeMessage("ovos.utterance.speak", {"utterance": "hi"}))) - self.assertEqual(got, ["speak"]) # emit_legacy bridged it + self.assertEqual(got, []) - def test_counterpart_payload_is_translated(self): - # a spec listener on the counterpart of a SHAPE-CHANGING legacy topic - # receives the payload reshaped into ITS shape. detach_intent -> - # ovos.intent.deregister splits "skill:intent" into skill_id/intent_name. + def test_no_payload_reshaping_for_shape_changing_renames(self): + # detach_intent -> ovos.intent.deregister used to be reshaped for a + # listener on the counterpart. There is no counterpart delivery now. bus = AsyncFakeBus() got = [] bus.on("ovos.intent.deregister", lambda m: got.append(dict(m.data))) _run(bus.emit(FakeMessage("detach_intent", {"intent_name": "skill.foo:HelloIntent"}))) - self.assertEqual(got, [{"skill_id": "skill.foo", "intent_name": "HelloIntent"}]) + self.assertEqual(got, []) - def test_dual_listener_fires_once(self): + def test_dual_listener_fires_per_real_emit(self): + # no mirror means no mirror-guard: two subscriptions to two topics are + # two ordinary subscriptions. bus = AsyncFakeBus() calls = [] handler = lambda m: calls.append(m.msg_type) bus.on("speak", handler) bus.on("ovos.utterance.speak", handler) _run(bus.emit(FakeMessage("speak", {"utterance": "hi"}))) - self.assertEqual(len(calls), 1) # mirror deduped + self.assertEqual(calls, ["speak"]) + _run(bus.emit(FakeMessage("ovos.utterance.speak", {"utterance": "hi"}))) + self.assertEqual(calls, ["speak", "ovos.utterance.speak"]) - def test_distinct_listeners_each_fire_once(self): + def test_spec_listener_receives_the_spec_topic(self): bus = AsyncFakeBus() - legacy, spec = [], [] - bus.on("speak", lambda m: legacy.append(1)) + spec = [] bus.on("ovos.utterance.speak", lambda m: spec.append(1)) - _run(bus.emit(FakeMessage("speak", {"utterance": "hi"}))) - self.assertEqual((len(legacy), len(spec)), (1, 1)) + _run(bus.emit(FakeMessage("ovos.utterance.speak", {"utterance": "hi"}))) + self.assertEqual(len(spec), 1) - def test_flags_off_no_bridging(self): - bus = AsyncFakeBus(modernize=False, emit_legacy=False) + def test_a_removed_flag_kwarg_is_ignored_not_fatal(self): + bus = AsyncFakeBus(emit_legacy=True) got = [] - bus.on("ovos.utterance.speak", lambda m: got.append(m.msg_type)) - _run(bus.emit(FakeMessage("speak", {"utterance": "hi"}))) + bus.on("speak", lambda m: got.append(m.msg_type)) + _run(bus.emit(FakeMessage("ovos.utterance.speak", {"utterance": "hi"}))) self.assertEqual(got, []) def test_remove_cleans_up(self): @@ -255,10 +257,7 @@ def test_remove_cleans_up(self): calls = [] handler = lambda m: calls.append(1) bus.on("speak", handler) - bus.on("ovos.utterance.speak", handler) bus.remove("speak", handler) - bus.remove("ovos.utterance.speak", handler) - self.assertNotIn(handler, bus._handler_guards) _run(bus.emit(FakeMessage("speak", {"utterance": "hi"}))) self.assertEqual(calls, []) diff --git a/test/unittests/test_fakebus_intent_legacy_reemit.py b/test/unittests/test_fakebus_intent_legacy_reemit.py deleted file mode 100644 index 75b89f9b..00000000 --- a/test/unittests/test_fakebus_intent_legacy_reemit.py +++ /dev/null @@ -1,239 +0,0 @@ -"""FakeBus mirrors MessageBusClient's legacy intent-topic bridge. - -Old ovos-workshop built the per-intent dispatch topic from the resource -filename, so ``:food.order.intent`` reached the wire. Current -workshop registers the canonical ``:food.order``. When emit_legacy -is on, a bus that has a handler bound to the suffixed spelling also gets the -dispatch mirrored onto that spelling. - -Both fake buses must behave like the real client, otherwise every harness -built on them hides the compat path. -""" -import asyncio -import unittest - -from ovos_spec_tools import Message - -from ovos_utils.fakebus import (INTENT_REEMIT_CONTEXT_KEY, AsyncFakeBus, - FakeBus) - -CANONICAL = "skill-food.jarbas:food.order" -LEGACY = "skill-food.jarbas:food.order.intent" - - -def _run(coro): - return asyncio.run(coro) - - -class TestAliasDrivenReemit(unittest.TestCase): - def test_suffixed_subscription_receives_canonical_dispatch(self): - bus = FakeBus() - got = [] - bus.on(LEGACY, got.append) - bus.emit(Message(CANONICAL, {"utterance": "one pizza"})) - self.assertEqual([m.msg_type for m in got], [LEGACY]) - self.assertEqual(got[0].data, {"utterance": "one pizza"}) - - def test_mirror_keeps_data_and_context(self): - bus = FakeBus() - got = [] - bus.on(LEGACY, got.append) - bus.emit(Message(CANONICAL, {"a": 1}, {"source": ["me"]})) - self.assertEqual(got[0].data, {"a": 1}) - self.assertEqual(got[0].context["source"], ["me"]) - - def test_mirror_is_marked_in_context(self): - bus = FakeBus() - got = [] - bus.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertTrue(got[0].context[INTENT_REEMIT_CONTEXT_KEY]) - - def test_no_mirror_without_a_suffixed_subscription(self): - bus = FakeBus() - got = [] - bus.on(CANONICAL, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual([m.msg_type for m in got], [CANONICAL]) - - def test_once_subscription_also_registers_the_alias(self): - bus = FakeBus() - got = [] - bus.once(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual([m.msg_type for m in got], [LEGACY]) - - def test_non_intent_topics_are_never_mirrored(self): - bus = FakeBus() - got = [] - bus.on("ovos.utterance.handled.intent", got.append) - bus.emit(Message("ovos.utterance.handled")) - self.assertEqual(got, []) - - -class TestExactlyOnce(unittest.TestCase): - def test_one_dispatch_yields_one_mirror(self): - bus = FakeBus() - got = [] - bus.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual(len(got), 1) - - def test_two_suffixed_handlers_each_run_once(self): - bus = FakeBus() - a, b = [], [] - bus.on(LEGACY, a.append) - bus.on(LEGACY, b.append) - bus.emit(Message(CANONICAL)) - self.assertEqual((len(a), len(b)), (1, 1)) - - def test_handler_on_both_spellings_gets_both_topics_once_each(self): - # the intent bridge does not dedupe across spellings - a handler bound - # to both asked for both. Workshop collapses aliases at registration. - bus = FakeBus() - got = [] - bus.on(CANONICAL, got.append) - bus.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual([m.msg_type for m in got], [CANONICAL, LEGACY]) - - -class TestLoopPrevention(unittest.TestCase): - def test_a_legacy_dispatch_is_not_mirrored_again(self): - bus = FakeBus() - got = [] - bus.on(LEGACY, got.append) - bus.on(CANONICAL, got.append) - bus.emit(Message(LEGACY)) - self.assertEqual([m.msg_type for m in got], [LEGACY]) - - def test_a_marked_message_is_not_mirrored(self): - bus = FakeBus() - got = [] - bus.on(LEGACY, got.append) - bus.emit(Message(CANONICAL, {}, {INTENT_REEMIT_CONTEXT_KEY: True})) - self.assertEqual(got, []) - - def test_reemitting_a_mirror_terminates(self): - bus = FakeBus(intent_reemit_blanket=True) - got = [] - bus.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - bus.emit(got[0]) # feed the twin back in - self.assertEqual(len(got), 2) - - -class TestBlanketMode(unittest.TestCase): - def test_blanket_mirrors_without_any_registration(self): - bus = FakeBus(intent_reemit_blanket=True) - got = [] - bus.ee.on(LEGACY, got.append) # subscribe behind the bus's back - bus.emit(Message(CANONICAL)) - self.assertEqual([m.msg_type for m in got], [LEGACY]) - - def test_blanket_off_by_default(self): - bus = FakeBus() - got = [] - bus.ee.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual(got, []) - - def test_blanket_still_skips_non_intent_topics(self): - bus = FakeBus(intent_reemit_blanket=True) - got = [] - bus.ee.on("ovos.utterance.handled.intent", got.append) - bus.emit(Message("ovos.utterance.handled")) - self.assertEqual(got, []) - - -class TestDisabled(unittest.TestCase): - def test_no_mirror_when_emit_legacy_is_off(self): - bus = FakeBus(emit_legacy=False) - got = [] - bus.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual(got, []) - - def test_no_mirror_when_emit_legacy_is_off_even_in_blanket(self): - bus = FakeBus(emit_legacy=False, intent_reemit_blanket=True) - got = [] - bus.ee.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual(got, []) - - def test_no_mirror_without_spec_tools_intent_support(self): - bus = FakeBus() - bus._intent_aliases = None # older spec-tools: helpers not importable - got = [] - bus.ee.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual(got, []) - - -class TestAliasLifecycle(unittest.TestCase): - def test_removing_the_last_suffixed_handler_stops_the_mirror(self): - bus = FakeBus() - got = [] - bus.on(LEGACY, got.append) - bus.remove(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual(got, []) - - def test_remove_all_listeners_stops_the_mirror(self): - bus = FakeBus() - got = [] - bus.on(LEGACY, got.append) - bus.remove_all_listeners(LEGACY) - bus.emit(Message(CANONICAL)) - self.assertEqual(got, []) - - def test_one_removal_of_two_handlers_keeps_the_mirror(self): - bus = FakeBus() - a, b = [], [] - bus.on(LEGACY, a.append) - bus.on(LEGACY, b.append) - bus.remove(LEGACY, a.append) - bus.emit(Message(CANONICAL)) - self.assertEqual(len(b), 1) - - -class TestAsyncFakeBusParity(unittest.TestCase): - def test_suffixed_subscription_receives_canonical_dispatch(self): - bus = AsyncFakeBus() - got = [] - bus.on(LEGACY, got.append) - _run(bus.emit(Message(CANONICAL, {"utterance": "one pizza"}))) - self.assertEqual([m.msg_type for m in got], [LEGACY]) - self.assertEqual(got[0].data, {"utterance": "one pizza"}) - - def test_no_mirror_without_a_suffixed_subscription(self): - bus = AsyncFakeBus() - got = [] - bus.on(CANONICAL, got.append) - _run(bus.emit(Message(CANONICAL))) - self.assertEqual([m.msg_type for m in got], [CANONICAL]) - - def test_legacy_dispatch_is_not_mirrored_again(self): - bus = AsyncFakeBus() - got = [] - bus.on(LEGACY, got.append) - _run(bus.emit(Message(LEGACY))) - self.assertEqual(len(got), 1) - - def test_blanket_mode(self): - bus = AsyncFakeBus(intent_reemit_blanket=True) - got = [] - bus.ee.on(LEGACY, got.append) - _run(bus.emit(Message(CANONICAL))) - self.assertEqual([m.msg_type for m in got], [LEGACY]) - - def test_no_mirror_when_emit_legacy_is_off(self): - bus = AsyncFakeBus(emit_legacy=False) - got = [] - bus.on(LEGACY, got.append) - _run(bus.emit(Message(CANONICAL))) - self.assertEqual(got, []) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/unittests/test_fakebus_namespace_migration.py b/test/unittests/test_fakebus_namespace_migration.py deleted file mode 100644 index 3eb4b79b..00000000 --- a/test/unittests/test_fakebus_namespace_migration.py +++ /dev/null @@ -1,150 +0,0 @@ -"""FakeBus mirrors MessageBusClient's legacy<->ovos.* namespace migration, so -e2e/satellite tests exercise the real cross-namespace behaviour.""" -import asyncio -import unittest -from unittest.mock import patch - -from ovos_utils.fakebus import AsyncFakeBus, FakeBus, Message - - -def _run(coro): - return asyncio.run(coro) - - -class TestFakeBusNamespaceMigration(unittest.TestCase): - def test_legacy_emit_reaches_spec_listener(self): - bus = FakeBus() # both flags default on - got = [] - bus.on("ovos.utterance.speak", lambda m: got.append(m.msg_type)) - bus.emit(Message("speak", {"utterance": "hi"})) - self.assertEqual(got, ["ovos.utterance.speak"]) # modernize bridged it - - def test_spec_emit_reaches_legacy_listener(self): - bus = FakeBus() - got = [] - bus.on("speak", lambda m: got.append(m.msg_type)) - bus.emit(Message("ovos.utterance.speak", {"utterance": "hi"})) - self.assertEqual(got, ["speak"]) # emit_legacy bridged it - - def test_dual_listener_fires_once(self): - bus = FakeBus() - calls = [] - handler = lambda m: calls.append(m.msg_type) - bus.on("speak", handler) - bus.on("ovos.utterance.speak", handler) - bus.emit(Message("speak", {"utterance": "hi"})) - self.assertEqual(len(calls), 1) # mirror deduped - - def test_distinct_listeners_each_fire_once(self): - bus = FakeBus() - legacy, spec = [], [] - bus.on("speak", lambda m: legacy.append(1)) - bus.on("ovos.utterance.speak", lambda m: spec.append(1)) - bus.emit(Message("speak", {"utterance": "hi"})) - self.assertEqual((len(legacy), len(spec)), (1, 1)) - - def test_flags_off_no_bridging(self): - bus = FakeBus(modernize=False, emit_legacy=False) - got = [] - bus.on("ovos.utterance.speak", lambda m: got.append(m.msg_type)) - bus.emit(Message("speak", {"utterance": "hi"})) - self.assertEqual(got, []) # no translation -> spec listener not reached - - def test_unmapped_topic_untouched(self): - bus = FakeBus() - got = [] - bus.on("my.custom.topic", lambda m: got.append(m.msg_type)) - bus.emit(Message("my.custom.topic", {"x": 1})) - self.assertEqual(got, ["my.custom.topic"]) - - def test_shape_changing_payload_reshaped_for_spec_listener(self): - # a spec listener on the counterpart of a SHAPE-CHANGING legacy topic - # receives the payload in ITS shape, not a verbatim legacy copy. - # detach_intent -> ovos.intent.deregister splits the compound - # "skill:intent" name into skill_id + intent_name. - bus = FakeBus() - got = [] - bus.on("ovos.intent.deregister", lambda m: got.append(dict(m.data))) - bus.emit(Message("detach_intent", {"intent_name": "skill.foo:HelloIntent"})) - self.assertEqual(len(got), 1) - self.assertEqual(got[0], {"skill_id": "skill.foo", "intent_name": "HelloIntent"}) - - def test_shape_changing_payload_reshaped_for_legacy_listener(self): - bus = FakeBus() - got = [] - bus.on("detach_intent", lambda m: got.append(dict(m.data))) - bus.emit(Message("ovos.intent.deregister", - {"skill_id": "skill.foo", "intent_name": "HelloIntent"})) - self.assertEqual(len(got), 1) - # rejoined to the legacy compound shape - self.assertEqual(got[0].get("intent_name"), "skill.foo:HelloIntent") - - def test_payload_compatible_rename_delivered_equivalent(self): - bus = FakeBus() - got = [] - bus.on("ovos.utterance.speak", lambda m: got.append(dict(m.data))) - bus.emit(Message("speak", {"utterance": "hi", "lang": "en-us"})) - self.assertEqual(got, [{"utterance": "hi", "lang": "en-us"}]) # identity - - def test_remove_cleans_up(self): - bus = FakeBus() - calls = [] - handler = lambda m: calls.append(1) - bus.on("speak", handler) - bus.on("ovos.utterance.speak", handler) - bus.remove("speak", handler) - bus.remove("ovos.utterance.speak", handler) - self.assertNotIn(handler, bus._handler_guards) - bus.emit(Message("speak", {"utterance": "hi"})) - self.assertEqual(calls, []) - - -class TestFakeBusFlagResolution(unittest.TestCase): - """When the kwarg is omitted, flags resolve via env -> websocket.* config -> - default True, matching MessageBusClient._bus_flag. An explicit kwarg wins.""" - - def _legacy_mirrored(self, bus): - # emit a legacy topic; if emit_legacy bridging is on a spec listener fires - got = [] - bus.on("ovos.utterance.speak", lambda m: got.append(m.msg_type)) - if isinstance(bus, AsyncFakeBus): - _run(bus.emit(Message("speak", {"utterance": "hi"}))) - else: - bus.emit(Message("speak", {"utterance": "hi"})) - return got == ["ovos.utterance.speak"] - - def test_default_true_no_env_mirrors(self): - with patch.dict("os.environ", {}, clear=False): - import os - os.environ.pop("OVOS_BUS_MODERNIZE", None) - os.environ.pop("OVOS_BUS_EMIT_LEGACY", None) - self.assertTrue(self._legacy_mirrored(FakeBus())) - self.assertTrue(self._legacy_mirrored(AsyncFakeBus())) - - def test_env_false_disables_mirror(self): - with patch.dict("os.environ", - {"OVOS_BUS_MODERNIZE": "false", - "OVOS_BUS_EMIT_LEGACY": "false"}): - self.assertFalse(self._legacy_mirrored(FakeBus())) - self.assertFalse(self._legacy_mirrored(AsyncFakeBus())) - - def test_explicit_kwarg_beats_env(self): - # env says off, but an explicit modernize=True kwarg still mirrors - with patch.dict("os.environ", - {"OVOS_BUS_MODERNIZE": "false", - "OVOS_BUS_EMIT_LEGACY": "false"}): - self.assertTrue(self._legacy_mirrored(FakeBus(modernize=True))) - self.assertTrue(self._legacy_mirrored(AsyncFakeBus(modernize=True))) - - def test_explicit_false_kwarg_beats_unset_env(self): - import os - with patch.dict("os.environ", {}, clear=False): - os.environ.pop("OVOS_BUS_MODERNIZE", None) - os.environ.pop("OVOS_BUS_EMIT_LEGACY", None) - # default would mirror; explicit modernize=False suppresses it - self.assertFalse(self._legacy_mirrored(FakeBus(modernize=False))) - self.assertFalse(self._legacy_mirrored(AsyncFakeBus(modernize=False))) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/unittests/test_fakebus_no_legacy_compat.py b/test/unittests/test_fakebus_no_legacy_compat.py new file mode 100644 index 00000000..09ffed36 --- /dev/null +++ b/test/unittests/test_fakebus_no_legacy_compat.py @@ -0,0 +1,143 @@ +"""Guards for the post-compat world: FakeBus carries no legacy wire bridge. + +These are the inverted twins of the old +``test_fakebus_namespace_migration.py`` and +``test_fakebus_intent_legacy_reemit.py``. Those files proved the bridge +worked; this one proves it is absent. + +A test double that kept the bridge would be worse than useless — every +harness built on it would pass against behaviour the real +``MessageBusClient`` no longer has. +""" +import unittest +from unittest.mock import patch + +from ovos_utils import fakebus as fakebus_mod +from ovos_utils.fakebus import FakeBus, FakeMessage +from ovos_spec_tools import MIGRATION_MAP + +LEGACY_SPEAK = "speak" +SPEC_SPEAK = MIGRATION_MAP[LEGACY_SPEAK].value +SKILL_ID = "ovos-skill-fake.openvoiceos" +CANONICAL_INTENT = f"{SKILL_ID}:food.order" +LEGACY_INTENT = f"{CANONICAL_INTENT}.intent" + + +class TestNoNamespaceBridge(unittest.TestCase): + def test_spec_emit_does_not_reach_legacy_listeners(self): + bus = FakeBus() + got = [] + bus.on(LEGACY_SPEAK, lambda m: got.append(m.msg_type)) + bus.emit(FakeMessage(SPEC_SPEAK, {"utterance": "hi"})) + self.assertEqual(got, []) + + def test_legacy_emit_does_not_reach_spec_listeners(self): + bus = FakeBus() + got = [] + bus.on(SPEC_SPEAK, lambda m: got.append(m.msg_type)) + bus.emit(FakeMessage(LEGACY_SPEAK, {"utterance": "hi"})) + self.assertEqual(got, []) + + def test_no_migrated_topic_is_bridged_in_either_direction(self): + """Sweep the whole map rather than trusting one sample pair.""" + for legacy, spec in MIGRATION_MAP.items(): + with self.subTest(topic=legacy): + bus = FakeBus() + seen = [] + bus.on(legacy, lambda m: seen.append("legacy")) + bus.on(spec.value, lambda m: seen.append("spec")) + bus.emit(FakeMessage(spec.value)) + self.assertEqual(seen, ["spec"]) + seen.clear() + bus.emit(FakeMessage(legacy)) + self.assertEqual(seen, ["legacy"]) + + def test_handler_on_both_namespaces_is_no_longer_deduped(self): + bus = FakeBus() + calls = [] + + def handler(message): + calls.append(message.msg_type) + + bus.on(LEGACY_SPEAK, handler) + bus.on(SPEC_SPEAK, handler) + bus.emit(FakeMessage(SPEC_SPEAK)) + bus.emit(FakeMessage(LEGACY_SPEAK)) + self.assertEqual(calls, [SPEC_SPEAK, LEGACY_SPEAK]) + + def test_on_registers_the_handler_itself_not_a_wrapper(self): + bus = FakeBus() + + def handler(message): + pass + + bus.on(SPEC_SPEAK, handler) + self.assertEqual(bus.ee.listeners(SPEC_SPEAK), [handler]) + bus.remove(SPEC_SPEAK, handler) + self.assertEqual(bus.ee.listeners(SPEC_SPEAK), []) + + +class TestNoIntentTopicTwin(unittest.TestCase): + def test_canonical_dispatch_does_not_reach_the_suffixed_twin(self): + bus = FakeBus() + got = [] + bus.on(LEGACY_INTENT, lambda m: got.append(m.msg_type)) + bus.emit(FakeMessage(CANONICAL_INTENT, {"utterance": "order food"})) + self.assertEqual(got, []) + + def test_canonical_listener_still_receives_the_canonical_dispatch(self): + bus = FakeBus() + got = [] + bus.on(CANONICAL_INTENT, lambda m: got.append(m.msg_type)) + bus.emit(FakeMessage(CANONICAL_INTENT, {"utterance": "order food"})) + self.assertEqual(got, [CANONICAL_INTENT]) + + def test_no_reemit_marker_is_stamped_on_the_dispatch(self): + bus = FakeBus() + got = [] + bus.on(CANONICAL_INTENT, lambda m: got.append(m)) + bus.emit(FakeMessage(CANONICAL_INTENT)) + self.assertNotIn("__legacy_intent_reemit__", got[0].context) + + +class TestBridgeSurfaceIsGone(unittest.TestCase): + def test_module_exports_no_bridge_symbols(self): + for name in ("INTENT_REEMIT_CONTEXT_KEY", "IntentAliasRegistry", + "legacy_reemit_targets", "NamespaceTranslator", + "_LegacyIntentBridge", "_resolve_bus_flags"): + self.assertFalse(hasattr(fakebus_mod, name), name) + + def test_instances_carry_no_bridge_state(self): + bus = FakeBus() + for name in ("_translator", "_handler_guards", "_dedup_registrations", + "_intent_aliases", "_intent_reemit_blanket"): + self.assertFalse(hasattr(bus, name), name) + + +class TestRemovedFlagsAreLoud(unittest.TestCase): + def test_kwarg_is_accepted_and_ignored(self): + """A harness kwarg is not an operator decision — it warns, not raises, + so every ecosystem harness that still passes it keeps booting.""" + for key in ("emit_legacy", "modernize", "intent_reemit_blanket"): + with self.subTest(key=key): + bus = FakeBus(**{key: True}) + got = [] + bus.on(LEGACY_SPEAK, lambda m: got.append(m.msg_type)) + bus.emit(FakeMessage(SPEC_SPEAK)) + self.assertEqual(got, []) + + def test_env_flag_raises(self): + for env_var in ("OVOS_BUS_EMIT_LEGACY", "OVOS_BUS_MODERNIZE", + "OVOS_BUS_INTENT_REEMIT_BLANKET"): + with self.subTest(env_var=env_var): + with patch.dict(fakebus_mod.environ, {env_var: "true"}): + with self.assertRaises(RuntimeError): + FakeBus() + + def test_explicitly_disabled_flags_are_accepted(self): + FakeBus(emit_legacy=False, modernize=False, + intent_reemit_blanket=False) + + +if __name__ == "__main__": + unittest.main() From de7421d27bfceb29ebdfa6a6a69cb935b28723bc Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sat, 1 Aug 2026 15:05:30 +0100 Subject: [PATCH 4/8] refactor: replace the intent alias registry with two stateless rules The fake buses bridge the canonical `:` dispatch topic and the legacy `.intent`-suffixed spelling with the same two rules the real `MessageBusClient` now uses. The client splits them over a wire send and a wire receive; a fake bus is one process, so both land in `emit`: * a canonical dispatch also fires its suffixed twin, marked in `context` with `_intent_compat_twin`; * a suffixed dispatch that is not already such a twin also fires its canonical spelling. The two cases are mutually exclusive and neither cascades, so one emit reaches each handler exactly once. The alias registry, the per-bus alias table, the `on()` / `remove()` bookkeeping, and the blanket flag are all gone: which handlers exist is not something the bridge needs to know. Only the two pure helpers are imported from ovos-spec-tools, so the existing >=1.6.0a1 floor stays sufficient. --- ovos_utils/fakebus.py | 123 +++------- test/unittests/log_test/configured.log | 222 ++++++++++-------- test/unittests/log_test/rotate.log | 2 +- test/unittests/log_test/rotate.log.1 | 2 +- .../test_fakebus_intent_legacy_reemit.py | 200 +++++----------- 5 files changed, 222 insertions(+), 327 deletions(-) diff --git a/ovos_utils/fakebus.py b/ovos_utils/fakebus.py index 1d48a50a..345b53f7 100644 --- a/ovos_utils/fakebus.py +++ b/ovos_utils/fakebus.py @@ -64,88 +64,49 @@ def _resolve_bus_flags(kwargs): # wire: a skill with ``food.order.intent`` listened on # ``:food.order.intent``. Current workshop is spec-pure and # registers the canonical ``:food.order`` (OVOS-MSG-1 §2.1.1). -# -# ``ovos_spec_tools.intent_topics`` is the whole compat surface for that gap. -# It is newer than the spec-tools floor declared here, so the import is -# guarded: against an older spec-tools the fake bus behaves as before. -try: - from ovos_spec_tools.intent_topics import (IntentAliasRegistry, - legacy_reemit_targets) - _HAS_INTENT_TOPICS = True -except ImportError: # spec-tools without OVOS-INTENT-4 compat helpers - IntentAliasRegistry = None - legacy_reemit_targets = None - _HAS_INTENT_TOPICS = False - -#: Context flag stamped on a mirrored intent dispatch. A message carrying it is -#: already a twin, so it is never mirrored again. Same key the real -#: ``MessageBusClient`` uses. -INTENT_REEMIT_CONTEXT_KEY = "__legacy_intent_reemit__" +from ovos_spec_tools.intent_topics import (canonical_intent_topic, + is_intent_topic, + legacy_intent_topic) + +#: Context flag stamped on a twin intent frame. Same key and same meaning as +#: in ``ovos_bus_client.client.client``. +INTENT_COMPAT_TWIN_KEY = "_intent_compat_twin" class _LegacyIntentBridge: - """Mirror an intent dispatch onto its legacy ``.intent``-suffixed twin. + """Bridge the canonical and legacy spellings of an intent dispatch topic. Shared by :class:`FakeBus` and :class:`AsyncFakeBus` so both test doubles behave like ``ovos_bus_client.MessageBusClient``, which runs the same bridge next to its namespace bridge. A test double that skipped it would hide the compat path from every harness built on it. - The alias table is owned by the bus instance and filled from its own - ``on()`` / ``once()`` calls: a bus mirrors only the intents one of its own - handlers asked for by the suffixed name, so no topic nobody listens on is - invented. - """ + The real client splits the bridge over a wire send and a wire receive. A + fake bus is one process, so both rules land in ``emit``: - def _init_intent_bridge(self, kwargs): - self._intent_aliases = IntentAliasRegistry() if _HAS_INTENT_TOPICS else None - blanket = kwargs.get("intent_reemit_blanket", _UNSET) - if blanket is _UNSET: - blanket = _bus_flag("OVOS_BUS_INTENT_REEMIT_BLANKET", - "intent_reemit_blanket", default=False) - # blanket mode mirrors EVERY intent dispatch, registered alias or not, - # for pure-bus listeners that subscribe without registering. It doubles - # intent traffic, so it is off unless asked for. - self._intent_reemit_blanket = blanket - - def _record_intent_alias(self, msg_type): - """Note that a handler subscribed to ``msg_type``. - - Only per-intent dispatch topics are recorded; the registry ignores - everything else. A subscription written with the legacy ``.intent`` - suffix is what marks the canonical intent as needing the mirror. - """ - if self._intent_aliases is not None: - self._intent_aliases.register(msg_type) + * a CANONICAL dispatch also fires its ``.intent``-suffixed twin, marked as + a twin, reaching a handler written against old workshop; + * a SUFFIXED dispatch that is NOT already such a twin also fires its + canonical spelling, reaching a spec-pure handler. - def _forget_intent_alias(self, msg_type): - """Drop the alias of ``msg_type`` once nothing listens on it.""" - if self._intent_aliases is None: - return - alias = self._intent_aliases.legacy_alias(msg_type) - if alias is None: - return - if not self.ee.listeners(alias): - self._intent_aliases.deregister(msg_type) - - def _reemit_legacy_intent(self, message): - """Dispatch the suffixed twin of ``message``, if one is called for. + The two cases are mutually exclusive and neither cascades, so one emit + reaches each handler exactly once. Nothing tracks who listens to what. + """ - The twin carries the same data and context plus - :data:`INTENT_REEMIT_CONTEXT_KEY`, and fires at most once: it is - already the suffixed spelling, which ``legacy_reemit_targets`` never - mirrors again. - """ - if self._intent_aliases is None or not self._translator.emit_legacy: + def _bridge_intent_topics(self, message): + """Fire the counterpart spelling of an intent dispatch, if any.""" + if not self._translator.emit_legacy: return - if message.context.get(INTENT_REEMIT_CONTEXT_KEY): + if not is_intent_topic(message.msg_type): return - for topic in legacy_reemit_targets(message.msg_type, - registry=self._intent_aliases, - blanket=self._intent_reemit_blanket): - twin = message.forward(topic, message.data) - twin.context[INTENT_REEMIT_CONTEXT_KEY] = True - self.ee.emit(topic, twin) + canonical = canonical_intent_topic(message.msg_type) + if canonical == message.msg_type: + twin = message.forward(legacy_intent_topic(message.msg_type), + message.data) + twin.context[INTENT_COMPAT_TWIN_KEY] = True + self.ee.emit(twin.msg_type, twin) + elif not message.context.get(INTENT_COMPAT_TWIN_KEY): + self.ee.emit(canonical, message.forward(canonical, message.data)) class FakeBus(_LegacyIntentBridge): @@ -161,8 +122,6 @@ def __init__(self, *args, **kwargs): self._translator = _resolve_bus_flags(kwargs) self._handler_guards = {} # handler -> shared mirror-guard self._dedup_registrations = {} # handler -> [(msg_type, wrapped), ...] - # legacy intent-topic bridge, gated by the SAME emit_legacy flag - self._init_intent_bridge(kwargs) self.on_open() try: self.session_id = kwargs["session"].session_id @@ -173,7 +132,6 @@ def __init__(self, *args, **kwargs): self.on_default_session_update) def on(self, msg_type, handler): - self._record_intent_alias(msg_type) # wrap handlers on migrated topics so a handler subscribed to both the # legacy and ovos.* topic fires once (the mirror is dropped) if self._translator.is_migrated(msg_type): @@ -193,7 +151,6 @@ def wrapped(message=None): self.ee.on(msg_type, handler) def once(self, msg_type, handler): - self._record_intent_alias(msg_type) self.ee.once(msg_type, handler) def emit(self, message): @@ -235,9 +192,9 @@ def emit(self, message): self.ee.emit(topic, message.forward(topic, translated)) except Exception as e: LOG.exception(f"Error in counterpart dispatch for '{topic}': {e}") - # legacy intent-topic bridge: mirror an intent dispatch onto its - # ``.intent``-suffixed twin for handlers written against old workshop. - self._reemit_legacy_intent(message) + # legacy intent-topic bridge: fire the counterpart spelling of an + # intent dispatch, for handlers written against old workshop. + self._bridge_intent_topics(message) def on_message(self, *args): """ @@ -337,17 +294,14 @@ def remove(self, msg_type, handler): if not regs: self._dedup_registrations.pop(handler, None) self._handler_guards.pop(handler, None) - self._forget_intent_alias(msg_type) return try: self.ee.remove_listener(msg_type, handler) except Exception: pass - self._forget_intent_alias(msg_type) def remove_all_listeners(self, event_name): self.ee.remove_all_listeners(event_name) - self._forget_intent_alias(event_name) def create_client(self): return self @@ -486,8 +440,6 @@ def __init__(self, *args, **kwargs): self._translator = _resolve_bus_flags(kwargs) self._handler_guards = {} # handler -> shared mirror-guard self._dedup_registrations = {} # handler -> [(msg_type, wrapped), ...] - # legacy intent-topic bridge, gated by the SAME emit_legacy flag - self._init_intent_bridge(kwargs) self.connected_event = asyncio.Event() self.connected_event.set() self.on_open() @@ -504,7 +456,6 @@ def __init__(self, *args, **kwargs): # ------------------------------------------------------------------ def on(self, msg_type, handler): - self._record_intent_alias(msg_type) # wrap handlers on migrated topics so a handler subscribed to both the # legacy and ovos.* topic fires once (the mirror is dropped) -- same as # FakeBus.on / MessageBusClient.on. @@ -525,7 +476,6 @@ def wrapped(message=None): self.ee.on(msg_type, handler) def once(self, msg_type, handler): - self._record_intent_alias(msg_type) self.ee.once(msg_type, handler) def remove(self, msg_type, handler): @@ -540,17 +490,14 @@ def remove(self, msg_type, handler): if not regs: self._dedup_registrations.pop(handler, None) self._handler_guards.pop(handler, None) - self._forget_intent_alias(msg_type) return try: self.ee.remove_listener(msg_type, handler) except Exception: pass - self._forget_intent_alias(msg_type) def remove_all_listeners(self, event_name): self.ee.remove_all_listeners(event_name) - self._forget_intent_alias(event_name) # ------------------------------------------------------------------ # Lifecycle (async) @@ -601,9 +548,9 @@ async def emit(self, message): self.ee.emit(topic, message.forward(topic, translated)) except Exception as e: LOG.exception(f"Error in counterpart dispatch for '{topic}': {e}") - # legacy intent-topic bridge: mirror an intent dispatch onto its - # ``.intent``-suffixed twin for handlers written against old workshop. - self._reemit_legacy_intent(message) + # legacy intent-topic bridge: fire the counterpart spelling of an + # intent dispatch, for handlers written against old workshop. + self._bridge_intent_topics(message) # ------------------------------------------------------------------ # Sync helpers used internally — same as FakeBus diff --git a/test/unittests/log_test/configured.log b/test/unittests/log_test/configured.log index b6e14f13..c889d3fa 100644 --- a/test/unittests/log_test/configured.log +++ b/test/unittests/log_test/configured.log @@ -1,96 +1,126 @@ -2026-08-01 01:07:01.284 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - systemctl poweroff -i -2026-08-01 01:07:01.296 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - sudo systemctl poweroff -i -2026-08-01 01:07:01.308 - configured - ovos_utils.system:system_reboot:83 - DEBUG - sudo systemctl reboot -i -2026-08-01 01:07:01.423 - configured - ovos_utils.security:decrypt:105 - ERROR - run pip install pycryptodomex -2026-08-01 01:07:01.434 - configured - ovos_utils.security:encrypt:92 - ERROR - run pip install pycryptodomex -2026-08-01 01:07:01.444 - configured - ovos_utils.security:decrypt:115 - ERROR - decryption failed, invalid key? -2026-08-01 01:07:01.463 - configured - ovos_utils.security:create_self_signed_cert:46 - ERROR - run pip install pyopenssl -2026-08-01 01:07:11.840 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:11.847 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:11.855 - configured - ovos_utils.skill_installer:handle_install_python:391 - ERROR - pip disabled in mycroft.conf -2026-08-01 01:07:11.862 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:11.871 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:11.878 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:11.885 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:11.892 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:11.900 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:11.908 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:11.918 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt -2026-08-01 01:07:11.925 - configured - ovos_utils.skill_installer:validate_constraints:205 - ERROR - Couldn't find the constraints file -2026-08-01 01:07:11.931 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt -2026-08-01 01:07:11.936 - configured - ovos_utils.skill_installer:validate_constraints:201 - ERROR - Error accessing remote constraints: timeout -2026-08-01 01:07:11.943 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/missing.txt -2026-08-01 01:07:11.948 - configured - ovos_utils.skill_installer:validate_constraints:195 - ERROR - Remote constraints file not accessible: 404 -2026-08-01 01:07:11.955 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.220 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.226 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.venvs/ovos/bin/python -m pip uninstall -y custom-pkg -2026-08-01 01:07:12.232 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.238 - configured - ovos_utils.skill_installer:pip_uninstall:295 - ERROR - no package list provided to uninstall -2026-08-01 01:07:12.246 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.254 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.490 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.495 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y --break-system-packages custom-pkg -2026-08-01 01:07:12.502 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.508 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.513 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg -2026-08-01 01:07:12.520 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.591 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.596 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg -2026-08-01 01:07:12.604 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.667 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.673 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg -2026-08-01 01:07:12.683 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.764 - configured - ovos_utils.skill_installer:pip_uninstall:330 - ERROR - tried to uninstall a protected package: ['ovos-core'] -2026-08-01 01:07:12.779 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.788 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.795 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg -2026-08-01 01:07:12.803 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:12.874 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-08-01 01:07:12.880 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg -2026-08-01 01:07:12.889 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.896 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing pkg -2026-08-01 01:07:12.903 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg -2026-08-01 01:07:12.912 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.919 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing bad-pkg -2026-08-01 01:07:12.924 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c https://raw.githubusercontent.com/OpenVoiceOS/ovos-releases/refs/heads/main/constraints-stable.txt bad-pkg -2026-08-01 01:07:12.934 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.943 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.950 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg -2026-08-01 01:07:12.957 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://example.com/c.txt my-pkg -2026-08-01 01:07:12.966 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.973 - configured - ovos_utils.skill_installer:pip_install:230 - ERROR - no package list provided to install -2026-08-01 01:07:12.984 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:12.990 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg -2026-08-01 01:07:12.995 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.venvs/ovos/bin/python -m pip install -c http://example.com/c.txt my-pkg -2026-08-01 01:07:13.005 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:13.024 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:13.036 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.043 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-08-01 01:07:13.049 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg -2026-08-01 01:07:13.056 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:13.065 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:13.074 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.078 - configured - ovos_utils.skill_installer:handle_uninstall_python:427 - ERROR - pip disabled in mycroft.conf -2026-08-01 01:07:13.087 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-08-01 01:07:13.095 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'my_service' -2026-08-01 01:07:13.102 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.109 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.116 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.122 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-08-01 01:07:13.127 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg -2026-08-01 01:07:13.135 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.140 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-08-01 01:07:13.146 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt --pre pkg -2026-08-01 01:07:13.154 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.159 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-08-01 01:07:13.164 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt --break-system-packages pkg -2026-08-01 01:07:13.171 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-08-01 01:07:13.176 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-08-01 01:07:13.181 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg -2026-08-01 01:07:13.194 - configured - ovos_utils.events:add:165 - DEBUG - Added event: id:f -2026-08-01 01:07:13.205 - configured - ovos_utils.events:remove:173 - DEBUG - Removing event id:f -2026-08-01 01:07:13.347 - configured - ovos_utils.gui:get_ui_directories:90 - DEBUG - Skill supports GUI framework: qt5 from folder: /home/miro/tmp/tmpilwf5jno/gui/qt5 -2026-08-01 01:07:13.352 - configured - ovos_utils.gui:get_ui_directories:90 - DEBUG - Skill supports GUI framework: kivy from folder: /home/miro/tmp/tmpilwf5jno/gui/kivy -2026-08-01 01:07:13.360 - configured - ovos_utils.gui:get_ui_directories:85 - DEBUG - legacy UI directory found - Handling `ui` directory as `qt5` -2026-08-01 01:07:13.421 - configured - ovos_utils.parse:_validate_matching_strategy:27 - ERROR - rapidfuzz is not installed, falling back to SequenceMatcher for all match strategies -2026-08-01 01:07:13.426 - configured - ovos_utils.parse:_validate_matching_strategy:29 - WARNING - pip install rapidfuzz +2026-03-11 04:12:11.787 - configured - ovos_utils.network_utils:get_external_ip:78 - ERROR - Got resp=503: +2026-03-11 04:12:11.789 - configured - ovos_utils.network_utils:get_external_ip:80 - ERROR - Unable to get external IP Address: network error +2026-03-11 04:12:11.796 - configured - ovos_utils.network_utils:check_captive_portal:162 - ERROR - Error checking for captive portal +Traceback (most recent call last): + File "/home/miro/PycharmProjects/OpenVoiceOS Workspace/ovos-utils/ovos_utils/network_utils.py", line 156, in check_captive_portal + html_doc = requests.get(host).text + ~~~~~~~~~~~~^^^^^^ + File "/home/miro/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/unittest/mock.py", line 1169, in __call__ + return self._mock_call(*args, **kwargs) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^ + File "/home/miro/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/unittest/mock.py", line 1173, in _mock_call + return self._execute_mock_call(*args, **kwargs) + ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^ + File "/home/miro/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/unittest/mock.py", line 1228, in _execute_mock_call + raise effect +Exception: timeout +2026-03-11 04:12:11.823 - configured - ovos_utils.ocp:from_dict:254 - ERROR - track dictionary does not contain 'uri', it is not a valid MediaEntry +2026-03-11 04:12:11.824 - configured - ovos_utils.ocp:from_dict:256 - WARNING - DEPRECATED: use dict2entry() for Playlists and PluginStreams, MediaEntry.from_dict is only for regular media, will start throwing ValueError in 0.1.0 +2026-03-11 04:12:11.837 - configured - ovos_utils.ocp:available_extractors:165 - ERROR - please install/update ovos_plugin_manager +2026-03-11 04:12:11.841 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 1 +2026-03-11 04:12:11.842 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 0 +2026-03-11 04:12:11.842 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 0 +2026-03-11 04:12:11.843 - configured - ovos_utils.ocp:goto_track:588 - ERROR - requested track not in the playlist: MediaEntry(uri='http://missing.com/x.mp3', title='', artist='', match_confidence=0, skill_id='ovos.common_play', playback=, status=, media_type=, length=0, image='', skill_icon='', javascript='') +2026-03-11 04:12:11.852 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 2 +2026-03-11 04:12:11.853 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 1 +2026-03-11 04:12:11.854 - configured - ovos_utils.ocp:goto_track:588 - ERROR - requested track not in the playlist: MediaEntry(uri='nonexistent', title='nonexistent', artist='', match_confidence=0, skill_id='ovos.common_play', playback=, status=, media_type=, length=0, image='', skill_icon='', javascript='') +2026-03-11 04:12:11.857 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (3! Going to start of playlist +2026-03-11 04:12:11.858 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (-1! Going to start of playlist +2026-03-11 04:12:11.859 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (-1! Going to start of playlist +2026-03-11 04:12:11.860 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (100! Going to start of playlist +2026-03-11 04:12:11.877 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (0! Going to start of playlist +2026-03-11 04:12:11.878 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (-1! Going to start of playlist +2026-03-11 04:12:11.878 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (1! Going to start of playlist +2026-03-11 04:12:11.879 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (1! Going to start of playlist +2026-03-11 04:12:11.880 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (10! Going to start of playlist +2026-03-11 04:12:11.882 - configured - ovos_utils.parse:_validate_matching_strategy:27 - ERROR - rapidfuzz is not installed, falling back to SequenceMatcher for all match strategies +2026-03-11 04:12:11.882 - configured - ovos_utils.parse:_validate_matching_strategy:29 - WARNING - pip install rapidfuzz +2026-03-11 04:12:11.904 - configured - ovos_utils.security:decrypt:93 - ERROR - run pip install pycryptodomex +2026-03-11 04:12:11.906 - configured - ovos_utils.security:decrypt:103 - ERROR - decryption failed, invalid key? +2026-03-11 04:12:11.910 - configured - ovos_utils.security:encrypt:80 - ERROR - run pip install pycryptodomex +2026-03-11 04:12:11.912 - configured - ovos_utils.security:create_self_signed_cert:34 - ERROR - run pip install pyopenssl +2026-03-11 04:12:11.917 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:11.918 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'my_service' +2026-03-11 04:12:11.918 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:11.919 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.920 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.921 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:11.923 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:11.924 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:11.924 - configured - ovos_utils.skill_installer:handle_install_python:391 - ERROR - pip disabled in mycroft.conf +2026-03-11 04:12:11.925 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.926 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.927 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.928 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.929 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.931 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.932 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.932 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:11.933 - configured - ovos_utils.skill_installer:handle_uninstall_python:427 - ERROR - pip disabled in mycroft.conf +2026-03-11 04:12:11.934 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.934 - configured - ovos_utils.skill_installer:pip_install:230 - ERROR - no package list provided to install +2026-03-11 04:12:11.935 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.937 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.938 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg +2026-03-11 04:12:11.938 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://example.com/c.txt my-pkg +2026-03-11 04:12:11.939 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.940 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg +2026-03-11 04:12:11.940 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/PycharmProjects/OpenVoiceOS Workspace/ovos-utils/.venv/bin/python3 -m pip install -c http://example.com/c.txt my-pkg +2026-03-11 04:12:11.941 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.942 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing bad-pkg +2026-03-11 04:12:11.943 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c https://raw.githubusercontent.com/OpenVoiceOS/ovos-releases/refs/heads/main/constraints-stable.txt bad-pkg +2026-03-11 04:12:11.944 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.945 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing pkg +2026-03-11 04:12:11.945 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt pkg +2026-03-11 04:12:11.947 - configured - ovos_utils.skill_installer:validate_constraints:205 - ERROR - Couldn't find the constraints file +2026-03-11 04:12:11.948 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt +2026-03-11 04:12:11.949 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/missing.txt +2026-03-11 04:12:11.949 - configured - ovos_utils.skill_installer:validate_constraints:195 - ERROR - Remote constraints file not accessible: 404 +2026-03-11 04:12:11.950 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt +2026-03-11 04:12:11.951 - configured - ovos_utils.skill_installer:validate_constraints:201 - ERROR - Error accessing remote constraints: timeout +2026-03-11 04:12:11.952 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.953 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.954 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:11.954 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-03-11 04:12:11.955 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt pkg +2026-03-11 04:12:11.956 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:11.956 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-03-11 04:12:11.957 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt --break-system-packages pkg +2026-03-11 04:12:11.958 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:11.959 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-03-11 04:12:11.959 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt --pre pkg +2026-03-11 04:12:11.960 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:11.961 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-03-11 04:12:11.961 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt pkg +2026-03-11 04:12:11.962 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:11.963 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-03-11 04:12:11.963 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt pkg +2026-03-11 04:12:11.964 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.965 - configured - ovos_utils.skill_installer:pip_uninstall:295 - ERROR - no package list provided to uninstall +2026-03-11 04:12:11.966 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:11.967 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-03-11 04:12:12.298 - configured - ovos_utils.skill_installer:pip_uninstall:330 - ERROR - tried to uninstall a protected package: ['ovos-core'] +2026-03-11 04:12:12.299 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:12.461 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-03-11 04:12:12.462 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg +2026-03-11 04:12:12.464 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:12.925 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-03-11 04:12:12.926 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/PycharmProjects/OpenVoiceOS Workspace/ovos-utils/.venv/bin/python3 -m pip uninstall -y custom-pkg +2026-03-11 04:12:12.929 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:13.262 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-03-11 04:12:13.263 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y custom-pkg +2026-03-11 04:12:13.267 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:13.269 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-03-11 04:12:13.294 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y custom-pkg +2026-03-11 04:12:13.296 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:13.296 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-03-11 04:12:13.297 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y custom-pkg +2026-03-11 04:12:13.298 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:13.569 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-03-11 04:12:13.570 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y --break-system-packages custom-pkg +2026-03-11 04:12:13.571 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-03-11 04:12:13.859 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-03-11 04:12:13.859 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y custom-pkg +2026-03-11 04:12:13.913 - configured - ovos_utils.system:system_reboot:83 - DEBUG - sudo systemctl reboot -i +2026-03-11 04:12:13.915 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - sudo systemctl poweroff -i +2026-03-11 04:12:13.916 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - systemctl poweroff -i diff --git a/test/unittests/log_test/rotate.log b/test/unittests/log_test/rotate.log index 85156fa2..dd7d75aa 100644 --- a/test/unittests/log_test/rotate.log +++ b/test/unittests/log_test/rotate.log @@ -1 +1 @@ -2026-08-01 01:07:13.189 - rotate - ovos_utils.events:EventSchedulerInterface.__init__ - WARNING - Deprecation version=1.0.0. Caller=test_event_scheduler:110. EventSchedulerInterface moved to ovos_bus_client. 'from ovos_bus_client.apis.events import EventSchedulerInterface' +2026-03-11 04:12:13.924 - rotate - ovos_utils.system:ssh_enable - WARNING - Deprecation version=0.2.0. Caller=test_system:165. DEPRECATED: use ovos-PHAL-plugin-system diff --git a/test/unittests/log_test/rotate.log.1 b/test/unittests/log_test/rotate.log.1 index d353a90e..8f4286ae 100644 --- a/test/unittests/log_test/rotate.log.1 +++ b/test/unittests/log_test/rotate.log.1 @@ -1 +1 @@ -2026-08-01 01:07:02.718 - rotate - ovos_utils.sound:_find_player:66 - ERROR - Can't find player for: test.xyz +2026-03-11 04:12:13.923 - rotate - ovos_utils.system:ssh_disable - WARNING - Deprecation version=0.2.0. Caller=test_system:175. DEPRECATED: use ovos-PHAL-plugin-system diff --git a/test/unittests/test_fakebus_intent_legacy_reemit.py b/test/unittests/test_fakebus_intent_legacy_reemit.py index 75b89f9b..0385f10f 100644 --- a/test/unittests/test_fakebus_intent_legacy_reemit.py +++ b/test/unittests/test_fakebus_intent_legacy_reemit.py @@ -2,9 +2,13 @@ Old ovos-workshop built the per-intent dispatch topic from the resource filename, so ``:food.order.intent`` reached the wire. Current -workshop registers the canonical ``:food.order``. When emit_legacy -is on, a bus that has a handler bound to the suffixed spelling also gets the -dispatch mirrored onto that spelling. +workshop registers the canonical ``:food.order``. The bridge is two +stateless rules. The real client splits them over a wire send and a wire +receive; a fake bus is one process, so both land in ``emit``: + +* a CANONICAL dispatch also fires its suffixed twin, marked as a twin; +* a SUFFIXED dispatch that is not already such a twin also fires its canonical + spelling. Both fake buses must behave like the real client, otherwise every harness built on them hides the compat path. @@ -14,8 +18,7 @@ from ovos_spec_tools import Message -from ovos_utils.fakebus import (INTENT_REEMIT_CONTEXT_KEY, AsyncFakeBus, - FakeBus) +from ovos_utils.fakebus import (INTENT_COMPAT_TWIN_KEY, AsyncFakeBus, FakeBus) CANONICAL = "skill-food.jarbas:food.order" LEGACY = "skill-food.jarbas:food.order.intent" @@ -25,8 +28,10 @@ def _run(coro): return asyncio.run(coro) -class TestAliasDrivenReemit(unittest.TestCase): - def test_suffixed_subscription_receives_canonical_dispatch(self): +class TestCanonicalDispatch(unittest.TestCase): + """Rule 1: a canonical dispatch also fires the marked suffixed twin.""" + + def test_suffixed_handler_receives_the_twin(self): bus = FakeBus() got = [] bus.on(LEGACY, got.append) @@ -34,200 +39,113 @@ def test_suffixed_subscription_receives_canonical_dispatch(self): self.assertEqual([m.msg_type for m in got], [LEGACY]) self.assertEqual(got[0].data, {"utterance": "one pizza"}) - def test_mirror_keeps_data_and_context(self): + def test_twin_is_marked_and_keeps_context(self): bus = FakeBus() got = [] bus.on(LEGACY, got.append) bus.emit(Message(CANONICAL, {"a": 1}, {"source": ["me"]})) - self.assertEqual(got[0].data, {"a": 1}) self.assertEqual(got[0].context["source"], ["me"]) + self.assertTrue(got[0].context[INTENT_COMPAT_TWIN_KEY]) - def test_mirror_is_marked_in_context(self): + def test_canonical_handler_fires_exactly_once(self): bus = FakeBus() got = [] - bus.on(LEGACY, got.append) + bus.on(CANONICAL, got.append) + bus.on(LEGACY, lambda m: None) bus.emit(Message(CANONICAL)) - self.assertTrue(got[0].context[INTENT_REEMIT_CONTEXT_KEY]) + self.assertEqual(len(got), 1) - def test_no_mirror_without_a_suffixed_subscription(self): + def test_a_handler_on_both_spellings_hears_both_frames_once_each(self): bus = FakeBus() got = [] bus.on(CANONICAL, got.append) + bus.on(LEGACY, got.append) bus.emit(Message(CANONICAL)) - self.assertEqual([m.msg_type for m in got], [CANONICAL]) + self.assertEqual([m.msg_type for m in got], [CANONICAL, LEGACY]) - def test_once_subscription_also_registers_the_alias(self): - bus = FakeBus() + def test_no_twin_when_compat_is_disabled(self): + bus = FakeBus(emit_legacy=False) got = [] - bus.once(LEGACY, got.append) + bus.on(LEGACY, got.append) bus.emit(Message(CANONICAL)) - self.assertEqual([m.msg_type for m in got], [LEGACY]) - - def test_non_intent_topics_are_never_mirrored(self): - bus = FakeBus() - got = [] - bus.on("ovos.utterance.handled.intent", got.append) - bus.emit(Message("ovos.utterance.handled")) self.assertEqual(got, []) -class TestExactlyOnce(unittest.TestCase): - def test_one_dispatch_yields_one_mirror(self): - bus = FakeBus() - got = [] - bus.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual(len(got), 1) - - def test_two_suffixed_handlers_each_run_once(self): - bus = FakeBus() - a, b = [], [] - bus.on(LEGACY, a.append) - bus.on(LEGACY, b.append) - bus.emit(Message(CANONICAL)) - self.assertEqual((len(a), len(b)), (1, 1)) +class TestSuffixedDispatch(unittest.TestCase): + """Rule 2: an unmarked suffixed dispatch also fires the canonical form.""" - def test_handler_on_both_spellings_gets_both_topics_once_each(self): - # the intent bridge does not dedupe across spellings - a handler bound - # to both asked for both. Workshop collapses aliases at registration. + def test_canonical_handler_hears_an_old_style_dispatch(self): bus = FakeBus() got = [] bus.on(CANONICAL, got.append) - bus.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual([m.msg_type for m in got], [CANONICAL, LEGACY]) - + bus.emit(Message(LEGACY, {"utterance": "one pizza"})) + self.assertEqual([m.msg_type for m in got], [CANONICAL]) + self.assertEqual(got[0].data, {"utterance": "one pizza"}) -class TestLoopPrevention(unittest.TestCase): - def test_a_legacy_dispatch_is_not_mirrored_again(self): + def test_suffixed_handler_still_gets_the_original(self): bus = FakeBus() got = [] bus.on(LEGACY, got.append) - bus.on(CANONICAL, got.append) bus.emit(Message(LEGACY)) - self.assertEqual([m.msg_type for m in got], [LEGACY]) + self.assertEqual(len(got), 1) - def test_a_marked_message_is_not_mirrored(self): + def test_a_marked_twin_is_not_modernized_again(self): bus = FakeBus() got = [] - bus.on(LEGACY, got.append) - bus.emit(Message(CANONICAL, {}, {INTENT_REEMIT_CONTEXT_KEY: True})) + bus.on(CANONICAL, got.append) + bus.emit(Message(LEGACY, {}, {INTENT_COMPAT_TWIN_KEY: True})) self.assertEqual(got, []) - def test_reemitting_a_mirror_terminates(self): - bus = FakeBus(intent_reemit_blanket=True) - got = [] - bus.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - bus.emit(got[0]) # feed the twin back in - self.assertEqual(len(got), 2) - - -class TestBlanketMode(unittest.TestCase): - def test_blanket_mirrors_without_any_registration(self): - bus = FakeBus(intent_reemit_blanket=True) - got = [] - bus.ee.on(LEGACY, got.append) # subscribe behind the bus's back - bus.emit(Message(CANONICAL)) - self.assertEqual([m.msg_type for m in got], [LEGACY]) - - def test_blanket_off_by_default(self): + def test_the_bridge_does_not_cascade(self): bus = FakeBus() got = [] - bus.ee.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual(got, []) - - def test_blanket_still_skips_non_intent_topics(self): - bus = FakeBus(intent_reemit_blanket=True) - got = [] - bus.ee.on("ovos.utterance.handled.intent", got.append) - bus.emit(Message("ovos.utterance.handled")) - self.assertEqual(got, []) - - -class TestDisabled(unittest.TestCase): - def test_no_mirror_when_emit_legacy_is_off(self): - bus = FakeBus(emit_legacy=False) - got = [] bus.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual(got, []) - - def test_no_mirror_when_emit_legacy_is_off_even_in_blanket(self): - bus = FakeBus(emit_legacy=False, intent_reemit_blanket=True) - got = [] - bus.ee.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual(got, []) + bus.emit(Message(LEGACY)) + self.assertEqual(len(got), 1) # not re-twinned off its own canonical - def test_no_mirror_without_spec_tools_intent_support(self): - bus = FakeBus() - bus._intent_aliases = None # older spec-tools: helpers not importable + def test_no_modernization_when_compat_is_disabled(self): + bus = FakeBus(emit_legacy=False) got = [] - bus.ee.on(LEGACY, got.append) - bus.emit(Message(CANONICAL)) + bus.on(CANONICAL, got.append) + bus.emit(Message(LEGACY)) self.assertEqual(got, []) -class TestAliasLifecycle(unittest.TestCase): - def test_removing_the_last_suffixed_handler_stops_the_mirror(self): +class TestNonIntentTopics(unittest.TestCase): + def test_dotted_topics_are_untouched(self): bus = FakeBus() got = [] - bus.on(LEGACY, got.append) - bus.remove(LEGACY, got.append) - bus.emit(Message(CANONICAL)) - self.assertEqual(got, []) + bus.on("ovos.utterance.handled", got.append) + bus.emit(Message("ovos.utterance.handled")) + self.assertEqual(len(got), 1) - def test_remove_all_listeners_stops_the_mirror(self): + def test_nothing_extra_is_dispatched(self): bus = FakeBus() got = [] - bus.on(LEGACY, got.append) - bus.remove_all_listeners(LEGACY) - bus.emit(Message(CANONICAL)) - self.assertEqual(got, []) + bus.on("message", got.append) + bus.emit(Message("ovos.utterance.handled")) + self.assertEqual(len(got), 1) - def test_one_removal_of_two_handlers_keeps_the_mirror(self): - bus = FakeBus() - a, b = [], [] - bus.on(LEGACY, a.append) - bus.on(LEGACY, b.append) - bus.remove(LEGACY, a.append) - bus.emit(Message(CANONICAL)) - self.assertEqual(len(b), 1) +class TestAsyncFakeBus(unittest.TestCase): + """The async double runs the same two rules.""" -class TestAsyncFakeBusParity(unittest.TestCase): - def test_suffixed_subscription_receives_canonical_dispatch(self): + def test_canonical_dispatch_fires_the_twin(self): bus = AsyncFakeBus() got = [] bus.on(LEGACY, got.append) - _run(bus.emit(Message(CANONICAL, {"utterance": "one pizza"}))) + _run(bus.emit(Message(CANONICAL))) self.assertEqual([m.msg_type for m in got], [LEGACY]) - self.assertEqual(got[0].data, {"utterance": "one pizza"}) + self.assertTrue(got[0].context[INTENT_COMPAT_TWIN_KEY]) - def test_no_mirror_without_a_suffixed_subscription(self): + def test_suffixed_dispatch_fires_the_canonical_form(self): bus = AsyncFakeBus() got = [] bus.on(CANONICAL, got.append) - _run(bus.emit(Message(CANONICAL))) - self.assertEqual([m.msg_type for m in got], [CANONICAL]) - - def test_legacy_dispatch_is_not_mirrored_again(self): - bus = AsyncFakeBus() - got = [] - bus.on(LEGACY, got.append) _run(bus.emit(Message(LEGACY))) - self.assertEqual(len(got), 1) - - def test_blanket_mode(self): - bus = AsyncFakeBus(intent_reemit_blanket=True) - got = [] - bus.ee.on(LEGACY, got.append) - _run(bus.emit(Message(CANONICAL))) - self.assertEqual([m.msg_type for m in got], [LEGACY]) + self.assertEqual([m.msg_type for m in got], [CANONICAL]) - def test_no_mirror_when_emit_legacy_is_off(self): + def test_no_bridge_when_compat_is_disabled(self): bus = AsyncFakeBus(emit_legacy=False) got = [] bus.on(LEGACY, got.append) From 4cb3bbf6b0550ad18245471d22984a38efc0973f Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sat, 1 Aug 2026 17:14:28 +0100 Subject: [PATCH 5/8] build: bump ovos-spec-tools floor to >=1.6.0a2 spec-tools#92 (registry removal) merged and released as 1.6.0a2; pin to the simplified intent_topics surface. Co-Authored-By: Claude Fable 5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 02714e0b..1071640b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "rich-click~=1.7", "rich~=13.7", "python-dateutil", - "ovos-spec-tools>=1.6.0a1", # intent_topics helpers for the FakeBus legacy re-emit + "ovos-spec-tools>=1.6.0a2", # intent_topics helpers for the FakeBus legacy re-emit ] [project.urls] From ebe3196a386a10e413d6aeb7bcc84dd9f8679821 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sat, 1 Aug 2026 17:48:47 +0100 Subject: [PATCH 6/8] ci: retrigger after 1.6.0a2 release settles Co-Authored-By: Claude Fable 5 From 82e5d849f2775500406bee7215cbbc12bfa33442 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sat, 1 Aug 2026 19:08:21 +0100 Subject: [PATCH 7/8] fix: deliver the intent twin unmarked so the marker cannot leak Message.forward()/reply() deep-copy the whole context, so the twin marker in message.context rode onto every descendant frame. A handler that forwarded a received twin's context to emit an unrelated suffixed intent branded that frame a twin, and _bridge_intent_topics skipped its canonical spelling -- silent loss for the old-emitter -> new-core population the rule serves. emit now pops the marker into a local decision before local dispatch, so descendants start clean, and _bridge_intent_topics trusts that decision instead of reading context. RULE 1's twin still carries the marker on the wire (its serialized form goes on the "message" firehose), while local handlers receive an unmarked copy -- mirroring the real client, whose local delivery happens on the receive side after the pop. Regression tests build the follow-up via forward()/reply() off a received twin and assert the unrelated canonical topic is still modernized; wire-survival keeps the second-receiver skip property. Co-Authored-By: Claude Fable 5 --- ovos_utils/fakebus.py | 44 +++- test/unittests/log_test/configured.log | 240 +++++++++--------- test/unittests/log_test/rotate.log | 2 +- test/unittests/log_test/rotate.log.1 | 2 +- .../test_fakebus_intent_legacy_reemit.py | 73 +++++- 5 files changed, 227 insertions(+), 134 deletions(-) diff --git a/ovos_utils/fakebus.py b/ovos_utils/fakebus.py index 345b53f7..153cd88d 100644 --- a/ovos_utils/fakebus.py +++ b/ovos_utils/fakebus.py @@ -93,19 +93,35 @@ class _LegacyIntentBridge: reaches each handler exactly once. Nothing tracks who listens to what. """ - def _bridge_intent_topics(self, message): - """Fire the counterpart spelling of an intent dispatch, if any.""" + def _bridge_intent_topics(self, message, is_twin=False): + """Fire the counterpart spelling of an intent dispatch, if any. + + ``is_twin`` is the twin-marker decision made by the caller, which pops + :data:`INTENT_COMPAT_TWIN_KEY` off the message context BEFORE local + dispatch so it cannot ride onto descendants. The marker is therefore + never read from ``context`` here — only ``is_twin`` is trusted. + ``Message.forward()``/``reply()`` deep-copy the whole context, so a + marked frame that stayed marked would brand every follow-up message a + twin and silently suppress its modernization. + """ if not self._translator.emit_legacy: return if not is_intent_topic(message.msg_type): return canonical = canonical_intent_topic(message.msg_type) if canonical == message.msg_type: - twin = message.forward(legacy_intent_topic(message.msg_type), - message.data) - twin.context[INTENT_COMPAT_TWIN_KEY] = True - self.ee.emit(twin.msg_type, twin) - elif not message.context.get(INTENT_COMPAT_TWIN_KEY): + # RULE 1. The twin is an outbound wire frame: it carries the marker + # on the wire (a receiver in another process needs it to skip + # re-modernizing), so its serialized form goes on the "message" + # firehose marked. What reaches LOCAL handlers is an UNMARKED copy, + # mirroring the real client, whose local delivery happens on the + # receive side after the marker is popped — descendants stay clean. + legacy = legacy_intent_topic(message.msg_type) + wire_twin = message.forward(legacy, message.data) + wire_twin.context[INTENT_COMPAT_TWIN_KEY] = True + self.ee.emit("message", wire_twin.serialize()) + self.ee.emit(legacy, message.forward(legacy, message.data)) + elif not is_twin: self.ee.emit(canonical, message.forward(canonical, message.data)) @@ -174,6 +190,12 @@ def emit(self, message): # run. self.on_message(message.serialize()) self.ee.emit("message", message.serialize()) + # RULE 2 dedup marker: read it, then POP it before any local dispatch. + # The "message" firehose above still carries the marked frame (wire + # survival), but local topic handlers and their forward()/reply() + # descendants must start clean, or an unrelated suffixed intent emitted + # from a handler would inherit the marker and never be modernized. + is_intent_twin = message.context.pop(INTENT_COMPAT_TWIN_KEY, False) try: self.ee.emit(message.msg_type, message) except Exception as e: @@ -194,7 +216,7 @@ def emit(self, message): LOG.exception(f"Error in counterpart dispatch for '{topic}': {e}") # legacy intent-topic bridge: fire the counterpart spelling of an # intent dispatch, for handlers written against old workshop. - self._bridge_intent_topics(message) + self._bridge_intent_topics(message, is_intent_twin) def on_message(self, *args): """ @@ -534,6 +556,10 @@ async def emit(self, message): # session mutations with the stale emit-time snapshot). self.on_message(message.serialize()) self.ee.emit("message", message.serialize()) + # RULE 2 dedup marker: pop it before local dispatch — see FakeBus.emit. + # The firehose above keeps the marked frame (wire survival); local + # handlers and their forward()/reply() descendants start clean. + is_intent_twin = message.context.pop(INTENT_COMPAT_TWIN_KEY, False) try: self.ee.emit(message.msg_type, message) except Exception as e: @@ -550,7 +576,7 @@ async def emit(self, message): LOG.exception(f"Error in counterpart dispatch for '{topic}': {e}") # legacy intent-topic bridge: fire the counterpart spelling of an # intent dispatch, for handlers written against old workshop. - self._bridge_intent_topics(message) + self._bridge_intent_topics(message, is_intent_twin) # ------------------------------------------------------------------ # Sync helpers used internally — same as FakeBus diff --git a/test/unittests/log_test/configured.log b/test/unittests/log_test/configured.log index c889d3fa..0b9ad23c 100644 --- a/test/unittests/log_test/configured.log +++ b/test/unittests/log_test/configured.log @@ -1,126 +1,126 @@ -2026-03-11 04:12:11.787 - configured - ovos_utils.network_utils:get_external_ip:78 - ERROR - Got resp=503: -2026-03-11 04:12:11.789 - configured - ovos_utils.network_utils:get_external_ip:80 - ERROR - Unable to get external IP Address: network error -2026-03-11 04:12:11.796 - configured - ovos_utils.network_utils:check_captive_portal:162 - ERROR - Error checking for captive portal +2026-08-01 19:07:55.198 - configured - ovos_utils.network_utils:get_external_ip:78 - ERROR - Got resp=503: +2026-08-01 19:07:55.202 - configured - ovos_utils.network_utils:get_external_ip:80 - ERROR - Unable to get external IP Address: network error +2026-08-01 19:07:55.211 - configured - ovos_utils.network_utils:check_captive_portal:162 - ERROR - Error checking for captive portal Traceback (most recent call last): - File "/home/miro/PycharmProjects/OpenVoiceOS Workspace/ovos-utils/ovos_utils/network_utils.py", line 156, in check_captive_portal + File "/home/miro/tmp/utils-fakebus-intent-reemit/ovos_utils/network_utils.py", line 156, in check_captive_portal html_doc = requests.get(host).text - ~~~~~~~~~~~~^^^^^^ - File "/home/miro/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/unittest/mock.py", line 1169, in __call__ + ^^^^^^^^^^^^^^^^^^ + File "/home/miro/.local/share/uv/python/cpython-3.12.13-linux-x86_64-gnu/lib/python3.12/unittest/mock.py", line 1139, in __call__ return self._mock_call(*args, **kwargs) - ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^ - File "/home/miro/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/unittest/mock.py", line 1173, in _mock_call + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/miro/.local/share/uv/python/cpython-3.12.13-linux-x86_64-gnu/lib/python3.12/unittest/mock.py", line 1143, in _mock_call return self._execute_mock_call(*args, **kwargs) - ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^ - File "/home/miro/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/unittest/mock.py", line 1228, in _execute_mock_call + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/miro/.local/share/uv/python/cpython-3.12.13-linux-x86_64-gnu/lib/python3.12/unittest/mock.py", line 1198, in _execute_mock_call raise effect Exception: timeout -2026-03-11 04:12:11.823 - configured - ovos_utils.ocp:from_dict:254 - ERROR - track dictionary does not contain 'uri', it is not a valid MediaEntry -2026-03-11 04:12:11.824 - configured - ovos_utils.ocp:from_dict:256 - WARNING - DEPRECATED: use dict2entry() for Playlists and PluginStreams, MediaEntry.from_dict is only for regular media, will start throwing ValueError in 0.1.0 -2026-03-11 04:12:11.837 - configured - ovos_utils.ocp:available_extractors:165 - ERROR - please install/update ovos_plugin_manager -2026-03-11 04:12:11.841 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 1 -2026-03-11 04:12:11.842 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 0 -2026-03-11 04:12:11.842 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 0 -2026-03-11 04:12:11.843 - configured - ovos_utils.ocp:goto_track:588 - ERROR - requested track not in the playlist: MediaEntry(uri='http://missing.com/x.mp3', title='', artist='', match_confidence=0, skill_id='ovos.common_play', playback=, status=, media_type=, length=0, image='', skill_icon='', javascript='') -2026-03-11 04:12:11.852 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 2 -2026-03-11 04:12:11.853 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 1 -2026-03-11 04:12:11.854 - configured - ovos_utils.ocp:goto_track:588 - ERROR - requested track not in the playlist: MediaEntry(uri='nonexistent', title='nonexistent', artist='', match_confidence=0, skill_id='ovos.common_play', playback=, status=, media_type=, length=0, image='', skill_icon='', javascript='') -2026-03-11 04:12:11.857 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (3! Going to start of playlist -2026-03-11 04:12:11.858 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (-1! Going to start of playlist -2026-03-11 04:12:11.859 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (-1! Going to start of playlist -2026-03-11 04:12:11.860 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (100! Going to start of playlist -2026-03-11 04:12:11.877 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (0! Going to start of playlist -2026-03-11 04:12:11.878 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (-1! Going to start of playlist -2026-03-11 04:12:11.878 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (1! Going to start of playlist -2026-03-11 04:12:11.879 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (1! Going to start of playlist -2026-03-11 04:12:11.880 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (10! Going to start of playlist -2026-03-11 04:12:11.882 - configured - ovos_utils.parse:_validate_matching_strategy:27 - ERROR - rapidfuzz is not installed, falling back to SequenceMatcher for all match strategies -2026-03-11 04:12:11.882 - configured - ovos_utils.parse:_validate_matching_strategy:29 - WARNING - pip install rapidfuzz -2026-03-11 04:12:11.904 - configured - ovos_utils.security:decrypt:93 - ERROR - run pip install pycryptodomex -2026-03-11 04:12:11.906 - configured - ovos_utils.security:decrypt:103 - ERROR - decryption failed, invalid key? -2026-03-11 04:12:11.910 - configured - ovos_utils.security:encrypt:80 - ERROR - run pip install pycryptodomex -2026-03-11 04:12:11.912 - configured - ovos_utils.security:create_self_signed_cert:34 - ERROR - run pip install pyopenssl -2026-03-11 04:12:11.917 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.918 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'my_service' -2026-03-11 04:12:11.918 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.919 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.920 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.921 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.923 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.924 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.924 - configured - ovos_utils.skill_installer:handle_install_python:391 - ERROR - pip disabled in mycroft.conf -2026-03-11 04:12:11.925 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.926 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.927 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.928 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.929 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.931 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.932 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.932 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.933 - configured - ovos_utils.skill_installer:handle_uninstall_python:427 - ERROR - pip disabled in mycroft.conf -2026-03-11 04:12:11.934 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.934 - configured - ovos_utils.skill_installer:pip_install:230 - ERROR - no package list provided to install -2026-03-11 04:12:11.935 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.937 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.938 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg -2026-03-11 04:12:11.938 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://example.com/c.txt my-pkg -2026-03-11 04:12:11.939 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.940 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg -2026-03-11 04:12:11.940 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/PycharmProjects/OpenVoiceOS Workspace/ovos-utils/.venv/bin/python3 -m pip install -c http://example.com/c.txt my-pkg -2026-03-11 04:12:11.941 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.942 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing bad-pkg -2026-03-11 04:12:11.943 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c https://raw.githubusercontent.com/OpenVoiceOS/ovos-releases/refs/heads/main/constraints-stable.txt bad-pkg -2026-03-11 04:12:11.944 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.945 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing pkg -2026-03-11 04:12:11.945 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt pkg -2026-03-11 04:12:11.947 - configured - ovos_utils.skill_installer:validate_constraints:205 - ERROR - Couldn't find the constraints file -2026-03-11 04:12:11.948 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt -2026-03-11 04:12:11.949 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/missing.txt -2026-03-11 04:12:11.949 - configured - ovos_utils.skill_installer:validate_constraints:195 - ERROR - Remote constraints file not accessible: 404 -2026-03-11 04:12:11.950 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt -2026-03-11 04:12:11.951 - configured - ovos_utils.skill_installer:validate_constraints:201 - ERROR - Error accessing remote constraints: timeout -2026-03-11 04:12:11.952 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.953 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.954 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.954 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-03-11 04:12:11.955 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt pkg -2026-03-11 04:12:11.956 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.956 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-03-11 04:12:11.957 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt --break-system-packages pkg -2026-03-11 04:12:11.958 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.959 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-03-11 04:12:11.959 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt --pre pkg -2026-03-11 04:12:11.960 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.961 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-03-11 04:12:11.961 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt pkg -2026-03-11 04:12:11.962 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:11.963 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg -2026-03-11 04:12:11.963 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.local/bin/uv pip install -c http://x.com/c.txt pkg -2026-03-11 04:12:11.964 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.965 - configured - ovos_utils.skill_installer:pip_uninstall:295 - ERROR - no package list provided to uninstall -2026-03-11 04:12:11.966 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:11.967 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' -2026-03-11 04:12:12.298 - configured - ovos_utils.skill_installer:pip_uninstall:330 - ERROR - tried to uninstall a protected package: ['ovos-core'] -2026-03-11 04:12:12.299 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:12.461 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:12.462 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg -2026-03-11 04:12:12.464 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:12.925 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:12.926 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/PycharmProjects/OpenVoiceOS Workspace/ovos-utils/.venv/bin/python3 -m pip uninstall -y custom-pkg -2026-03-11 04:12:12.929 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:13.262 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:13.263 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y custom-pkg -2026-03-11 04:12:13.267 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:13.269 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:13.294 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y custom-pkg -2026-03-11 04:12:13.296 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:13.296 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:13.297 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y custom-pkg -2026-03-11 04:12:13.298 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:13.569 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:13.570 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y --break-system-packages custom-pkg -2026-03-11 04:12:13.571 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' -2026-03-11 04:12:13.859 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg -2026-03-11 04:12:13.859 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.local/bin/uv pip uninstall -y custom-pkg -2026-03-11 04:12:13.913 - configured - ovos_utils.system:system_reboot:83 - DEBUG - sudo systemctl reboot -i -2026-03-11 04:12:13.915 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - sudo systemctl poweroff -i -2026-03-11 04:12:13.916 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - systemctl poweroff -i +2026-08-01 19:07:55.249 - configured - ovos_utils.ocp:from_dict:254 - ERROR - track dictionary does not contain 'uri', it is not a valid MediaEntry +2026-08-01 19:07:55.251 - configured - ovos_utils.ocp:from_dict:256 - WARNING - DEPRECATED: use dict2entry() for Playlists and PluginStreams, MediaEntry.from_dict is only for regular media, will start throwing ValueError in 0.1.0 +2026-08-01 19:07:55.270 - configured - ovos_utils.ocp:available_extractors:165 - ERROR - please install/update ovos_plugin_manager +2026-08-01 19:07:55.277 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 1 +2026-08-01 19:07:55.279 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 0 +2026-08-01 19:07:55.282 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 0 +2026-08-01 19:07:55.286 - configured - ovos_utils.ocp:goto_track:588 - ERROR - requested track not in the playlist: MediaEntry(uri='http://missing.com/x.mp3', title='', artist='', match_confidence=0, skill_id='ovos.common_play', playback=, status=, media_type=, length=0, image='', skill_icon='', javascript='') +2026-08-01 19:07:55.299 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 2 +2026-08-01 19:07:55.301 - configured - ovos_utils.ocp:goto_track:586 - DEBUG - New playlist position: 1 +2026-08-01 19:07:55.332 - configured - ovos_utils.ocp:goto_track:588 - ERROR - requested track not in the playlist: MediaEntry(uri='nonexistent', title='nonexistent', artist='', match_confidence=0, skill_id='ovos.common_play', playback=, status=, media_type=, length=0, image='', skill_icon='', javascript='') +2026-08-01 19:07:55.337 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (3! Going to start of playlist +2026-08-01 19:07:55.340 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (-1! Going to start of playlist +2026-08-01 19:07:55.343 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (-1! Going to start of playlist +2026-08-01 19:07:55.345 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (100! Going to start of playlist +2026-08-01 19:07:55.369 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (0! Going to start of playlist +2026-08-01 19:07:55.371 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (-1! Going to start of playlist +2026-08-01 19:07:55.373 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (1! Going to start of playlist +2026-08-01 19:07:55.375 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (1! Going to start of playlist +2026-08-01 19:07:55.378 - configured - ovos_utils.ocp:_validate_position:607 - ERROR - Playlist pointer is in an invalid position (10! Going to start of playlist +2026-08-01 19:07:55.382 - configured - ovos_utils.parse:_validate_matching_strategy:27 - ERROR - rapidfuzz is not installed, falling back to SequenceMatcher for all match strategies +2026-08-01 19:07:55.384 - configured - ovos_utils.parse:_validate_matching_strategy:29 - WARNING - pip install rapidfuzz +2026-08-01 19:07:55.413 - configured - ovos_utils.security:decrypt:105 - ERROR - run pip install pycryptodomex +2026-08-01 19:07:55.416 - configured - ovos_utils.security:decrypt:115 - ERROR - decryption failed, invalid key? +2026-08-01 19:07:55.421 - configured - ovos_utils.security:encrypt:92 - ERROR - run pip install pycryptodomex +2026-08-01 19:07:55.427 - configured - ovos_utils.security:create_self_signed_cert:46 - ERROR - run pip install pyopenssl +2026-08-01 19:07:55.439 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:55.442 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'my_service' +2026-08-01 19:07:55.444 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:55.447 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.450 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.452 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:55.455 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:55.458 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:55.460 - configured - ovos_utils.skill_installer:handle_install_python:391 - ERROR - pip disabled in mycroft.conf +2026-08-01 19:07:55.463 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.466 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.468 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.472 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.475 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.478 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.481 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.483 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:55.486 - configured - ovos_utils.skill_installer:handle_uninstall_python:427 - ERROR - pip disabled in mycroft.conf +2026-08-01 19:07:55.489 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.491 - configured - ovos_utils.skill_installer:pip_install:230 - ERROR - no package list provided to install +2026-08-01 19:07:55.494 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.497 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.500 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg +2026-08-01 19:07:55.502 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://example.com/c.txt my-pkg +2026-08-01 19:07:55.505 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.508 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing my-pkg +2026-08-01 19:07:55.510 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /home/miro/.venvs/ovos/bin/python -m pip install -c http://example.com/c.txt my-pkg +2026-08-01 19:07:55.513 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.516 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing bad-pkg +2026-08-01 19:07:55.518 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c https://raw.githubusercontent.com/OpenVoiceOS/ovos-releases/refs/heads/main/constraints-stable.txt bad-pkg +2026-08-01 19:07:55.521 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.524 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [ovos_test] (pip) Installing pkg +2026-08-01 19:07:55.527 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg +2026-08-01 19:07:55.531 - configured - ovos_utils.skill_installer:validate_constraints:205 - ERROR - Couldn't find the constraints file +2026-08-01 19:07:55.534 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt +2026-08-01 19:07:55.537 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/missing.txt +2026-08-01 19:07:55.539 - configured - ovos_utils.skill_installer:validate_constraints:195 - ERROR - Remote constraints file not accessible: 404 +2026-08-01 19:07:55.542 - configured - ovos_utils.skill_installer:validate_constraints:191 - DEBUG - Constraints url: http://example.com/c.txt +2026-08-01 19:07:55.545 - configured - ovos_utils.skill_installer:validate_constraints:201 - ERROR - Error accessing remote constraints: timeout +2026-08-01 19:07:55.547 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.550 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.553 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:55.556 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-08-01 19:07:55.558 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg +2026-08-01 19:07:55.561 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:55.564 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-08-01 19:07:55.566 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt --break-system-packages pkg +2026-08-01 19:07:55.569 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:55.572 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-08-01 19:07:55.574 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt --pre pkg +2026-08-01 19:07:55.577 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:55.580 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-08-01 19:07:55.582 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg +2026-08-01 19:07:55.585 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:55.588 - configured - ovos_utils.skill_installer:pip_install:256 - INFO - [svc] (pip) Installing pkg +2026-08-01 19:07:55.591 - configured - ovos_utils.skill_installer:pip_install:258 - DEBUG - /usr/bin/uv pip install -c http://x.com/c.txt pkg +2026-08-01 19:07:55.594 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.597 - configured - ovos_utils.skill_installer:pip_uninstall:295 - ERROR - no package list provided to uninstall +2026-08-01 19:07:55.600 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.603 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'ovos_test' +2026-08-01 19:07:55.917 - configured - ovos_utils.skill_installer:pip_uninstall:330 - ERROR - tried to uninstall a protected package: ['ovos-core'] +2026-08-01 19:07:55.920 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:55.986 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 19:07:55.988 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg +2026-08-01 19:07:55.991 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:56.053 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 19:07:56.055 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /home/miro/.venvs/ovos/bin/python -m pip uninstall -y custom-pkg +2026-08-01 19:07:56.058 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:56.121 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 19:07:56.123 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg +2026-08-01 19:07:56.127 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:56.130 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 19:07:56.132 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg +2026-08-01 19:07:56.135 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:56.138 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 19:07:56.140 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg +2026-08-01 19:07:56.144 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:56.206 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 19:07:56.208 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y --break-system-packages custom-pkg +2026-08-01 19:07:56.211 - configured - ovos_utils.skill_installer:__init__:146 - INFO - ServiceInstaller registered for service 'svc' +2026-08-01 19:07:56.278 - configured - ovos_utils.skill_installer:pip_uninstall:344 - INFO - [svc] (pip) Uninstalling custom-pkg +2026-08-01 19:07:56.280 - configured - ovos_utils.skill_installer:pip_uninstall:348 - DEBUG - /usr/bin/uv pip uninstall -y custom-pkg +2026-08-01 19:07:56.368 - configured - ovos_utils.system:system_reboot:83 - DEBUG - sudo systemctl reboot -i +2026-08-01 19:07:56.373 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - sudo systemctl poweroff -i +2026-08-01 19:07:56.378 - configured - ovos_utils.system:system_shutdown:66 - DEBUG - systemctl poweroff -i diff --git a/test/unittests/log_test/rotate.log b/test/unittests/log_test/rotate.log index dd7d75aa..04018e48 100644 --- a/test/unittests/log_test/rotate.log +++ b/test/unittests/log_test/rotate.log @@ -1 +1 @@ -2026-03-11 04:12:13.924 - rotate - ovos_utils.system:ssh_enable - WARNING - Deprecation version=0.2.0. Caller=test_system:165. DEPRECATED: use ovos-PHAL-plugin-system +2026-08-01 19:07:56.393 - rotate - ovos_utils.system:ssh_enable - WARNING - Deprecation version=0.2.0. Caller=test_system:165. DEPRECATED: use ovos-PHAL-plugin-system diff --git a/test/unittests/log_test/rotate.log.1 b/test/unittests/log_test/rotate.log.1 index 8f4286ae..d6bc10dc 100644 --- a/test/unittests/log_test/rotate.log.1 +++ b/test/unittests/log_test/rotate.log.1 @@ -1 +1 @@ -2026-03-11 04:12:13.923 - rotate - ovos_utils.system:ssh_disable - WARNING - Deprecation version=0.2.0. Caller=test_system:175. DEPRECATED: use ovos-PHAL-plugin-system +2026-08-01 19:07:56.390 - rotate - ovos_utils.system:ssh_disable - WARNING - Deprecation version=0.2.0. Caller=test_system:175. DEPRECATED: use ovos-PHAL-plugin-system diff --git a/test/unittests/test_fakebus_intent_legacy_reemit.py b/test/unittests/test_fakebus_intent_legacy_reemit.py index 0385f10f..5dd86a6e 100644 --- a/test/unittests/test_fakebus_intent_legacy_reemit.py +++ b/test/unittests/test_fakebus_intent_legacy_reemit.py @@ -39,13 +39,28 @@ def test_suffixed_handler_receives_the_twin(self): self.assertEqual([m.msg_type for m in got], [LEGACY]) self.assertEqual(got[0].data, {"utterance": "one pizza"}) - def test_twin_is_marked_and_keeps_context(self): + def test_twin_keeps_context_but_is_delivered_unmarked(self): + # the twin keeps the ordinary context it forwards, but the dedup marker + # must NOT reach local handlers: it would ride forward()/reply() onto + # any follow-up message a handler emits and suppress its modernization. bus = FakeBus() got = [] bus.on(LEGACY, got.append) bus.emit(Message(CANONICAL, {"a": 1}, {"source": ["me"]})) self.assertEqual(got[0].context["source"], ["me"]) - self.assertTrue(got[0].context[INTENT_COMPAT_TWIN_KEY]) + self.assertNotIn(INTENT_COMPAT_TWIN_KEY, got[0].context) + + def test_twin_carries_the_marker_on_the_wire(self): + # wire survival: the serialized twin on the "message" firehose keeps the + # marker, so a receiver in another process still skips re-modernizing it. + import json + bus = FakeBus() + wire = [] + bus.on("message", lambda m: wire.append(json.loads(m))) + bus.emit(Message(CANONICAL)) + twins = [f for f in wire if f["type"] == LEGACY] + self.assertEqual(len(twins), 1) + self.assertTrue(twins[0]["context"][INTENT_COMPAT_TWIN_KEY]) def test_canonical_handler_fires_exactly_once(self): bus = FakeBus() @@ -111,6 +126,57 @@ def test_no_modernization_when_compat_is_disabled(self): self.assertEqual(got, []) +class TestMarkerDoesNotLeakToDescendants(unittest.TestCase): + """The twin marker must not ride forward()/reply() onto later messages. + + Message.forward()/reply() deep-copy the whole context. If a delivered twin + kept the marker, a handler that forwards that context to emit an UNRELATED + suffixed intent would brand the follow-up a twin, and the bridge would + silently drop its canonical spelling. + """ + + UNRELATED_LEGACY = "other-skill.jarbas:unrelated.intent" + UNRELATED_CANON = "other-skill.jarbas:unrelated" + + def test_forward_off_a_twin_does_not_suppress_an_unrelated_intent(self): + bus = FakeBus() + seen_twin = [] + bus.on(LEGACY, seen_twin.append) + bus.emit(Message(CANONICAL, {"utterance": "one pizza"})) + twin_msg = seen_twin[0] + self.assertNotIn(INTENT_COMPAT_TWIN_KEY, twin_msg.context) + # a handler forwards this frame's context to emit an unrelated intent. + got_canon = [] + bus.on(self.UNRELATED_CANON, got_canon.append) + followup = twin_msg.forward(self.UNRELATED_LEGACY, {}) + self.assertNotIn(INTENT_COMPAT_TWIN_KEY, followup.context) + bus.emit(followup) + # the unrelated canonical topic IS modernized: the marker did not leak. + self.assertEqual([m.msg_type for m in got_canon], [self.UNRELATED_CANON]) + + def test_reply_off_a_twin_does_not_suppress_an_unrelated_intent(self): + bus = FakeBus() + seen_twin = [] + bus.on(LEGACY, seen_twin.append) + bus.emit(Message(CANONICAL)) + got_canon = [] + bus.on(self.UNRELATED_CANON, got_canon.append) + bus.emit(seen_twin[0].reply(self.UNRELATED_LEGACY, {})) + self.assertEqual([m.msg_type for m in got_canon], [self.UNRELATED_CANON]) + + def test_marker_survives_on_the_wire_for_a_second_receiver(self): + # a marked frame emitted (as if arriving from the wire) is delivered to + # its legacy listener but NOT re-modernized: wire survival intact. + bus = FakeBus() + got_legacy = [] + got_canon = [] + bus.on(LEGACY, got_legacy.append) + bus.on(CANONICAL, got_canon.append) + bus.emit(Message(LEGACY, {}, {INTENT_COMPAT_TWIN_KEY: True})) + self.assertEqual(len(got_legacy), 1) + self.assertEqual(len(got_canon), 0) + + class TestNonIntentTopics(unittest.TestCase): def test_dotted_topics_are_untouched(self): bus = FakeBus() @@ -136,7 +202,8 @@ def test_canonical_dispatch_fires_the_twin(self): bus.on(LEGACY, got.append) _run(bus.emit(Message(CANONICAL))) self.assertEqual([m.msg_type for m in got], [LEGACY]) - self.assertTrue(got[0].context[INTENT_COMPAT_TWIN_KEY]) + # delivered unmarked (no leak onto descendants); marker rides the wire + self.assertNotIn(INTENT_COMPAT_TWIN_KEY, got[0].context) def test_suffixed_dispatch_fires_the_canonical_form(self): bus = AsyncFakeBus() From a345cefd6024c6c69fcd568e68bdd4e399413eb1 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sat, 1 Aug 2026 19:26:21 +0100 Subject: [PATCH 8/8] fix: isolate errors from the legacy intent-topic bridge in FakeBus.emit _bridge_intent_topics() ran unguarded in both FakeBus.emit and AsyncFakeBus.emit, unlike the counterpart-topics loop right above it, which isolates per-topic errors with try/except + LOG.exception. A raising bridge helper would propagate out of emit() and take down local handler dispatch with it. Wrap both call sites in the same guard. Addresses CodeRabbit review on ovos-utils#411. --- ovos_utils/fakebus.py | 10 ++++++-- .../test_fakebus_intent_legacy_reemit.py | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/ovos_utils/fakebus.py b/ovos_utils/fakebus.py index 153cd88d..60de1333 100644 --- a/ovos_utils/fakebus.py +++ b/ovos_utils/fakebus.py @@ -216,7 +216,10 @@ def emit(self, message): LOG.exception(f"Error in counterpart dispatch for '{topic}': {e}") # legacy intent-topic bridge: fire the counterpart spelling of an # intent dispatch, for handlers written against old workshop. - self._bridge_intent_topics(message, is_intent_twin) + try: + self._bridge_intent_topics(message, is_intent_twin) + except Exception as e: + LOG.exception(f"Error in intent-topic bridge for '{message.msg_type}': {e}") def on_message(self, *args): """ @@ -576,7 +579,10 @@ async def emit(self, message): LOG.exception(f"Error in counterpart dispatch for '{topic}': {e}") # legacy intent-topic bridge: fire the counterpart spelling of an # intent dispatch, for handlers written against old workshop. - self._bridge_intent_topics(message, is_intent_twin) + try: + self._bridge_intent_topics(message, is_intent_twin) + except Exception as e: + LOG.exception(f"Error in intent-topic bridge for '{message.msg_type}': {e}") # ------------------------------------------------------------------ # Sync helpers used internally — same as FakeBus diff --git a/test/unittests/test_fakebus_intent_legacy_reemit.py b/test/unittests/test_fakebus_intent_legacy_reemit.py index 5dd86a6e..cdb19317 100644 --- a/test/unittests/test_fakebus_intent_legacy_reemit.py +++ b/test/unittests/test_fakebus_intent_legacy_reemit.py @@ -193,6 +193,30 @@ def test_nothing_extra_is_dispatched(self): self.assertEqual(len(got), 1) +class TestBridgeIntentTopicsErrorIsolation(unittest.TestCase): + """A raising `_bridge_intent_topics` must not propagate out of emit(), + matching the counterpart-topics loop's own try/except + LOG.exception + resilience pattern in the same method (CodeRabbit review, #411).""" + + def test_sync_bus_emit_survives_a_raising_bridge(self): + bus = FakeBus() + bus._bridge_intent_topics = lambda *a, **k: (_ for _ in ()).throw( + RuntimeError("boom")) + got = [] + bus.on(CANONICAL, got.append) + bus.emit(Message(CANONICAL)) # must not raise + self.assertEqual([m.msg_type for m in got], [CANONICAL]) + + def test_async_bus_emit_survives_a_raising_bridge(self): + bus = AsyncFakeBus() + bus._bridge_intent_topics = lambda *a, **k: (_ for _ in ()).throw( + RuntimeError("boom")) + got = [] + bus.on(CANONICAL, got.append) + _run(bus.emit(Message(CANONICAL))) # must not raise + self.assertEqual([m.msg_type for m in got], [CANONICAL]) + + class TestAsyncFakeBus(unittest.TestCase): """The async double runs the same two rules."""