diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml index 237096e4..44534d48 100644 --- a/.github/workflows/build_tests.yml +++ b/.github/workflows/build_tests.yml @@ -13,3 +13,8 @@ jobs: with: test_path: 'test/unittests' install_extras: 'test' + # ovos-gui-api-client is not yet on PyPI (lives on its feat/packaging branch, + # PR OpenVoiceOS/ovos-gui-api-client#2). Install it from git so CI can resolve + # the new skill-side GUIInterface. Revert to the released version (or @dev) once + # that PR merges and an alpha is published. + pre_install_pip: 'git+https://github.com/OpenVoiceOS/ovos-gui-api-client@dev' diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ba4fdddc..ef483ea4 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,6 +21,10 @@ jobs: sudo apt-get update sudo apt install python3-dev python -m pip install build wheel + - name: Install unreleased ovos-gui-api-client (until PyPI release) + # PR OpenVoiceOS/ovos-gui-api-client#2 — drop once published + run: | + pip install git+https://github.com/OpenVoiceOS/ovos-gui-api-client@dev - name: Install repo run: | pip install -e . diff --git a/ovos_workshop/app.py b/ovos_workshop/app.py index 65536e8c..2f3d1073 100644 --- a/ovos_workshop/app.py +++ b/ovos_workshop/app.py @@ -3,7 +3,7 @@ from ovos_config.locations import get_xdg_config_save_path from ovos_bus_client.util import get_mycroft_bus from ovos_utils.lang import standardize_lang_tag -from ovos_bus_client.apis.gui import GUIInterface +from ovos_gui_api_client import GUIInterface from ovos_bus_client.client.client import MessageBusClient from ovos_workshop.resource_files import locate_lang_directories from ovos_workshop.skills.ovos import OVOSSkill diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index 1236c636..c54b11f6 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -31,7 +31,7 @@ from ovos_bus_client import MessageBusClient from ovos_bus_client.apis.enclosure import EnclosureAPI from ovos_bus_client.apis.events import EventSchedulerInterface -from ovos_bus_client.apis.gui import GUIInterface +from ovos_gui_api_client import GUIInterface from ovos_bus_client.apis.ocp import OCPInterface from ovos_bus_client.handler import HandlerLifecycle from ovos_bus_client.message import Message, dig_for_message @@ -50,7 +50,6 @@ from ovos_utils.dialog import MustacheDialogRenderer from ovos_utils.events import EventContainer, get_handler_name, create_wrapper from ovos_utils.file_utils import FileWatcher -from ovos_utils.gui import get_ui_directories from ovos_utils.json_helper import merge_dict from ovos_utils.lang import standardize_lang_tag from ovos_utils.log import LOG @@ -845,7 +844,6 @@ def _init_skill_gui(self): Set up the SkillGUI for this skill and connect relevant bus events. """ self.gui = SkillGUI(self) - self.gui.setup_default_handlers() def register_homescreen_app(self, icon: str, name: str, event: str): """the icon file MUST be located under 'gui' subfolder""" @@ -2547,15 +2545,13 @@ def __init__(self, skill: OVOSSkill): Initialize a SkillGUI that connects a skill to the GUI framework. Parameters: - skill (OVOSSkill): The skill instance whose GUI should be managed. The constructor initializes the underlying GUIInterface using the skill's id, message bus, GUI configuration, and UI directories. + skill (OVOSSkill): The skill instance whose GUI should be managed. The constructor initializes the underlying GUIInterface using the skill's id, message bus, and GUI configuration. """ self._skill = skill skill_id = skill.skill_id bus = skill.bus config = skill.config_core.get('gui') - ui_directories = get_ui_directories(skill.root_dir) - GUIInterface.__init__(self, skill_id=skill_id, bus=bus, config=config, - ui_directories=ui_directories) + GUIInterface.__init__(self, skill_id=skill_id, bus=bus, config=config) diff --git a/pyproject.toml b/pyproject.toml index a08cfcae..042dd3f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ requires-python = ">=3.9" dependencies = [ "ovos-utils>= 0.13.3a2,<1.0.0", # FakeBus folds before handlers (ovos-utils#396) — keeps in-place add_context mutations alive so intent-layer gating works "ovos_bus_client>=2.6.2a2,<3.0.0", # HandlerLifecycle done-signal util (#246) + singleton-registry SessionManager (2.6.2a2) + "ovos-gui-api-client>=0.0.1,<1.0.0", "ovos-config>=0.0.12,<3.0.0", "ovos-spec-tools>=1.2.3a1", # canonical SessionManager registry + OVOS-INTENT-4 intent-definition primitives "ovos-yes-no-plugin>=0.3.0,<1.0.0", diff --git a/test/unittests/test_gui_rebind.py b/test/unittests/test_gui_rebind.py new file mode 100644 index 00000000..8e27caa5 --- /dev/null +++ b/test/unittests/test_gui_rebind.py @@ -0,0 +1,65 @@ +"""Tests for binding OVOSSkill.gui to the standalone ovos-gui-api-client. + +The skill-side GUIInterface now lives in `ovos_gui_api_client` (extracted from +ovos-bus-client). SkillGUI must subclass it and construct without the obsolete +`ui_directories` argument (skills no longer ship QML). +""" +import unittest +from unittest.mock import MagicMock + +from ovos_utils.fakebus import FakeBus + + +class TestGUIRebind(unittest.TestCase): + def test_skillgui_subclasses_api_client_interface(self): + from ovos_gui_api_client import GUIInterface + from ovos_workshop.skills.ovos import SkillGUI, GUIInterface as wGUI + # workshop must import the api-client interface, not the legacy one + self.assertIs(wGUI, GUIInterface) + self.assertTrue(issubclass(SkillGUI, GUIInterface)) + + def test_app_imports_api_client_interface(self): + from ovos_gui_api_client import GUIInterface + import ovos_workshop.app as app + self.assertIs(app.GUIInterface, GUIInterface) + + def _make_skillgui(self, bus): + from ovos_workshop.skills.ovos import SkillGUI + skill = MagicMock() + skill.skill_id = "test.skill" + skill.bus = bus + skill.config_core = {"gui": {}} + return SkillGUI(skill) + + def test_skillgui_constructs_without_ui_directories(self): + # regression: new GUIInterface ctor has no ui_directories param + gui = self._make_skillgui(FakeBus()) + self.assertEqual(gui.skill_id, "test.skill") + + def test_show_template_emits_page_show(self): + bus = FakeBus() + seen = [] + bus.on("gui.page.show", lambda m: seen.append(m)) + gui = self._make_skillgui(bus) + gui.show_text("hello world", "a title") + self.assertTrue(seen, "show_text should emit gui.page.show") + page_names = seen[-1].data.get("page_names") or seen[-1].data.get("page") + self.assertIn("SYSTEM_text", str(page_names)) + + def test_show_without_bus_raises(self): + # contract: a skill always has a bus; calling a template with no bus + # set is a misconfiguration and surfaces a clear error (it is NOT a + # silent no-op — the headless/no-adapter no-op guarantee lives in the + # ovos-gui service + adapter layer, not the skill-side interface). + from ovos_workshop.skills.ovos import SkillGUI + skill = MagicMock() + skill.skill_id = "test.skill" + skill.bus = None + skill.config_core = {"gui": {}} + gui = SkillGUI(skill) + with self.assertRaises(RuntimeError): + gui.show_text("needs a bus") + + +if __name__ == "__main__": + unittest.main()