Skip to content

feat: OVOS-GUI-1 service conformance — SYSTEM_ gate, dual frame vocab, per-session namespace stacks - #117

Closed
JarbasAl wants to merge 1 commit into
devfrom
feat/gui1-service-conformance
Closed

JarbasAl wants to merge 1 commit into
devfrom
feat/gui1-service-conformance

Conversation

@JarbasAl

@JarbasAl JarbasAl commented Jun 27, 2026 •

Copy link
Copy Markdown
Member

🤖 Auto-generated by Claude Fable 5 (claude-fable-5) via Claude Code — NOT human-reviewed. Verify before acting. The "Rework after pair review" section below was executed and its test results verified by running the suite in this session; the rest of the body predates this session and carries its original disclosure line.

Brings ovos_gui into line with the service-side MUSTs of OVOS-GUI-1
(§8.3) without disturbing the legacy QML/WebSocket render path. Scoped
deliberately to the tractable, non-breaking pieces; see Deferred below for
the parts that belong to the larger adapter/render rework (#112).

This is additive on top of current origin/dev (legacy mycroft-gui protocol);
the default single-screen behaviour is preserved exactly.

What this implements

§3.2 / §4.2 / §8.3 — SYSTEM_ prefix gate

New ovos_gui/templates.py is the single source of truth for the closed
SYSTEM_* template vocabulary (§3.4). handle_show_page recognises a template
intent by the reserved SYSTEM_ prefix. A first page_names entry that does
not begin with SYSTEM_ is recognised as not a template of this
spec and is logged as the deployment-specific legacy custom-QML path (§4.2) —
it is never dispatched as a template. Custom-QML skill pages keep rendering.

§3.1 / §8.1 — frame vocabulary (additive dual-accept)

The service now accepts both the legacy CamelCase frame names
(SYSTEM_TextFrame, SYSTEM_Status, …) and the GUI-1 spec names
(SYSTEM_text, SYSTEM_status, …). A spec name is resolved to the legacy QML
resource the current render backends ship (SYSTEM_text → SYSTEM_TextFrame),
so the producer (ovos-bus-client) can rename its emitted frames to the
spec vocabulary without breaking rendering — unblocking the rename deferred
in ovos-bus-client #238.

§4.3 / §5.1 / §8.3 — per-session namespace stacks

Display state is partitioned per session_id via a new GUISession, read from
context.session and defaulting to the reserved "default" id (OVOS-SESSION-1
§3.1). Each session owns an independent namespace stack so sessions no longer
collide. The legacy single-screen QML transport maps to the "default"
session; loaded_namespaces / active_namespaces / remove_namespace_timers
are now read/write proxies onto that default session, so bus.py
synchronize() and all existing handlers behave unchanged.

Tests

  • test/unittests/test_templates.py — closed vocab, prefix gate accept/reject,
    legacy⇄spec alias maps, render-resource resolution, reserved-name handling.
  • test/unittests/test_gui1_conformance.py — boots the real NamespaceManager
    on a FakeBus and drives gui.page.show / gui.value.set /
    gui.clear.namespace: SYSTEM_ gate, legacy/spec dual-accept, default-session
    fallback, per-session isolation, legacy-wire gating, and session eviction.
  • Existing test_namespace.py / test_gui1_service_e2e.py updated for the
    threaded session argument and the eviction behaviour.

135 tests passing locally on the final rebased/squashed commit.

Deferred — needs the adapter/render rework (coordination, not in scope here)

  • True multi-session delivery. The legacy QML/WebSocket transport is
    inherently single-screen: clients do not declare a session_id on connect,
    and bus.py broadcasts to all clients. This PR makes the service state
    fully per-session and correct, but actually delivering a non-"default"
    session only to its clients requires the adapter model (per-session_id
    client routing, §5.1). That lives in the adapter/template rework (feat: land GUI adapter/template rework (session_id-only); fix B1-B9 (supersedes #100) #112), which
    replaces the WebSocket server with opm.gui_adapter fan-out and is blocked on
    an unpublished ovos-plugin-manager + adapter plugins. Non-default sessions
    here are isolated in state and observable via the query surface, but the
    legacy transport still renders the "default" session.
  • Adapter fan-out / headless no-op / state-query surface (§6). Also owned by
    feat: land GUI adapter/template rework (session_id-only); fix B1-B9 (supersedes #100) #112.
  • Producer rename. Emitting spec frame names is a producer change in
    ovos-bus-client; this PR makes the service accept them so that rename can
    land safely.

Rework after pair review

A pair review of this PR against the competing rewrite (#112) ruled: #117
lands first
(after the fixes below), #112 is BLOCKed separately and gets
rebuilt later
on top of whatever lands here. This PR is the chosen
first-lander for OVOS-GUI-1 service conformance; #112 is not touched by this
work.

Four issues the review found were fixed and are included in the squashed
commit now on this branch:

  1. Withdrawn templates removed from the vocabulary. SYSTEM_confirm and
    SYSTEM_select are reserved by the merged GUI-1 spec (§3.4) — producers
    must not emit them and they are not (yet) part of the closed vocabulary.
    Removed both from SYSTEM_TEMPLATES, removed the SYSTEM_InputBox legacy
    alias (it pointed at a SYSTEM_confirm that no QML resource
    SYSTEM_InputBox.qml ever shipped for), and added tests asserting these
    names are unknown/undispatchable — still prefix-gated as SYSTEM_* (§3.2)
    but not a known template, i.e. an ordinary undispatchable template per §3.2,
    not an error.
  2. Legacy wire gated to the default session (cross-session corruption fix).
    The per-session namespace split still let every session's Namespace.add()
    / activate() / remove() / page-load path emit unconditionally onto the
    one global mycroft.session.list.* / mycroft.events.triggered legacy
    wire and the shared websocket clients. Executed proof from the review: a
    remote session's page could take over the on-device screen, and a
    non-default session's remove() could emit a stack position that actually
    belonged to the default session's entry. Fixed by threading the owning
    session_id into Namespace and gating every legacy-wire emission
    (add/activate/remove/load_data/unload_data/_activate_page/
    remove_pages/_add_pages's websocket push) on session_id == DEFAULT_SESSION_ID; non-default sessions keep their state but emit nothing
    on the legacy transport. Added a red-before-fix regression test
    (TestLegacyWireGatedToDefaultSession), verified via patch-revert to fail
    without the gate and pass with it, asserting both zero legacy-wire frames
    for a non-default session and that the default session's stack positions
    stay consistent after non-default activity plus a default removal.
  3. Rebased onto current origin/dev. The branch's pyproject.toml had
    drifted from dev and would have regressed the ovos-utils floor from
    0.8.5 to 0.0.37; resolved keeping dev's floor (>=0.8.5,<1.0.0) while
    keeping this branch's ovos_bus_client/ovos-spec-tools bumps (needed for
    the GUI-1 conformance features) and pytest>=8 (needed by the ovoscope
    pytest11 plugin used in the e2e tests).
  4. Reverted the system-resource cache change. The _cache_system_resources
    rewrite from rmtree+copytree to a single copytree(..., dirs_exist_ok=True) silently persists removed QML resources across
    refreshes (files present in the old cache but absent from the new source
    tree are never cleaned up) and was out of this PR's stated scope. Checked
    whether it was load-bearing for anything in this PR's test suite (it was
    motivated by a hypothetical parallel-xdist race) — this repo has no
    pytest-xdist / parallel-worker wiring anywhere in its test config or CI, so
    it wasn't protecting any test here. Reverted to dev's
    rmtree-then-copytree semantics.

Also included (from the review's list of cheap/uncontested fixes):

  1. Session eviction. A non-default GUISession is now evicted from
    NamespaceManager.sessions (its remaining remove_namespace_timers
    cancelled) once its last active namespace is removed, instead of lingering
    empty forever. The reserved "default" session is never evicted. Added
    TestSessionEviction covering eviction, non-eviction while a namespace
    remains active, and default-session exemption.

Suite arithmetic: review's baseline on the pre-rework branch was 125
passed
. This session's fixes added 4 reserved-vocabulary tests, 3
legacy-wire-gate tests, and 3 session-eviction tests (+10), for 135
passed
on the final rebased/squashed commit — verified by running the full
suite in this session.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 27, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a1b42d1-6438-45b4-a0a7-8e4e44ea63c3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gui1-service-conformance

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 27, 2026 •

Copy link
Copy Markdown
Contributor

The automated sentinels have completed their watch. 💂‍♂️

I've aggregated the results of the automated checks for this PR below.

🔨 Build Tests

Build test complete! Let's see if everything fits together. 🧩

✅ All versions pass

Python Build Install
3.10 ✅ ✅
3.11 ✅ ✅
3.12 ✅ ✅
3.13 ✅ ✅
3.14 ✅ ✅

🔌 Skill Tests (ovoscope)

Checking the skill's 'poker face' during errors. 😐

❌ 10/12 passed, 2 failed

✅ TestFrameVocabularyDualAccept — 3/3
✅ TestNamespaceLifecycle — 1/1
✅ TestPerSessionRouting — 3/3

❌ **TestProducerWireRoundTrip** — 2/3
Test Result
test_reserved_keys_are_not_namespace_session_data ❌ failed
test_every_producer_message_carries_from ✅ passed
test_producer_drives_service_over_the_bus ✅ passed

test_reserved_keys_are_not_namespace_session_data failure:

it__
    self._init_gui_file_share()
ovos_gui/namespace.py:556: in _init_gui_file_share
    self._cache_system_resources()
ovos_gui/namespace.py:1166: in _cache_system_resources
    shutil.copytree(self._system_res_dir, output_path)
/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/shutil.py:573: in copytree
    return _copytree(entries=entries, src=src, dst=dst, symlinks=symlinks,
/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/shutil.py:471: in _copytree
    os.makedirs(dst, exist_ok=dirs_exist_ok)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = '/home/runner/.cache/ovos_gui/system', mode = 511, exist_ok = False

>   ???
E   FileExistsError: [Errno 17] File exists: '/home/runner/.cache/ovos_gui/system'

<frozen os>:225: FileExistsError
❌ **TestSystemPrefixGate** — 1/2
Test Result
test_system_template_dispatched ❌ failed
test_non_system_first_page_is_legacy_not_template ✅ passed

test_system_template_dispatched failure:

alse)
            except OSError:
                is_dir = False
            else:
                if is_dir:
                    try:
                        orig_st = entry.stat(follow_symlinks=False)
                        is_dir = stat.S_ISDIR(orig_st.st_mode)
                    except OSError:
                        onerror(os.lstat, fullname, sys.exc_info())
                        continue
            if is_dir:
                try:
>                   dirfd = os.open(entry.name, os.O_RDONLY | os.O_NONBLOCK, dir_fd=topfd)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E                   FileNotFoundError: [Errno 2] No such file or directory: 'face'

/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/shutil.py:665: FileNotFoundError

🚌 Bus Coverage

Ensuring the skill's internal gears are meshing correctly on the bus. ⚙️

⚠️ Bus coverage report unavailable — check the job log.


The silent partner in your development journey 👥

JarbasAl added a commit that referenced this pull request Jun 28, 2026
Adds test/end2end/test_gui1_service_e2e.py: boots the real NamespaceManager
on a real bus (render websocket mocked) and drives the full
producer -> bus -> service path with the installed GUIInterface producer,
capturing the gui.* wire via ovoscope's GUICaptureSession. Asserts the GUI-1
clauses #117 implements end to end:

- §2.3/§4.1/§4.2 producer wire reaches the service; __from rides every
  producer message and reserved keys never become namespace session data
- §3.2/§4.2/§8.3 SYSTEM_ prefix gates a template intent; a non-SYSTEM_ first
  page is the legacy custom-page path, not a template
- §3.1/§8.1 the service dual-accepts legacy (SYSTEM_TextFrame) and spec
  (SYSTEM_text) frame names; spec names resolve to the legacy render resource
- §4.3/§5.1/§8.3 independent namespace stack per session_id; absent session
  defaults to the on-device 'default'; clear is session-scoped
- §4.3 lifecycle: gui.namespace.removed on the core bus on clear

Wires the gh-automations ovoscope.yml reusable workflow (test_path=test/end2end)
and a [test] extra pulling ovoscope>=1.0.2a1 with the GUI-1 prerelease-floor
pins (bus-client>=2.5.1a1, spec-tools>=0.17.3a1, utils>=0.12.0a1) so pip
resolves them with no --pre.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JarbasAl added a commit that referenced this pull request Jul 4, 2026
Adds test/end2end/test_gui1_service_e2e.py: boots the real NamespaceManager
on a real bus (render websocket mocked) and drives the full
producer -> bus -> service path with the installed GUIInterface producer,
capturing the gui.* wire via ovoscope's GUICaptureSession. Asserts the GUI-1
clauses #117 implements end to end:

- §2.3/§4.1/§4.2 producer wire reaches the service; __from rides every
  producer message and reserved keys never become namespace session data
- §3.2/§4.2/§8.3 SYSTEM_ prefix gates a template intent; a non-SYSTEM_ first
  page is the legacy custom-page path, not a template
- §3.1/§8.1 the service dual-accepts legacy (SYSTEM_TextFrame) and spec
  (SYSTEM_text) frame names; spec names resolve to the legacy render resource
- §4.3/§5.1/§8.3 independent namespace stack per session_id; absent session
  defaults to the on-device 'default'; clear is session-scoped
- §4.3 lifecycle: gui.namespace.removed on the core bus on clear

Wires the gh-automations ovoscope.yml reusable workflow (test_path=test/end2end)
and a [test] extra pulling ovoscope>=1.0.2a1 with the GUI-1 prerelease-floor
pins (bus-client>=2.5.1a1, spec-tools>=0.17.3a1, utils>=0.12.0a1) so pip
resolves them with no --pre.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@JarbasAl
JarbasAl force-pushed the feat/gui1-service-conformance branch from 8ebd8b3 to 4c773e0 Compare July 4, 2026 00:37
@JarbasAl
JarbasAl marked this pull request as ready for review July 16, 2026 07:21
@JarbasAl
JarbasAl marked this pull request as draft July 24, 2026 00:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant