Skip to content

Commit 40c7757

Browse files
JarbasAlclaude
andauthored
refactor: delegate handler-lifecycle done-signal to HandlerLifecycle util (#440)
The three event-wrapper callbacks (_on_event_start/_on_event_end/ _on_event_error) emitted the internal workshop->core done-signal trio (mycroft.skill.handler.{start,complete,error}) by hand. Delegate that emission to the shared ovos_bus_client.handler.HandlerLifecycle util (#246) so the wire format lives in one place. Pure DRY: same topics, same payloads ({'name': <handler>} on start/complete, + {'exception': repr(error)} on error), same context['skill_id'] stamping and preserved session. The util stamps skill_id on the forwarded copy rather than mutating the caller's message, and builds a fresh payload per emission instead of mutating the shared skill_data dict. Preserved (NOT part of the trio): _on_event_end still emits ovos.utterance.handled when is_intent and still runs settings.store(); _on_event_error still speaks the skill.error dialog and logs the exception (the util deliberately does not speak). Bumps the ovos-bus-client floor to >=2.6.0a1 (carries the util). Fills the previously-stubbed test_on_event_{start,end,error} tests, asserting the emitted topic/payload/context is unchanged. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7be1f20 commit 40c7757

3 files changed

Lines changed: 103 additions & 21 deletions

File tree

ovos_workshop/skills/ovos.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from ovos_bus_client.apis.events import EventSchedulerInterface
3434
from ovos_bus_client.apis.gui import GUIInterface
3535
from ovos_bus_client.apis.ocp import OCPInterface
36+
from ovos_bus_client.handler import HandlerLifecycle
3637
from ovos_bus_client.message import Message, dig_for_message
3738
from ovos_bus_client.session import SessionManager, Session
3839
from ovos_bus_client.util import get_message_lang
@@ -1454,10 +1455,11 @@ def _on_event_start(self, message: Message, handler_info: str,
14541455
"""
14551456
if handler_info:
14561457
# internal workshop->core done-signal (see docstring); NOT a spec
1457-
# topic -> emits mycroft.skill.handler.start
1458-
msg_type = handler_info + '.start'
1459-
message.context["skill_id"] = self.skill_id
1460-
self.bus.emit(message.forward(msg_type, skill_data))
1458+
# topic -> emits mycroft.skill.handler.start. Delegated to the
1459+
# shared ovos-bus-client HandlerLifecycle util (DRY; same topic,
1460+
# payload and context["skill_id"] as before).
1461+
HandlerLifecycle(self.bus, message, skill_id=self.skill_id,
1462+
data=skill_data, handler_info=handler_info).start()
14611463

14621464
def _on_event_end(self, message: Message, handler_info: str,
14631465
skill_data: dict, is_intent: bool = False):
@@ -1467,10 +1469,10 @@ def _on_event_end(self, message: Message, handler_info: str,
14671469
"""
14681470
if handler_info:
14691471
# internal workshop->core done-signal (see _on_event_start); NOT a
1470-
# spec topic -> emits mycroft.skill.handler.complete
1471-
msg_type = handler_info + '.complete'
1472-
message.context["skill_id"] = self.skill_id
1473-
self.bus.emit(message.forward(msg_type, skill_data))
1472+
# spec topic -> emits mycroft.skill.handler.complete. Delegated to
1473+
# the shared HandlerLifecycle util (same topic/payload/context).
1474+
HandlerLifecycle(self.bus, message, skill_id=self.skill_id,
1475+
data=skill_data, handler_info=handler_info).complete()
14741476
if is_intent:
14751477
self.bus.emit(message.forward(SpecMessage.UTTERANCE_HANDLED, skill_data))
14761478

@@ -1492,15 +1494,15 @@ def _on_event_error(self, error: str, message: Message, handler_info: str,
14921494
if speak_errors:
14931495
self.speak(speech)
14941496
self.log.exception(error)
1495-
# append exception information in message
1496-
skill_data['exception'] = repr(error)
14971497
if handler_info:
14981498
# internal workshop->core done-signal (see _on_event_start); NOT a
1499-
# spec topic -> emits mycroft.skill.handler.error
1500-
msg_type = handler_info + '.error'
1499+
# spec topic -> emits mycroft.skill.handler.error. Delegated to the
1500+
# shared HandlerLifecycle util, which merges {"exception": repr(...)}
1501+
# into the payload (same topic/payload/context as before). The util
1502+
# deliberately does NOT speak; the spoken-error UX above stays here.
15011503
message = message or Message("")
1502-
message.context["skill_id"] = self.skill_id
1503-
self.bus.emit(message.forward(msg_type, skill_data))
1504+
HandlerLifecycle(self.bus, message, skill_id=self.skill_id,
1505+
data=skill_data, handler_info=handler_info).error(error)
15041506

15051507
def _register_adapt_intent(self,
15061508
intent_parser: Union[IntentBuilder, Intent, str],

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ authors = [{name = "jarbasAi", email = "jarbasai@mailfence.com"}]
1212
requires-python = ">=3.9"
1313
dependencies = [
1414
"ovos-utils>= 0.7.0,<1.0.0",
15-
"ovos_bus_client>=2.2.0a1,<3.0.0",
15+
"ovos_bus_client>=2.6.0a1,<3.0.0", # HandlerLifecycle done-signal util (#246)
1616
"ovos-config>=0.0.12,<3.0.0",
1717
"ovos-spec-tools>=0.16.1a2", # carries the adapt-free intent-definition primitives (OVOS-INTENT-4)
1818
"ovos-yes-no-plugin>=0.3.0,<1.0.0",

test/unittests/skills/test_base.py

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -297,17 +297,97 @@ def test_find_resource(self):
297297
# TODO
298298
pass
299299

300+
def _capture(self, fn, *args, **kwargs):
301+
"""Run an _on_event_* callback against a private FakeBus and return the
302+
list of (type, data, context) tuples it emitted on the topics of
303+
interest (the handler trio + ovos.utterance.handled)."""
304+
from ovos_spec_tools import SpecMessage
305+
skill = OVOSSkill(bus=FakeBus(), skill_id=self.skill_id)
306+
captured = []
307+
topics = ["mycroft.skill.handler.start",
308+
"mycroft.skill.handler.complete",
309+
"mycroft.skill.handler.error",
310+
SpecMessage.UTTERANCE_HANDLED.value]
311+
for t in topics:
312+
skill.bus.on(t, lambda m: captured.append(
313+
(m.msg_type, m.data, dict(m.context))))
314+
fn(skill, *args, **kwargs)
315+
return captured
316+
300317
def test_on_event_start(self):
301-
# TODO
302-
pass
318+
from ovos_bus_client.message import Message
319+
msg = Message("trigger", {}, {"session": {"session_id": "sess1"}})
320+
skill_data = {"name": "TestSkill.handle_test"}
321+
captured = self._capture(OVOSSkill._on_event_start, msg,
322+
"mycroft.skill.handler", dict(skill_data))
323+
# exactly one emission: the .start done-signal
324+
starts = [c for c in captured
325+
if c[0] == "mycroft.skill.handler.start"]
326+
self.assertEqual(len(starts), 1)
327+
mtype, data, context = starts[0]
328+
# byte-identical topic + payload
329+
self.assertEqual(mtype, "mycroft.skill.handler.start")
330+
self.assertEqual(data, {"name": "TestSkill.handle_test"})
331+
# context carries skill_id + preserves originating session
332+
self.assertEqual(context["skill_id"], self.skill_id)
333+
self.assertEqual(context["session"]["session_id"], "sess1")
334+
# original message context not mutated by the util
335+
self.assertNotIn("skill_id", msg.context)
336+
# empty/false handler_info disables emission entirely
337+
none_captured = self._capture(OVOSSkill._on_event_start, msg, "",
338+
dict(skill_data))
339+
self.assertEqual(
340+
[c for c in none_captured
341+
if c[0].startswith("mycroft.skill.handler")], [])
303342

304343
def test_on_event_end(self):
305-
# TODO
306-
pass
344+
from ovos_bus_client.message import Message
345+
msg = Message("trigger", {}, {"session": {"session_id": "sess1"}})
346+
skill_data = {"name": "TestSkill.handle_test"}
347+
captured = self._capture(OVOSSkill._on_event_end, msg,
348+
"mycroft.skill.handler", dict(skill_data))
349+
completes = [c for c in captured
350+
if c[0] == "mycroft.skill.handler.complete"]
351+
self.assertEqual(len(completes), 1)
352+
mtype, data, context = completes[0]
353+
self.assertEqual(mtype, "mycroft.skill.handler.complete")
354+
self.assertEqual(data, {"name": "TestSkill.handle_test"})
355+
self.assertEqual(context["skill_id"], self.skill_id)
356+
self.assertEqual(context["session"]["session_id"], "sess1")
357+
358+
def test_on_event_end_is_intent_still_emits_utterance_handled(self):
359+
# the ovos.utterance.handled emission must be PRESERVED alongside the
360+
# delegated .complete done-signal when is_intent=True
361+
from ovos_bus_client.message import Message
362+
from ovos_spec_tools import SpecMessage
363+
msg = Message("trigger", {}, {})
364+
skill_data = {"name": "TestSkill.handle_test"}
365+
captured = self._capture(OVOSSkill._on_event_end, msg,
366+
"mycroft.skill.handler", dict(skill_data),
367+
True)
368+
types = [c[0] for c in captured]
369+
self.assertIn("mycroft.skill.handler.complete", types)
370+
self.assertIn(SpecMessage.UTTERANCE_HANDLED.value, types)
307371

308372
def test_on_event_error(self):
309-
# TODO
310-
pass
373+
from ovos_bus_client.message import Message
374+
msg = Message("trigger", {}, {"session": {"session_id": "sess1"}})
375+
skill_data = {"name": "TestSkill.handle_test"}
376+
err = "boom" # workshop passes str(error)
377+
captured = self._capture(OVOSSkill._on_event_error, err, msg,
378+
"mycroft.skill.handler", dict(skill_data),
379+
False)
380+
errors = [c for c in captured
381+
if c[0] == "mycroft.skill.handler.error"]
382+
self.assertEqual(len(errors), 1)
383+
mtype, data, context = errors[0]
384+
self.assertEqual(mtype, "mycroft.skill.handler.error")
385+
# payload = original {name} + repr(error) under "exception" (identical
386+
# to the pre-refactor skill_data['exception'] = repr(error))
387+
self.assertEqual(data, {"name": "TestSkill.handle_test",
388+
"exception": repr(err)})
389+
self.assertEqual(context["skill_id"], self.skill_id)
390+
self.assertEqual(context["session"]["session_id"], "sess1")
311391

312392
def test_add_event(self):
313393
# TODO

0 commit comments

Comments
 (0)