Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion ovos_workshop/skills/ovos.py
Original file line number Diff line number Diff line change
Expand Up @@ -1551,7 +1551,19 @@ def speak(self, utterance: str, expect_response: bool = False,
meta["translation_data"])
m.context["translation_data"] = tx_data

self.bus.emit(m)
# bus-namespace migration: "speak" -> "ovos.utterance.speak"
# (architecture PIPELINE-1 §9.6). During the migration we dual-emit on
# both topics so consumers on either namespace are reached; they dedupe
# on content. Always emit the new topic; also emit the legacy "speak"
# while `legacy_namespace` is True (default). The full payload (incl.
# expect_response/meta) is kept on both emissions; the wait/
# expect_response handshake is keyed on the session, not the topic, so
# it is unaffected by the topic change.
# TODO: remove the legacy "speak" branch in the next major release,
# once every node emits the ovos.* topic only.
if Configuration().get("legacy_namespace", True):
self.bus.emit(m.forward("speak", data))
self.bus.emit(m.forward("ovos.utterance.speak", data))

if wait:
timeout = 15 if isinstance(wait, bool) else wait
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ authors = [{name = "jarbasAi", email = "jarbasai@mailfence.com"}]
requires-python = ">=3.9"
dependencies = [
"ovos-utils>= 0.7.0,<1.0.0",
"ovos_bus_client>=1.3.8a1,<3.0.0",
"ovos_bus_client>=2.2.0a1,<3.0.0",
"ovos-config>=0.0.12,<3.0.0",
"ovos-yes-no-plugin>=0.3.0,<1.0.0",
"ovos-option-matcher-fuzzy-plugin>=0.0.1,<1.0.0",
Expand Down
57 changes: 57 additions & 0 deletions test/unittests/skills/test_ovos.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,60 @@ def test_class_inheritance(self):
self.assertIsInstance(skill, OVOSSkill)
self.assertNotIsInstance(skill, OVOSAbstractApplication)


class TestSpeakNamespaceMigration(unittest.TestCase):
"""speak -> ovos.utterance.speak bus-namespace migration (dual-emit)."""

def setUp(self):
self.bus = FakeBus()
self.skill = OVOSSkill(bus=self.bus, skill_id="test_speak_ns")
self.emitted = []
# FakeBus dispatches by exact msg_type, so subscribe to both topics
self.bus.on("speak", lambda m: self.emitted.append(m))
self.bus.on("ovos.utterance.speak",
lambda m: self.emitted.append(m))

def _types(self):
return [m.msg_type for m in self.emitted]

def test_dual_emit_when_legacy_namespace(self):
from unittest.mock import patch
cfg = {"legacy_namespace": True}
with patch("ovos_workshop.skills.ovos.Configuration",
return_value=cfg):
self.skill.speak("hello world")
# both the legacy and the new topic are emitted
self.assertIn("speak", self._types())
self.assertIn("ovos.utterance.speak", self._types())
self.assertEqual(len(self.emitted), 2)
# identical payload on both, all fields preserved
for m in self.emitted:
self.assertEqual(m.data["utterance"], "hello world")
self.assertIn("expect_response", m.data)
self.assertIn("meta", m.data)
self.assertIn("lang", m.data)
self.assertEqual(m.data["meta"]["skill"], "test_speak_ns")
# context preserved on both emissions
self.assertEqual(m.context.get("skill_id"), "test_speak_ns")

def test_only_new_topic_when_legacy_disabled(self):
from unittest.mock import patch
cfg = {"legacy_namespace": False}
with patch("ovos_workshop.skills.ovos.Configuration",
return_value=cfg):
self.skill.speak("goodbye", expect_response=True)
self.assertEqual(self._types(), ["ovos.utterance.speak"])
m = self.emitted[0]
self.assertEqual(m.data["utterance"], "goodbye")
# expect_response carried on the new topic too
self.assertTrue(m.data["expect_response"])

def test_default_is_dual_emit(self):
# no legacy_namespace key -> defaults to True (dual-emit during migration)
from unittest.mock import patch
with patch("ovos_workshop.skills.ovos.Configuration",
return_value={}):
self.skill.speak("default behaviour")
self.assertIn("speak", self._types())
self.assertIn("ovos.utterance.speak", self._types())

Loading