Skip to content
Draft
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
21 changes: 21 additions & 0 deletions .github/workflows/ovoscope.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Ovoscope End-to-End Tests

on:
push:
branches: [dev]
pull_request:
branches: [dev]
workflow_dispatch:

jobs:
ovoscope:
uses: OpenVoiceOS/gh-automations/.github/workflows/ovoscope.yml@dev
secrets: inherit
with:
runner: "ubuntu-latest"
python_version: "3.11"
install_extras: "test"
test_path: "test/end2end/"
bus_coverage: true
bus_coverage_exclude: "^Thread-|^intents$|^skills$|^__core__$"
pr_comment: true
317 changes: 228 additions & 89 deletions ovos_gui/namespace.py

Large diffs are not rendered by default.

151 changes: 151 additions & 0 deletions ovos_gui/templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Copyright 2024 OpenVoiceOS
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""The OVOS-GUI-1 closed template vocabulary.

This module is the single source of truth for the ``SYSTEM_*`` template
vocabulary defined by the **OVOS-GUI-1** specification (§3). A render
backend styles each template once; producers may only name templates from
this closed set (§3.1).

Two recognition concerns live here:

* **The ``SYSTEM_`` prefix gate** (§3.2 / §8.3). The prefix is the
discriminator the GUI service uses to recognise a conformant template
intent. A page name that does not begin with ``SYSTEM_`` is *not* a
template of this specification; the service must not dispatch it as one
(it may still route it to a deployment-specific legacy path, §4.2).

* **Legacy ⇄ spec frame-name aliasing** (§3.1 / §8.1). Historically the
producer (``ovos-bus-client``) emitted CamelCase frame names such as
``SYSTEM_TextFrame``; the spec vocabulary uses ``SYSTEM_text``. The
service accepts **both** so the producer rename can land without
breaking the QML render path that dispatches on the legacy names. The
alias map is additive: every legacy name resolves to its spec template,
and every spec/legacy name resolves to the legacy QML resource name the
current render backends expect.
"""
from typing import Optional

#: Reserved prefix that discriminates a conformant template intent (§3.2).
SYSTEM_PREFIX = "SYSTEM_"

#: The closed GUI-1 template vocabulary (§3.4). Grows only by amendment of
#: the specification.
SYSTEM_TEMPLATES = frozenset({
# State and feedback
"SYSTEM_idle",
"SYSTEM_loading",
"SYSTEM_status",
"SYSTEM_error",
# Content primitives
"SYSTEM_text",
"SYSTEM_image",
"SYSTEM_animated_image",
"SYSTEM_list",
"SYSTEM_grid",
"SYSTEM_table",
"SYSTEM_html",
"SYSTEM_url",
# Media
"SYSTEM_audio_player",
"SYSTEM_video_player",
"SYSTEM_media_player",
# Domain cards
"SYSTEM_clock",
"SYSTEM_timer",
"SYSTEM_weather",
"SYSTEM_map",
"SYSTEM_face",
# Interactive companions
"SYSTEM_confirm",
"SYSTEM_select",
})

#: Legacy CamelCase frame names (as emitted by ``ovos-bus-client``'s GUI
#: API and shipped as QML resources) mapped to their GUI-1 spec template.
#: This lets the service accept the spec names additively — a producer may
#: emit either, and the service treats them as the same template.
LEGACY_TO_SPEC = {
"SYSTEM_TextFrame": "SYSTEM_text",
"SYSTEM_ImageFrame": "SYSTEM_image",
"SYSTEM_AnimatedImageFrame": "SYSTEM_animated_image",
"SYSTEM_HtmlFrame": "SYSTEM_html",
"SYSTEM_UrlFrame": "SYSTEM_url",
"SYSTEM_Status": "SYSTEM_status",
"SYSTEM_Loading": "SYSTEM_loading",
"SYSTEM_Face": "SYSTEM_face",
"SYSTEM_InputBox": "SYSTEM_confirm",
}

#: Spec template name -> legacy QML resource name. The current QML render
#: backends ship resources keyed by the legacy CamelCase names, so when a
#: producer emits a spec name we resolve it to the legacy resource so
#: rendering keeps working. Only the templates that have a shipped legacy
#: resource are mapped; spec templates without a legacy resource resolve
#: to themselves.
SPEC_TO_LEGACY = {spec: legacy for legacy, spec in LEGACY_TO_SPEC.items()}


def is_system_template(page_name: str) -> bool:
"""Whether ``page_name`` is recognised as a GUI-1 template intent.

A page name is a template intent if it begins with the reserved
``SYSTEM_`` prefix (§3.2). This intentionally accepts both the spec
vocabulary (``SYSTEM_text``) and the legacy frame names
(``SYSTEM_TextFrame``) — both carry the prefix. A name without the
prefix is a custom (non-spec) page and must not be dispatched as a
template.

@param page_name: candidate page name
@return: True if the name is a ``SYSTEM_*`` template intent
"""
return isinstance(page_name, str) and page_name.startswith(SYSTEM_PREFIX)


def normalize_template(page_name: str) -> str:
"""Resolve a template name to its canonical GUI-1 spec name.

Legacy CamelCase frame names are mapped to their spec equivalent; spec
names and unknown ``SYSTEM_*`` names pass through unchanged.

@param page_name: a ``SYSTEM_*`` template name (spec or legacy)
@return: the canonical spec template name where known, else the input
"""
return LEGACY_TO_SPEC.get(page_name, page_name)


def resolve_render_name(page_name: str) -> str:
"""Resolve a template name to the resource name the render backend expects.

The current QML render backends ship resources under the legacy
CamelCase names. When a producer emits a spec name (``SYSTEM_text``)
we resolve it to the legacy resource (``SYSTEM_TextFrame``) so existing
QML keeps rendering. Legacy names and names without a legacy resource
pass through unchanged.

@param page_name: a ``SYSTEM_*`` template name (spec or legacy)
@return: the render-backend resource name
"""
return SPEC_TO_LEGACY.get(page_name, page_name)


def is_known_template(page_name: str) -> Optional[str]:
"""Return the canonical spec name if ``page_name`` is in the closed set.

@param page_name: candidate template name (spec or legacy)
@return: canonical spec name if it is a known GUI-1 template, else None
"""
spec = normalize_template(page_name)
return spec if spec in SYSTEM_TEMPLATES else None
27 changes: 25 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,37 @@ classifiers = [
"License :: OSI Approved :: Apache Software License",
]
dependencies = [
"ovos_bus_client>=2.2.0a1,<3.0.0",
"ovos-spec-tools>=0.9.0a1",
"ovos_bus_client>=2.5.1a1,<3.0.0",
"ovos-spec-tools>=0.17.3a1",
"ovos-utils>=0.0.37,<1.0.0",
"ovos-config>=0.0.12,<3.0.0",
"tornado~=6.0, >=6.0.3",
"ovos-plugin-manager>=2.5.0a1,<3.0.0",
]

[project.optional-dependencies]
test = [
# ovoscope ships a pytest11 plugin and requires pytest>=8 (the
# pytest_pycollect_makemodule hook dropped 'path' in pytest 8).
"pytest>=8",
"pytest-cov>=4.1",
# ovoscope drives the in-repo OVOS-GUI-1 end-to-end conformance
# (test/end2end/test_gui1_service_e2e.py): it boots the real
# NamespaceManager + the GUIInterface producer on a bus and captures the
# gui.* wire with GUICaptureSession. The 1.0.2a1 line is the first whose
# transitive ovos-core no longer caps ovos-bus-client<2.0.0, so it resolves
# against the GUI-1 floors below. Prerelease-floor pin (>=) so pip resolves
# the prerelease with no --pre.
"ovoscope>=1.0.2a1",
# GUI-1 service-contract floors: per-session routing reads context.session
# via the bus-client 2.5.x session carrier; the SYSTEM_ template vocabulary
# lives in ovos-spec-tools; ovoscope's GUICaptureSession + the FakeBus
# legacy<->ovos.* session bridging needs ovos-utils>=0.12.0a1.
"ovos_bus_client>=2.5.1a1,<3.0.0",
"ovos-spec-tools>=0.17.3a1",
"ovos-utils>=0.12.0a1,<1.0.0",
]

[project.urls]
Homepage = "https://github.com/OpenVoiceOS/ovos-gui"

Expand Down
Empty file added test/end2end/__init__.py
Empty file.
Loading
Loading