Skip to content

feat: Bus coverage + media testing#49

Merged
JarbasAl merged 17 commits into
devfrom
bus_coverage
Mar 13, 2026
Merged

feat: Bus coverage + media testing#49
JarbasAl merged 17 commits into
devfrom
bus_coverage

Conversation

@JarbasAl

@JarbasAl JarbasAl commented Mar 12, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Bus coverage reporting for tests (CLI, pytest integration, per-test opt-in, aggregated printable reports)
    • Media testing harness for OCP: mock backend, player harness, capture session
    • GUI testing: assert_namespace_has_key for GUI namespace assertions
  • Documentation

    • Added/updated bus-coverage, media-testing, GUI testing docs, FAQ, SKILL, SUGGESTIONS, and docs index
  • Tests

    • Extensive unit tests for bus coverage, media harness, remote recorder, GUI capture, and global coverage
  • Chores

    • CI/workflow updates and project metadata tweaks (pyproject, packaging, tooling)

@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Bus Coverage feature (tracker, report models, pytest integration, CLI, docs, and tests), a media testing harness, GUI assertion helper, pipeline/diff enhancements, CI workflow updates, many documentation/maintenance artifacts, and extensive unit tests. No breaking removals; several public APIs extended. (50 words)

Changes

Cohort / File(s) Summary
Bus Coverage Core
ovoscope/bus_coverage.py, ovoscope/__init__.py
New BusCoverageTracker, BusCoverageReport, SkillBusCoverage, HandlerEntry, EmitterEntry; End2EndTest gains track_bus_coverage, print_bus_coverage, bus_coverage_report; global collector and FakeBus instrumentation; start/stop/record/report integration.
Pytest Integration
ovoscope/pytest_plugin.py
Adds BusCoverageCollector, pytest_addoption flags, session-scoped bus_coverage_session fixture, autouse behavior to merge per-test reports, and pytest_terminal_summary hook.
CLI
ovoscope/cli.py
Adds cmd_bus_coverage subcommand (run fixtures with coverage, aggregate/filter, output table/json). Patch contains duplicate definitions noted.
Docs & Guides
docs/bus-coverage.md, docs/index.md, FAQ.md, MAINTENANCE_REPORT.md, AUDIT.md, SUGGESTIONS.md, QUICK_FACTS.md, docs/media-testing.md, docs/pipeline.md, docs/ocp.md, SKILL.md, docs/gui-testing.md
New and updated docs describing bus coverage concepts, CLI usage, limitations, media-testing harness, pipeline notes, GUI testing API (assert_namespace_has_key), audits, suggestions, and index updates.
Unit Tests
test/unittests/test_bus_coverage.py, test/unittests/test_media.py, test/unittests/test_remote_recorder.py, test/unittests/test_phal.py, test/unittests/test_gui_capture.py, test/unittests/test_global_bus_coverage.py
Large suite of new tests validating bus coverage dataclasses/tracker, global collector, media harness, remote recorder, GUI capture assertions, and minor import path update.
Media Harness
ovoscope/media.py
New MockOCPBackend, OCPPlayerHarness, and OCPCaptureSession for testing OCP media playback flows and capturing media-related bus messages.
Pipeline & Diff
ovoscope/pipeline.py, ovoscope/diff.py
PipelineHarness.match reworked to use separate success/failure events and a watcher thread; diff functions gain strict mode and updated signatures to report extra keys.
Coverage / CI Workflows
.github/workflows/*.yml
Adds/updates workflows (build-tests, coverage, lint), renames/adjusts coverage pages workflow, and removes older workflow files; various YAML input/permission tweaks.
Packaging / Config
pyproject.toml
Adds pinned ovos-core>=2.0.4a2, splits dev deps, adds package-data and pytest timeout, and updates project URLs.
Misc Maintenance
AUDIT.md, MAINTENANCE_REPORT.md, SUGGESTIONS.md, QUICK_FACTS.md
New audit/maintenance/suggestions artifacts and quick facts update reflecting test count changes.

Sequence Diagram

sequenceDiagram
    participant Test as End2EndTest
    participant Tracker as BusCoverageTracker
    participant Bus as FakeBus
    participant Capture as CaptureSession
    participant Report as BusCoverageReport

    Note right of Test: pre-capture
    Test->>Tracker: snapshot_listeners()
    Test->>Tracker: start_tracking()
    Tracker->>Bus: monkey-patch emit/register hooks
    Capture->>Bus: emits & handler invocations during capture
    Bus->>Tracker: intercepted registrations & emissions
    Test->>Tracker: stop_tracking()
    Tracker->>Bus: restore original emit/register
    Test->>Tracker: record_session(responses, expected_messages)
    Tracker->>Tracker: attribute events to skills
    Tracker->>Report: build_report()
    Report->>Test: assign bus_coverage_report / optional print
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰
I hopped through bus events, quick and spry,
Counting handlers as they wandered by,
Each skill and emitter I file with care,
So no message hops off into thin air! 🎩✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main features added in this pull request: bus coverage tracking and media testing support, which are the primary contributions evident throughout the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bus_coverage
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Comment thread test/unittests/test_setup_skill.py Fixed
JarbasAl and others added 8 commits March 12, 2026 02:16
Single command after `pip install ovoscope` installs the skill/agent
definitions into Claude Code, Gemini CLI, and OpenCode.

- ovoscope/setup_skill.py: install_claude(), install_gemini(),
  install_opencode(), uninstall_*(), detect_tools(), main()
- ovoscope/skill_data/claude/: SKILL.md + scripts/ovoscope.sh + assets/docs/
- ovoscope/skill_data/gemini/: same format, project-level install
- ovoscope/opencode/ovoscope.md: YAML frontmatter agent for OpenCode
- pyproject.toml: ovoscope-setup entrypoint + package-data for skill_data/
- 26 unit tests (test_setup_skill.py)

Usage:
  ovoscope-setup           # auto-detect all tools on PATH
  ovoscope-setup --claude  # Claude only (~/.claude/skills/ovoscope/)
  ovoscope-setup --gemini --path /my/workspace
  ovoscope-setup --opencode --path /my/project
  ovoscope-setup --list    # show detected tools
  ovoscope-setup --uninstall --claude

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove all docs from skill_data/ — only SKILL.md + scripts/ are bundled
- Remove OpenCode support
- Add download_docs() using urllib.request (no new deps) to fetch docs
  from raw.githubusercontent.com at install time
- Add --no-docs flag for offline/CI use
- 32 unit tests, all network calls mocked

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…itHub

- Remove bundled skill_data/ directory entirely; nothing is shipped in
  the wheel beyond setup_skill.py itself
- SKILL.md is now downloaded from raw.githubusercontent.com at install
  time, keeping installed skills up-to-date with the live repo
- Wrapper script (ovoscope.sh) generated inline — no local template needed
- Shared _install_skill() helper used by both Claude and Gemini paths
- Remove opencode support (out of scope)
- Add --no-docs flag to skip docs fetch for offline / CI installs
- Drop [tool.setuptools.package-data] from pyproject.toml (no bundled data)
- Update unit tests: mock _fetch for SKILL.md download, remove
  TestSkillDataDir (no longer relevant), add test_skill_md_fetched_from_github

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds two-dimensional bus coverage measurement to ovoscope:
- Listener coverage: which bus.on() registrations were invoked per test
- Emitter coverage: which message types were observed vs asserted

Key changes:
- ovoscope/bus_coverage.py: BusCoverageTracker, BusCoverageReport,
  SkillBusCoverage, HandlerEntry, EmitterEntry; handles pyee v9
  OrderedDict handler storage; attributes handlers via __self__ to
  minicroft.plugin_skills
- ovoscope/__init__.py: track_bus_coverage / print_bus_coverage /
  bus_coverage_report fields on End2EndTest; hooks tracker in execute()
- ovoscope/pytest_plugin.py: BusCoverageCollector, bus_coverage_session
  fixture, pytest_terminal_summary hook
- ovoscope/cli.py: bus-coverage subcommand (table/json/verbose output)
- docs/bus-coverage.md: full API reference with source citations
- test/unittests/test_bus_coverage.py: 32 new tests (all passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OVOS wraps all skill handlers in create_wrapper closures before
registering on FakeBus, so handler.__self__ never points directly
to the skill instance.

Fix snapshot_listeners() to use the primary path:
  skill.events.events (EventContainer) — the authoritative list of
  (msg_type, handler) pairs maintained by ovos-workshop.

Fallback path: bus introspection + closure scanning for __self__
(used when EventContainer is absent, e.g. injected test skills).

Also:
- _skill_instance_map() unwraps PluginSkillLoader.instance
- Added _skill_id_from_closure() helper
- Updated snapshot tests to use _FakeEventContainer stub

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously snapshot_listeners() only attributed handlers to loaded
plugin skills. Now it covers every component that registers handlers
on FakeBus — pipelines, core services, and the skill manager itself.

Attribution strategy (three passes):
1. Skills with EventContainer: read skill.events.events directly
   (authoritative, handles create_wrapper closures)
2. Direct __self__ handlers: use type(owner).__name__ as component
   label; prefer owner.skill_id / owner.name attributes; look up
   in combined id→name map so skill instances without EventContainer
   still resolve to their skill_id
3. Closure scan: for handlers with no __self__, walk __closure__
   cells for bound methods; uses the same combined map

New component labels visible in reports:
  IntentService, AdaptPipeline, PadaciosoPipeline, PadatiousPipeline,
  ConverseService, FallbackService, StopService, MiniCroft (Thread-1)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents 4 critical, 7 major, 3 minor, 1 nitpick issues found in the
bus coverage implementation:
- async_responses excluded from emitter coverage
- unattributed expected_messages silently dropped (no __core__ fallback)
- registration-time handlers always show NOT TESTED (misleading)
- non-skill messages excluded from emitter coverage (skill_id missing)
- dead skill_map variable in record_session()
- triple _get_bus_events() call in snapshot_listeners()
- double-stop risk in cmd_bus_coverage()
- pytest terminal hook uses private _arg2fixturedefs / cached_result
- once() handlers invisible after firing before snapshot
- ignore_messages silently excluded from emitter coverage
- no JSON schema version field
- docs gaps (async_responses, registration-time, pipeline, ignore_messages)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Critical fixes:
- C1: pass capture.async_responses to record_session() so async messages
  are included in emitter coverage (__init__.py:666)
- C2/C4: messages with no skill_id in context now attributed to __core__
  bucket instead of silently dropped (bus_coverage.py:record_session)

Major fixes:
- M1: remove dead skill_map variable in record_session()
- M2: cache _get_bus_events() once in snapshot_listeners() — was called 3x
- M3: set test.managed=False before execute() in cli.py to prevent
  double-stop; removed redundant mc.stop() in except branch
- M4: replace private pytest internals (_arg2fixturedefs, cached_result)
  with config._bus_coverage_reports list populated in fixture teardown
- M7: add "schema_version": "1" to to_json() output

Minor/nitpick fixes:
- Dynamic column widths in print_report() using max skill_id length
- Move print_bus_coverage to after all assertions in execute()
- bus_coverage_report type hint changed to Optional["BusCoverageReport"]
- Pass 2 now uses isinstance(owner, type) check instead of "type" string

Documentation:
- docs/bus-coverage.md Limitations section updated with all missing items:
  registration-time handlers, once() handlers, pipeline matching,
  ignore_messages exclusion, async_responses inclusion, __core__ bucket
- AUDIT.md: all fixed items marked as resolved

Tests:
- 4 new tests: TestCoreAttributionBucket (3) + TestJsonSchemaVersion (1)
- 37 tests total in test_bus_coverage.py, all pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Mar 12, 2026

Copy link
Copy Markdown

Checking the status... all automated tasks are done! ✅

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

📋 Repo Health

I've checked the repo's eyesight (aka observability). 👓

✅ All required files present.

Latest Version: 0.12.0a1

ovoscope/version.py — Version file
README.md — README
LICENSE — License file
pyproject.toml — pyproject.toml
CHANGELOG.md — Changelog
⚠️ requirements.txt — Requirements
ovoscope/version.py has valid version block markers

🏷️ Release Preview

Get ready for the next big release! 🚀

Current: 0.12.0a1Next: 0.13.0a1

Signal Value
Label feature
PR title feat: Bus coverage + media testing
Bump minor

✅ PR title follows conventional commit format.


🚀 Release Channel Compatibility

Predicted next version: 0.13.0a1

Channel Status Note Current Constraint
Stable Not in channel -
Testing Compatible ovoscope>=0.7.2,<1.0.0
Alpha Compatible ovoscope>=0.12.0a1

🔒 Security (pip-audit)

The security scan is now complete. 🏁

✅ No known vulnerabilities found (68 packages scanned).

⚖️ License Check

Ensuring our project is well-protected legally. 🛡️

✅ No license violations found (60 packages).

License distribution: 14× MIT, 14× MIT License, 8× Apache Software License, 6× Apache-2.0, 4× BSD-3-Clause, 2× ISC License (ISCL), 2× PSF-2.0, 2× Python Software Foundation License, +7 more

Full breakdown — 60 packages
Package Version License URL
annotated-doc 0.0.4 MIT link
annotated-types 0.7.0 MIT License link
anyio 4.12.1 MIT link
audioop-lts 0.2.2 PSF-2.0 link
build 1.4.0 MIT link
certifi 2026.2.25 Mozilla Public License 2.0 (MPL 2.0) link
charset-normalizer 3.4.5 MIT link
click 8.3.1 BSD-3-Clause link
combo_lock 0.3.0 Apache Software License link
fastapi 0.135.1 MIT link
filelock 3.25.2 MIT link
h11 0.16.0 MIT License link
idna 3.11 BSD-3-Clause link
importlib_metadata 8.7.1 Apache-2.0 link
json-database 0.10.1 MIT link
kthread 0.2.3 MIT License link
langcodes 3.5.1 MIT License link
markdown-it-py 4.0.0 MIT License link
mdurl 0.1.2 MIT License link
memory-tempfile 2.2.3 MIT License link
ovos-config 2.1.1 Apache-2.0 link
ovos-core 2.1.4a1 Apache-2.0 link
ovos-number-parser 0.5.1 Apache Software License link
ovos-plugin-manager 2.2.0 Apache-2.0 link
ovos-pydantic-models 0.1.1 Apache-2.0 link
ovos-solver-yes-no-plugin 0.2.8 MIT link
ovos-utils 0.8.5 Apache-2.0 link
ovos_bus_client 1.5.0 Apache Software License link
ovos_workshop 8.0.0 apache-2.0 link
ovoscope 0.12.0a1 Apache Software License link
packaging 26.0 Apache-2.0 OR BSD-2-Clause link
padacioso 1.0.0 apache-2.0 link
pexpect 4.9.0 ISC License (ISCL) link
ptyprocess 0.7.0 ISC License (ISCL) link
pydantic 2.12.5 MIT link
pydantic_core 2.41.5 MIT link
pyee 12.1.1 MIT License link
Pygments 2.19.2 BSD License link
pyproject_hooks 1.2.0 MIT License link
python-dateutil 2.9.0.post0 Apache Software License; BSD License link
PyYAML 6.0.3 MIT License link
quebra-frases 0.3.7 Apache Software License link
RapidFuzz 3.14.3 MIT link
regex 2026.2.28 Apache-2.0 AND CNRI-Python link
requests 2.32.5 Apache Software License link
rich 13.9.4 MIT License link
rich-click 1.9.7 MIT License

Copyright (c) 2022 Phil Ewels

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
| link |
| simplematch | 1.4 | MIT License | link |
| six | 1.17.0 | MIT License | link |
| standard-aifc | 3.13.0 | Python Software Foundation License | link |
| standard-chunk | 3.13.0 | Python Software Foundation License | link |
| starlette | 0.52.1 | BSD-3-Clause | link |
| typing-inspection | 0.4.2 | MIT | link |
| typing_extensions | 4.15.0 | PSF-2.0 | link |
| unicode-rbnf | 2.4.0 | MIT License | |
| urllib3 | 2.6.3 | MIT | link |
| uvicorn | 0.41.0 | BSD-3-Clause | link |
| watchdog | 2.3.1 | Apache Software License | link |
| websocket-client | 1.9.0 | Apache Software License | link |
| zipp | 3.23.0 | MIT | link |

Policy: Apache 2.0 (universal donor). StrongCopyleft / NetworkCopyleft / WeakCopyleft / Other / Error categories fail. MPL allowed.

🔨 Build Tests

The build bots are giving this a thumbs up. 👍

✅ All versions pass

Python Build Install Tests
3.10
3.11
3.12
3.13
3.14

🔍 Lint

The automated gnomes have finished their shift. 🍄

ruff: issues found — see job log

📊 Coverage

How well do we know our own code? 🧠

57.9% total coverage

Files below 80% coverage (10 files)
File Coverage Missing lines
ovoscope/ocp.py 0.0% 83
ovoscope/pipeline.py 0.0% 95
ovoscope/version.py 0.0% 5
ovoscope/pytest_plugin.py 22.3% 87
ovoscope/cli.py 32.8% 162
ovoscope/listener.py 38.9% 162
ovoscope/media.py 57.9% 90
ovoscope/__init__.py 60.1% 288
ovoscope/audio.py 63.1% 111
ovoscope/coverage.py 63.3% 69

Full report: download the coverage-report artifact.


Beep boop. See you in the next PR! 👋

@github-actions github-actions Bot added feature and removed feature labels Mar 12, 2026
…ntion

Added (new):
- build-tests.yml — build+test matrix Python 3.10–3.14, audio+pydantic extras
- coverage.yml — PR coverage report (replaces unit_tests.yml)
- coverage-pages.yml — push HTML coverage to gh-pages on dev push
- lint.yml — ruff check on PRs
- release-preview.yml — version bump preview on PR
- repo-health.yml — required files/version block checks

Removed (replaced or deprecated):
- build_tests.yml → build-tests.yml (renamed + fixed test path)
- unit_tests.yml → superseded by coverage.yml
- coverage_pages.yml → coverage-pages.yml (updated to use coverage.yml@dev
  with deploy_pages: true; was using deprecated coverage-pages.yml@dev)
- release_preview.yml → release-preview.yml
- repo_health.yml → repo-health.yml
- python-support.yml → deprecated; covered by build-tests.yml
- skill-check.yml / ovoscope.yml → removed (ovoscope is a framework, not a skill)
- conventional-label.yaml → conventional-label.yml (extension normalized)

Renamed to hyphen convention (no content change):
- downstream_check.yml → downstream-check.yml
- license_check.yml → license-check.yml
- pip_audit.yml → pip-audit.yml
- publish_stable.yml → publish-stable.yml
- release_workflow.yml → release-workflow.yml

All workflows use OpenVoiceOS/gh-automations@dev refs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ovoscope/__init__.py (1)

642-669: ⚠️ Potential issue | 🟡 Minor

Reset bus_coverage_report before each execution.

If the same End2EndTest instance is reused, a prior coverage report survives runs where tracking is disabled and can still be printed on Lines 795-796. Clear the field up front so it always reflects the current execution only.

Suggested fix
 def execute(self, timeout: int = 30) -> List[Message]:
+        self.bus_coverage_report = None
         if self.minicroft is None:
             self.minicroft = get_minicroft(self.skill_ids)
             self.managed = True

Also applies to: 795-796

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovoscope/__init__.py` around lines 642 - 669, Reset self.bus_coverage_report
to None at the start of the run so prior reports don't survive when
track_bus_coverage is false; specifically, add self.bus_coverage_report = None
just before the block that checks self.track_bus_coverage (where _bus_tracker is
initialized) and ensure any later printing paths (the code that references
self.bus_coverage_report) see only the fresh value for the current execution.
This change should be applied around the End2EndTest execution entry point that
contains the _bus_tracker logic to ensure every run clears the previous report.
🧹 Nitpick comments (3)
ovoscope/bus_coverage.py (1)

665-665: Consider moving collections import to module level.

The import collections inside _iter_handlers() is executed on every call. Since this is a static method that may be called frequently during listener introspection, moving the import to the module level would be slightly more efficient.

♻️ Suggested refactor
 from __future__ import annotations
 
+import collections
 import dataclasses
 import json
 from typing import Any, Dict, List, Optional

Then remove line 665.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovoscope/bus_coverage.py` at line 665, The import of collections inside the
static method _iter_handlers causes repeated imports on every call; move the
import statement (import collections) to the module level (top of
ovoscope/bus_coverage.py) and remove the inline import from the _iter_handlers
method so the function uses the module-level collections reference instead.
ovoscope/cli.py (1)

366-416: Consider extracting shared report-merging logic.

The merging logic here (lines 366-416) is nearly identical to BusCoverageCollector.merged_report() in pytest_plugin.py (lines 133-188). Both merge by skill_id, sum invocation counts, and construct the same data structures.

Consider extracting a shared merge_reports(reports: List[BusCoverageReport]) -> BusCoverageReport function in bus_coverage.py to eliminate duplication and ensure consistent behavior.

♻️ Suggested approach

Add to ovoscope/bus_coverage.py:

def merge_reports(reports: List[BusCoverageReport]) -> BusCoverageReport:
    """Merge multiple BusCoverageReport instances into one.
    
    Sums invocation counts, observed counts, and asserted counts
    across all reports, grouped by skill_id.
    """
    # ... extracted merging logic ...

Then both cli.py and pytest_plugin.py can call:

from ovoscope.bus_coverage import merge_reports
final_report = merge_reports(collected_reports)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovoscope/cli.py` around lines 366 - 416, The merging logic duplicated in
ovoscope/cli.py (the block that builds
merged_listeners/merged_observed/merged_asserted and constructs
SkillBusCoverage/HandlerEntry/EmitterEntry into a BusCoverageReport) should be
extracted into a single function merge_reports(reports: List[BusCoverageReport])
-> BusCoverageReport in ovoscope/bus_coverage.py; implement the same aggregation
(sum invocation_count, observed_count, asserted_count grouped by skill_id and
msg_type) and return a BusCoverageReport, then replace the inline code in cli.py
and the BusCoverageCollector.merged_report() in pytest_plugin.py to call
merge_reports(reports) to remove duplication and keep behavior consistent.
ovoscope/__init__.py (1)

587-589: Use TYPE_CHECKING import for forward-reference best practice (optional improvement).

Line 589 uses a string forward reference for BusCoverageReport. While this is valid Python and will not trigger linting errors, following the TYPE_CHECKING pattern is a best practice for consistency with the rest of the codebase where external types are explicitly imported.

Suggested improvement
+from typing import Union, List, Dict, Any, Optional, TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from ovoscope.bus_coverage import BusCoverageReport
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovoscope/__init__.py` around lines 587 - 589, Replace the string
forward-reference for BusCoverageReport on the bus_coverage_report field with an
actual type import using the TYPE_CHECKING pattern: add "from typing import
TYPE_CHECKING" and inside "if TYPE_CHECKING:" import BusCoverageReport, then
change the annotation on bus_coverage_report to Optional[BusCoverageReport] (no
quotes); keep the dataclasses.field(...) setup unchanged and ensure
typing.Optional is already imported or imported if not.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@ovoscope/__init__.py`:
- Around line 642-669: Reset self.bus_coverage_report to None at the start of
the run so prior reports don't survive when track_bus_coverage is false;
specifically, add self.bus_coverage_report = None just before the block that
checks self.track_bus_coverage (where _bus_tracker is initialized) and ensure
any later printing paths (the code that references self.bus_coverage_report) see
only the fresh value for the current execution. This change should be applied
around the End2EndTest execution entry point that contains the _bus_tracker
logic to ensure every run clears the previous report.

---

Nitpick comments:
In `@ovoscope/__init__.py`:
- Around line 587-589: Replace the string forward-reference for
BusCoverageReport on the bus_coverage_report field with an actual type import
using the TYPE_CHECKING pattern: add "from typing import TYPE_CHECKING" and
inside "if TYPE_CHECKING:" import BusCoverageReport, then change the annotation
on bus_coverage_report to Optional[BusCoverageReport] (no quotes); keep the
dataclasses.field(...) setup unchanged and ensure typing.Optional is already
imported or imported if not.

In `@ovoscope/bus_coverage.py`:
- Line 665: The import of collections inside the static method _iter_handlers
causes repeated imports on every call; move the import statement (import
collections) to the module level (top of ovoscope/bus_coverage.py) and remove
the inline import from the _iter_handlers method so the function uses the
module-level collections reference instead.

In `@ovoscope/cli.py`:
- Around line 366-416: The merging logic duplicated in ovoscope/cli.py (the
block that builds merged_listeners/merged_observed/merged_asserted and
constructs SkillBusCoverage/HandlerEntry/EmitterEntry into a BusCoverageReport)
should be extracted into a single function merge_reports(reports:
List[BusCoverageReport]) -> BusCoverageReport in ovoscope/bus_coverage.py;
implement the same aggregation (sum invocation_count, observed_count,
asserted_count grouped by skill_id and msg_type) and return a BusCoverageReport,
then replace the inline code in cli.py and the
BusCoverageCollector.merged_report() in pytest_plugin.py to call
merge_reports(reports) to remove duplication and keep behavior consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fcc9a5d8-4675-443c-8fc7-12ecb94b9242

📥 Commits

Reviewing files that changed from the base of the PR and between 98d3b60 and 15a6925.

📒 Files selected for processing (10)
  • AUDIT.md
  • FAQ.md
  • MAINTENANCE_REPORT.md
  • docs/bus-coverage.md
  • docs/index.md
  • ovoscope/__init__.py
  • ovoscope/bus_coverage.py
  • ovoscope/cli.py
  • ovoscope/pytest_plugin.py
  • test/unittests/test_bus_coverage.py

JarbasAl and others added 4 commits March 12, 2026 05:25
… E2E tests

Adds ovoscope/media.py with:
- MockOCPBackend: no-op AudioBackend tracking state + simulate_end/invalid_stream helpers
- OCPPlayerHarness: context manager wrapping real OCPMediaPlayer on FakeBus with
  all heavy deps mocked (AudioService, VideoService, WebService, MPRIS, GUI, Config)
- OCPCaptureSession: bus message capture for asserting OCP message sequences
- _TolerantPlaylist patch to handle Playlist("title") incompatibility in installed ovos_utils

Adds docs/media-testing.md mirroring audio-testing.md structure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rk distinction

OCPPlayerHarness:
- cork(): emits ovos.common_play.cork → handle_cork_request (pauses player)
- uncork(): emits ovos.common_play.uncork → handle_uncork_request (resumes if _paused_on_duck)
- Expanded docstrings for duck()/unduck() clarifying the PAUSED guard in
  handle_unduck_request (ovos_media/player.py:1228) and when restore_volume
  is and is not called

docs/media-testing.md:
- New "Duck / Unduck vs Cork / Uncork" section with side-by-side comparison,
  bus message tables, code examples, and the record_end auto-uncork pattern
- Updated API reference table with cork/uncork entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
handle_unduck_request no longer requires state==PAUSED — restore_volume is
now called whenever _paused_on_duck is True, covering the PLAYING (duck)
path correctly.

Remove the "design note" warning about the PAUSED guard and update the
code example + API table to show the corrected behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ckaging

- Fix pipeline.py race condition and timeout detection in match()
- Add strict mode to diff.py _dict_diff() for extra key detection
- Remove dead _skill_id_for_handler() from bus_coverage.py
- Add test_media.py and test_remote_recorder.py (P2 coverage)
- Fix deprecated Message import in test_phal.py
- Create SUGGESTIONS.md (required by AGENTS.md)
- Update QUICK_FACTS.md test count (243→348)
- Expand docs/pipeline.md to full API reference
- Fix docs/ocp.md OCPTest class name and add OCPPlayerHarness cross-ref
- Update AUDIT.md with new findings
- Add pyproject.toml URLs, package-data, pytest timeout
- Make coverage.py fixture search recursive with Path.rglob
- Add proper type annotations to pytest_plugin.py _reports
- 348 unit tests pass (was 301)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines +10 to +15
uses: OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev
secrets: inherit
with:
python_versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]'
install_extras: 'audio,pydantic'
test_path: 'test/unittests/'

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {}

Copilot Autofix

AI 4 months ago

In general, the fix is to add an explicit permissions: block to the workflow (either at the root or under the affected job) that grants only the minimal scopes required. For a typical build-and-test workflow that just checks out code and runs tests, contents: read is sufficient; if the reusable workflow needs more (e.g., to write checks or pull request comments), additional scopes could be added, but we cannot infer that from the snippet, so we start with a minimal, safe configuration.

The best, least intrusive fix here is to add a top-level permissions: block (aligned with on: and jobs:) in .github/workflows/build-tests.yml, setting contents: read. This applies to all jobs that don’t override permissions, including the build job using the reusable workflow, and does not change any other behavior of the workflow. Concretely, we should insert:

permissions:
  contents: read

after the on: block (e.g., after line 6) or before jobs:. No new imports or additional definitions are required because this is just YAML configuration for GitHub Actions.

Suggested changeset 1
.github/workflows/build-tests.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/build-tests.yml b/.github/workflows/build-tests.yml
--- a/.github/workflows/build-tests.yml
+++ b/.github/workflows/build-tests.yml
@@ -5,6 +5,9 @@
     branches: [dev, master, main]
   workflow_dispatch:
 
+permissions:
+  contents: read
+
 jobs:
   build:
     uses: OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev
EOF
@@ -5,6 +5,9 @@
branches: [dev, master, main]
workflow_dispatch:

permissions:
contents: read

jobs:
build:
uses: OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +10 to +17
uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev
secrets: inherit
with:
python_version: '3.11'
coverage_source: 'ovoscope'
test_path: 'test/unittests/'
install_extras: 'audio,pydantic'
min_coverage: 0

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {}

Copilot Autofix

AI 4 months ago

In general, to fix this class of issue you add a permissions block either at the top level of the workflow (so it applies to all jobs without their own permissions) or within the specific job. The block should grant only the minimum access needed, typically contents: read for jobs that just check out and read code or run tests.

For this specific workflow, the best fix with minimal behavioral change is to define explicit read-only permissions at the workflow level, directly under name: or on:. Since this workflow delegates to a reusable workflow to run coverage, and there is no evidence that it needs any write access (no steps are shown that push commits, create releases, comment on PRs, etc.), we can safely restrict permissions to contents: read. If the reusable workflow needs additional scopes, they can be added later in that reusable workflow, but here we should at least limit the default token. Concretely, edit .github/workflows/coverage.yml to insert:

permissions:
  contents: read

near the top (e.g., after name: Code Coverage), leaving the rest of the workflow untouched.

Suggested changeset 1
.github/workflows/coverage.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -1,5 +1,8 @@
 name: Code Coverage
 
+permissions:
+  contents: read
+
 on:
   pull_request:
     branches: [dev]
EOF
@@ -1,5 +1,8 @@
name: Code Coverage

permissions:
contents: read

on:
pull_request:
branches: [dev]
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +10 to +14
uses: OpenVoiceOS/gh-automations/.github/workflows/lint.yml@dev
secrets: inherit
with:
ruff: true
pre_commit: false # set true if .pre-commit-config.yaml exists

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {}

Copilot Autofix

AI 4 months ago

In general, the fix is to explicitly define a permissions block that grants the minimal GITHUB_TOKEN scopes required for this lint job, either at the workflow root (for all jobs) or under the specific job (lint). Since this workflow only delegates to a reusable lint workflow, the host workflow itself likely only needs read access to the repository contents.

The least intrusive and clearest fix is to add a permissions block at the job level for lint, directly under the lint: job configuration, alongside uses, secrets, and with. For a linting workflow that merely reads code, contents: read is usually sufficient; if the reusable workflow needs additional scopes (e.g., to write PR comments), those can be added there as well, but given the information provided, we will use a conservative contents: read. Concretely, in .github/workflows/lint.yml, after line 9 (lint:) and before line 10 (uses: OpenVoiceOS/...), insert:

    permissions:
      contents: read

This ensures the lint job (and thus the GITHUB_TOKEN available to the reusable workflow) is limited to read-only access to repository contents, satisfying the CodeQL recommendation without changing the existing behavior of the linting logic.

Suggested changeset 1
.github/workflows/lint.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -7,6 +7,8 @@
 
 jobs:
   lint:
+    permissions:
+      contents: read
     uses: OpenVoiceOS/gh-automations/.github/workflows/lint.yml@dev
     secrets: inherit
     with:
EOF
@@ -7,6 +7,8 @@

jobs:
lint:
permissions:
contents: read
uses: OpenVoiceOS/gh-automations/.github/workflows/lint.yml@dev
secrets: inherit
with:
Copilot is powered by AI and may make mistakes. Always verify output.
@github-actions github-actions Bot added feature and removed feature labels Mar 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (3)
.github/workflows/coverage.yml (1)

17-17: Consider enforcing a non-zero minimum coverage.

min_coverage: 0 removes the gating value of this workflow. Even a low baseline helps prevent regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/coverage.yml at line 17, The workflow currently sets
min_coverage: 0 which disables gating; change the min_coverage value from 0 to a
non-zero threshold (e.g., 70 or your chosen baseline) in the workflow config so
the coverage job fails when coverage drops below the baseline; update the
min_coverage field and, if needed, document or expose it as a workflow
input/variable so teams can adjust the threshold over time.
ovoscope/media.py (1)

385-494: Replace fixed sleeps with condition-based waits.

Every control helper sleeps 50 ms after emitting. That makes the harness both slower and flaky: synchronous paths always pay the delay, while slower CI can still read stale player/backend state after 50 ms. Waiting for a concrete bus event or state predicate will make these helpers deterministic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovoscope/media.py` around lines 385 - 494, The helper methods (play, pause,
resume, stop, next_track, prev_track, duck, unduck, cork, uncork,
simulate_track_end, simulate_invalid_stream) currently use fixed
time.sleep(0.05) which makes tests flaky; replace those sleeps with
deterministic waits: after emitting the Message, wait for a concrete bus event
or state predicate that indicates the action completed (e.g. use
self.bus.wait_for(...) for the appropriate OCP event like ovos.common_play.state
or recognizer_loop events, or poll a PlayerState/backend property until it
reflects the expected change), include a sensible timeout and clear fallback to
avoid hanging, and apply this change consistently across all listed methods.
test/unittests/test_media.py (1)

14-24: Add at least one OCPPlayerHarness smoke test.

This module is positioned as the unit-test home for the new media harnesses, but it never imports or exercises OCPPlayerHarness. The patch-heavy __enter__/__exit__ path is the riskiest part of ovoscope.media, so a simple context/open/close test would buy much more confidence than another backend-state assertion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_media.py` around lines 14 - 24, Add a simple smoke test
that imports OCPPlayerHarness and exercises its context manager and lifecycle
methods: create a MockOCPBackend (or MagicMock backend) and a FakeBus/Message
stub, instantiate OCPPlayerHarness with those dependencies, then use a with
OCPPlayerHarness(...) as harness: block and call harness.open() and
harness.close() (or just ensure exit runs without error) to validate
__enter__/__exit__ paths; name the test like
test_ocpplayerharness_context_manager and place it in test_media.py alongside
existing tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/build-tests.yml:
- Around line 3-15: Add an explicit top-level permissions block for the
GITHUB_TOKEN to avoid relying on default scopes; because this workflow runs on
pull_request and delegates to the reusable job referenced by uses:
OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev (job: build),
add a permissions: mapping that grants only the least-privilege scopes required
(for example, contents: read and any other specific scopes you need such as
pull-requests: write or id-token: write) instead of the defaults so the token
exposure is minimized.
- Line 10: The workflow currently references the reusable workflow using a
mutable ref "OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev";
update that to an immutable commit SHA (replace "@dev" with the specific commit
hash) so the workflow is pinned and reproducible. Locate the line that calls the
reusable workflow (the "uses:
OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev" entry) and
replace the ref with the full commit SHA from the target repository, commit the
change, and verify the workflow still executes as expected.

In @.github/workflows/coverage-pages.yml:
- Line 13: The workflow references a mutable branch tag in the reusable workflow
declaration ("uses:
OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev"); replace the
branch ref with a specific commit SHA to pin the reusable workflow. Locate the
"uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev" line and
replace "@dev" with the full commit SHA (e.g., "@<full-commit-sha>") from the
OpenVoiceOS/gh-automations repo (obtain via git ls-remote or the repo's commit
history) so the workflow is deterministic and not tied to a moving branch.

In @.github/workflows/coverage.yml:
- Around line 3-17: Add an explicit top-level permissions mapping to this
workflow (near the top alongside the existing on: block) to enforce
least-privilege instead of using defaults; for example, add a permissions: block
granting only the minimum scopes your reusable workflow needs (e.g., contents:
read and any other specific scopes required by the reused workflow) so the
coverage job (the coverage reusable workflow invoked with uses:
OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev) does not inherit
broad default permissions.
- Line 10: Replace the mutable branch reference in the reusable workflow
invocation "uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev"
with a specific commit SHA (e.g., @<full-40-char-commit-sha>) so the workflow is
pinned to an immutable commit; update the "uses" value to use that exact SHA and
commit the change.

In @.github/workflows/lint.yml:
- Around line 3-14: Add an explicit least-privilege permissions block to the
workflow so the "lint" job doesn't rely on broad default token scopes: update
the top-level workflow to include a permissions mapping (e.g., set contents:
read, checks: write or as minimal needed for the OpenVoiceOS/gh-automations
action) and ensure the "lint" job still passes secrets: inherit and with inputs
(ruff, pre_commit) to the referenced reusable workflow; reference the "lint" job
and the existing uses: OpenVoiceOS/gh-automations/.github/workflows/lint.yml@dev
when adding the permissions block.
- Line 10: The workflow is using a mutable ref ("uses:
OpenVoiceOS/gh-automations/.github/workflows/lint.yml@dev") which should be
pinned to an immutable commit SHA; update the `uses` entry in this workflow (and
the other workflows referenced: build-tests, coverage, downstream-check,
license-check, pip-audit, publish-stable, release-preview, release-workflow,
repo-health) to point to a specific commit SHA of OpenVoiceOS/gh-automations
(replace the `@dev` suffix with `@<full-commit-sha>`), ensuring each reusable
workflow reference uses the exact same or appropriate commit SHA for
reproducible CI.

In `@docs/pipeline.md`:
- Around line 66-69: PipelineHarness.match() claims to handle "late success" but
currently returns None as soon as _failed is set; update the method so a prior
_failed Event does not cause an immediate return — instead prefer a later
success by waiting on _succeeded for the remaining timeout even if _failed is
set, and only treat failure as terminal if timeout elapses without _succeeded
being set. Modify the match() implementation (the function named match in class
PipelineHarness) to compute remaining timeout and call
_succeeded.wait(remaining) rather than immediately returning when _failed is
True (or loop waiting on _succeeded with small sleeps), and only return None
after timeout/final failure; alternatively, if you prefer not to change code,
soften the docs to remove the stronger "does not mask a subsequent late success"
wording and explicitly state the current behavior that a failure currently
causes immediate return.

In `@ovoscope/bus_coverage.py`:
- Around line 489-516: The patched bus.emit installed by
BusCoverage.start_tracking can remain wrapped if End2EndTest.execute raises an
exception, so update End2EndTest.execute (the block that calls
BusCoverage.start_tracking / stop_tracking) to guarantee restoration in all
cases by invoking BusCoverage.stop_tracking inside a finally block (or use a
context-manager pattern around start_tracking/stop_tracking); ensure you
reference the BusCoverage instance used by End2EndTest.execute and restore its
_original_emit back onto self._bus.emit via stop_tracking even when
assertions/timeouts occur so _bus.emit and _original_emit are always reset.
- Around line 546-557: The fallback that guesses a skill by scanning
self._observed (inside the for msg in expected_messages loop) should be removed
so unscoped expected messages are not attributed to an arbitrary skill; change
the logic in the loop that uses _skill_id_for_message(msg) to skip the
scanning-for-first-emitter block and immediately set skill_id = "__core__" when
_skill_id_for_message returns None (affecting expected_messages handling and
asserted_emitter_pct attribution), leaving _observed and _skill_id_for_message
intact.

In `@ovoscope/pipeline.py`:
- Around line 180-193: The watcher thread started for _wait_either can run
forever if done.wait(timeout=timeout) times out; fix by adding a dedicated stop
Event (e.g., stop_event) that _wait_either checks in its loop in addition to
_matched and _failed, and when the caller times out set stop_event.set() and
join the watcher (or set the watcher as daemon and explicitly stop it) so the
thread can exit cleanly; update the watcher creation/cleanup around
watcher/_wait_either/done/timeout (and apply the same change to the similar
block at the later lines) so no orphan daemon threads accumulate.

In `@ovoscope/pytest_plugin.py`:
- Around line 165-191: The merge loop omits skills that only appear in
asserted_data; update the skill_id iteration in the bus report builder (the loop
building skills and using HandlerEntry, EmitterEntry, SkillBusCoverage,
BusCoverageReport) to union asserted_data as well — i.e., iterate over
sorted(set(listener_data) | set(observed_data) | set(asserted_data)) so
asserted-only skills are included in the final report.

In `@pyproject.toml`:
- Line 57: The pyproject setting "timeout = 60" relies on the pytest-timeout
plugin but that package is not listed alongside the existing dev dependencies
"pytest" and "pytest-cov"; add "pytest-timeout" to the project's dev
dependencies (the same section where "pytest" and "pytest-cov" are declared) so
the timeout option is honored by test runs, specifying an appropriate version
constraint.
- Around line 43-44: The Documentation URL in pyproject.toml currently points to
/tree/master/docs which will be broken for this dev-branch PR; update the
"Documentation" value to a branch-agnostic path such as using HEAD (e.g.,
/tree/HEAD/docs) or point it to the dev branch (/tree/dev/docs) or a
relative/docs-only URL so the docs link remains valid while this PR is
open—leave the "Issue Tracker" entry unchanged.

In `@test/unittests/test_remote_recorder.py`:
- Around line 32-58: The mock's emit_side_effect currently replays the entire
messages_to_emit list on every client.emit call, masking duplicate-emission
bugs; modify _make_mock_client so emit_side_effect consumes the scripted
messages only once (e.g., capture messages_to_emit into a local queue/iterator
and on the first client.emit deliver each message to handlers, then no-op on
subsequent emits), keeping references to handlers and using the same handler
invocation logic as before.

---

Nitpick comments:
In @.github/workflows/coverage.yml:
- Line 17: The workflow currently sets min_coverage: 0 which disables gating;
change the min_coverage value from 0 to a non-zero threshold (e.g., 70 or your
chosen baseline) in the workflow config so the coverage job fails when coverage
drops below the baseline; update the min_coverage field and, if needed, document
or expose it as a workflow input/variable so teams can adjust the threshold over
time.

In `@ovoscope/media.py`:
- Around line 385-494: The helper methods (play, pause, resume, stop,
next_track, prev_track, duck, unduck, cork, uncork, simulate_track_end,
simulate_invalid_stream) currently use fixed time.sleep(0.05) which makes tests
flaky; replace those sleeps with deterministic waits: after emitting the
Message, wait for a concrete bus event or state predicate that indicates the
action completed (e.g. use self.bus.wait_for(...) for the appropriate OCP event
like ovos.common_play.state or recognizer_loop events, or poll a
PlayerState/backend property until it reflects the expected change), include a
sensible timeout and clear fallback to avoid hanging, and apply this change
consistently across all listed methods.

In `@test/unittests/test_media.py`:
- Around line 14-24: Add a simple smoke test that imports OCPPlayerHarness and
exercises its context manager and lifecycle methods: create a MockOCPBackend (or
MagicMock backend) and a FakeBus/Message stub, instantiate OCPPlayerHarness with
those dependencies, then use a with OCPPlayerHarness(...) as harness: block and
call harness.open() and harness.close() (or just ensure exit runs without error)
to validate __enter__/__exit__ paths; name the test like
test_ocpplayerharness_context_manager and place it in test_media.py alongside
existing tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c9fe6c7a-625e-43b8-bbe0-1f91f724e77e

📥 Commits

Reviewing files that changed from the base of the PR and between 15a6925 and 448d310.

📒 Files selected for processing (31)
  • .github/workflows/build-tests.yml
  • .github/workflows/build_tests.yml
  • .github/workflows/conventional-label.yml
  • .github/workflows/coverage-pages.yml
  • .github/workflows/coverage.yml
  • .github/workflows/coverage_pages.yml
  • .github/workflows/downstream-check.yml
  • .github/workflows/license-check.yml
  • .github/workflows/lint.yml
  • .github/workflows/pip-audit.yml
  • .github/workflows/publish-stable.yml
  • .github/workflows/release-preview.yml
  • .github/workflows/release-workflow.yml
  • .github/workflows/repo-health.yml
  • AUDIT.md
  • MAINTENANCE_REPORT.md
  • QUICK_FACTS.md
  • SUGGESTIONS.md
  • docs/media-testing.md
  • docs/ocp.md
  • docs/pipeline.md
  • ovoscope/bus_coverage.py
  • ovoscope/coverage.py
  • ovoscope/diff.py
  • ovoscope/media.py
  • ovoscope/pipeline.py
  • ovoscope/pytest_plugin.py
  • pyproject.toml
  • test/unittests/test_media.py
  • test/unittests/test_phal.py
  • test/unittests/test_remote_recorder.py
💤 Files with no reviewable changes (2)
  • .github/workflows/coverage_pages.yml
  • .github/workflows/build_tests.yml
✅ Files skipped from review due to trivial changes (5)
  • docs/ocp.md
  • docs/media-testing.md
  • .github/workflows/conventional-label.yml
  • QUICK_FACTS.md
  • .github/workflows/release-preview.yml

Comment on lines +3 to +15
on:
pull_request:
branches: [dev, master, main]
workflow_dispatch:

jobs:
build:
uses: OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev
secrets: inherit
with:
python_versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]'
install_extras: 'audio,pydantic'
test_path: 'test/unittests/'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add explicit GITHUB_TOKEN permissions.

This workflow currently relies on default token scopes. Please define least-privilege permissions explicitly to reduce token exposure.

Suggested fix
 on:
   pull_request:
     branches: [dev, master, main]
   workflow_dispatch:

+permissions:
+  contents: read
+
 jobs:
   build:
🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 10-15: Workflow does not contain permissions
Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {{}}

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build-tests.yml around lines 3 - 15, Add an explicit
top-level permissions block for the GITHUB_TOKEN to avoid relying on default
scopes; because this workflow runs on pull_request and delegates to the reusable
job referenced by uses:
OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev (job: build),
add a permissions: mapping that grants only the least-privilege scopes required
(for example, contents: read and any other specific scopes you need such as
pull-requests: write or id-token: write) instead of the defaults so the token
exposure is minimized.


jobs:
build:
uses: OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/build-tests.yml | head -20

Repository: TigreGotico/ovoscope

Length of output: 524


Pin reusable workflow to an immutable ref.

Using @dev is mutable and can change behavior unexpectedly across runs. Pin to a commit SHA for reproducibility and supply-chain safety.

Suggested fix
-    uses: OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev
+    uses: OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@<commit-sha>
🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 10-15: Workflow does not contain permissions
Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {{}}

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build-tests.yml at line 10, The workflow currently
references the reusable workflow using a mutable ref
"OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev"; update that
to an immutable commit SHA (replace "@dev" with the specific commit hash) so the
workflow is pinned and reproducible. Locate the line that calls the reusable
workflow (the "uses:
OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev" entry) and
replace the ref with the full commit SHA from the target repository, commit the
change, and verify the workflow still executes as expected.

jobs:
unit_tests:
coverage_pages:
uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

GitHub Actions reusable workflow pinning commit SHA vs branch reference best practices

💡 Result:

Best practice: prefer immutable refs (commit SHA) over mutable refs (branches)

1) If the reusable workflow is in a different repo

  • Security-first default: pin the called workflow to a full commit SHA (immutable) to reduce supply-chain risk from a moved tag/branch or repo compromise. This is the same guidance GitHub gives for actions generally, and org policies can enforce SHA pinning. [2], [3]
  • Avoid @main, @master, or any branch ref for “trusted” behavior—branches are mutable. [2]

Example:

jobs:
  call:
    uses: org/central-workflows/.github/workflows/ci.yml@<FULL_COMMIT_SHA>

2) If the reusable workflow is in the same repo

  • You can call it without owner/repo@ref; GitHub resolves it from the same commit as the caller workflow, which is effectively immutable for that run and avoids ref-drift. [1]

Example:

jobs:
  call:
    uses: ./.github/workflows/ci.yml

3) When a branch/tag ref can be acceptable (trade-off)

  • If you need easier updates across many repos, use a versioned tag or release branch (e.g., v1 / v1.2.3) only if you also put governance around it (protected tags/branches, required reviews, restricted push) and accept the residual risk that the ref can still move unless you enforce immutability. GitHub’s security guidance still prefers SHA pinning for strongest protection. [2]

4) Organization enforcement

  • GitHub Actions policies can require full commit SHAs for dependencies (including reusable workflow references), failing workflows that aren’t pinned. [3]

Rule of thumb:

  • External or cross-repo reuse: pin to full SHA. [2], [3]
  • Same-repo reuse: omit @ref and rely on same-commit resolution. [1]

Sources: GitHub Docs on reusable workflows/ref resolution [1]; GitHub Well-Architected Actions security recommendation on immutable pinning [2]; GitHub Changelog on SHA-pinning policy enforcement [3].


Pin reusable workflow to a full commit SHA.

@dev is a mutable branch reference that can change unexpectedly, introducing supply-chain risk. GitHub's security guidance recommends pinning external reusable workflows to a specific commit SHA for deterministic CI behavior.

Suggested fix
-    uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev
+    uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@<commit-sha>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/coverage-pages.yml at line 13, The workflow references a
mutable branch tag in the reusable workflow declaration ("uses:
OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev"); replace the
branch ref with a specific commit SHA to pin the reusable workflow. Locate the
"uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev" line and
replace "@dev" with the full commit SHA (e.g., "@<full-commit-sha>") from the
OpenVoiceOS/gh-automations repo (obtain via git ls-remote or the repo's commit
history) so the workflow is deterministic and not tied to a moving branch.

Comment on lines +3 to +17
on:
pull_request:
branches: [dev]
workflow_dispatch:

jobs:
coverage:
uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev
secrets: inherit
with:
python_version: '3.11'
coverage_source: 'ovoscope'
test_path: 'test/unittests/'
install_extras: 'audio,pydantic'
min_coverage: 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Define explicit workflow permissions.

This workflow should declare least-privilege permissions instead of relying on defaults.

Suggested fix
 on:
   pull_request:
     branches: [dev]
   workflow_dispatch:

+permissions:
+  contents: read
+
 jobs:
   coverage:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
on:
pull_request:
branches: [dev]
workflow_dispatch:
jobs:
coverage:
uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev
secrets: inherit
with:
python_version: '3.11'
coverage_source: 'ovoscope'
test_path: 'test/unittests/'
install_extras: 'audio,pydantic'
min_coverage: 0
on:
pull_request:
branches: [dev]
workflow_dispatch:
permissions:
contents: read
jobs:
coverage:
uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev
secrets: inherit
with:
python_version: '3.11'
coverage_source: 'ovoscope'
test_path: 'test/unittests/'
install_extras: 'audio,pydantic'
min_coverage: 0
🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 10-17: Workflow does not contain permissions
Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {{}}

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/coverage.yml around lines 3 - 17, Add an explicit
top-level permissions mapping to this workflow (near the top alongside the
existing on: block) to enforce least-privilege instead of using defaults; for
example, add a permissions: block granting only the minimum scopes your reusable
workflow needs (e.g., contents: read and any other specific scopes required by
the reused workflow) so the coverage job (the coverage reusable workflow invoked
with uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev) does
not inherit broad default permissions.


jobs:
coverage:
uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and examine its content around line 10
if [ -f ".github/workflows/coverage.yml" ]; then
  echo "=== File exists. Content around line 10: ==="
  cat -n .github/workflows/coverage.yml | sed -n '1,20p'
else
  echo "File not found"
fi

Repository: TigreGotico/ovoscope

Length of output: 590


🏁 Script executed:

# Check if there are other workflow files using similar patterns
echo "=== Searching for other workflow references to gh-automations ==="
rg "gh-automations" .github/workflows/ -A 1 -B 1

Repository: TigreGotico/ovoscope

Length of output: 2985


Pin reusable workflow to a commit SHA.

@dev is a mutable branch reference; pinning to a commit SHA ensures deterministic CI behavior and prevents unexpected upstream changes.

Suggested fix
-    uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev
+    uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@<commit-sha>
🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 10-17: Workflow does not contain permissions
Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {{}}

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/coverage.yml at line 10, Replace the mutable branch
reference in the reusable workflow invocation "uses:
OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev" with a specific
commit SHA (e.g., @<full-40-char-commit-sha>) so the workflow is pinned to an
immutable commit; update the "uses" value to use that exact SHA and commit the
change.

Comment thread ovoscope/pipeline.py
Comment on lines +180 to +193
# Wait for either a match or a failure signal
import threading as _threading
done = _threading.Event()

def _wait_either() -> None:
while not _matched.is_set() and not _failed.is_set():
_matched.wait(timeout=0.05)
if _matched.is_set() or _failed.is_set():
break
done.set()

watcher = _threading.Thread(target=_wait_either, daemon=True)
watcher.start()
timed_out = not done.wait(timeout=timeout)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Timeout path leaks watcher threads.

If no event arrives, done.wait(timeout=timeout) times out, but the watcher loop keeps running indefinitely. Repeated calls can accumulate orphan daemon threads.

Suggested fix
         captured: List[Message] = []
         _matched = threading.Event()
         _failed = threading.Event()
+        done = threading.Event()
@@
         def _on_success(msg: Any) -> None:
             if isinstance(msg, str):
                 try:
                     msg = Message.deserialize(msg)
                 except Exception:
                     return
             captured.append(msg)
             _matched.set()
+            done.set()

         def _on_failure(msg: Any) -> None:
             _failed.set()
+            done.set()
@@
-        # Wait for either a match or a failure signal
-        import threading as _threading
-        done = _threading.Event()
-
-        def _wait_either() -> None:
-            while not _matched.is_set() and not _failed.is_set():
-                _matched.wait(timeout=0.05)
-                if _matched.is_set() or _failed.is_set():
-                    break
-            done.set()
-
-        watcher = _threading.Thread(target=_wait_either, daemon=True)
-        watcher.start()
         timed_out = not done.wait(timeout=timeout)

Also applies to: 199-203

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovoscope/pipeline.py` around lines 180 - 193, The watcher thread started for
_wait_either can run forever if done.wait(timeout=timeout) times out; fix by
adding a dedicated stop Event (e.g., stop_event) that _wait_either checks in its
loop in addition to _matched and _failed, and when the caller times out set
stop_event.set() and join the watcher (or set the watcher as daemon and
explicitly stop it) so the thread can exit cleanly; update the watcher
creation/cleanup around watcher/_wait_either/done/timeout (and apply the same
change to the similar block at the later lines) so no orphan daemon threads
accumulate.

Comment thread ovoscope/pytest_plugin.py
Comment on lines +165 to +191
skills = []
for skill_id in sorted(set(listener_data) | set(observed_data)):
listeners = [
HandlerEntry(
msg_type=mt,
handler_count=hc,
invocation_count=ic,
covered=ic > 0,
)
for mt, (hc, ic) in sorted(listener_data.get(skill_id, {}).items())
]
all_emitted = set(observed_data.get(skill_id, {}).keys()) | set(
asserted_data.get(skill_id, {}).keys()
)
emitters = [
EmitterEntry(
msg_type=mt,
observed_count=observed_data.get(skill_id, {}).get(mt, 0),
asserted_count=asserted_data.get(skill_id, {}).get(mt, 0),
observed=observed_data.get(skill_id, {}).get(mt, 0) > 0,
asserted=asserted_data.get(skill_id, {}).get(mt, 0) > 0,
)
for mt in sorted(all_emitted)
]
skills.append(SkillBusCoverage(skill_id=skill_id, listeners=listeners, emitters=emitters))

return BusCoverageReport(skills=skills)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep asserted-only skills in the merged session report.

The merge loop only unions listener_data and observed_data, so a skill that appears solely in asserted_data disappears from the final terminal summary. That hides the "asserted but not observed" case instead of surfacing it.

💡 Minimal fix
-        for skill_id in sorted(set(listener_data) | set(observed_data)):
+        for skill_id in sorted(
+            set(listener_data) | set(observed_data) | set(asserted_data)
+        ):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovoscope/pytest_plugin.py` around lines 165 - 191, The merge loop omits
skills that only appear in asserted_data; update the skill_id iteration in the
bus report builder (the loop building skills and using HandlerEntry,
EmitterEntry, SkillBusCoverage, BusCoverageReport) to union asserted_data as
well — i.e., iterate over sorted(set(listener_data) | set(observed_data) |
set(asserted_data)) so asserted-only skills are included in the final report.

Comment thread pyproject.toml
Comment on lines +43 to +44
Documentation = "https://github.com/TigreGotico/ovoscope/tree/master/docs"
"Issue Tracker" = "https://github.com/TigreGotico/ovoscope/issues"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Documentation URL references master branch.

The Documentation URL points to /tree/master/docs, but this PR targets the dev branch. If the docs (including docs/bus-coverage.md) are new additions, this link will be broken until dev is merged into master.

Consider using a branch-agnostic URL or ensuring docs exist on master before this PR merges:

-Documentation = "https://github.com/TigreGotico/ovoscope/tree/master/docs"
+Documentation = "https://github.com/TigreGotico/ovoscope/blob/dev/docs"

Alternatively, keep as-is if master is the primary release branch and broken links during development are acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyproject.toml` around lines 43 - 44, The Documentation URL in pyproject.toml
currently points to /tree/master/docs which will be broken for this dev-branch
PR; update the "Documentation" value to a branch-agnostic path such as using
HEAD (e.g., /tree/HEAD/docs) or point it to the dev branch (/tree/dev/docs) or a
relative/docs-only URL so the docs link remains valid while this PR is
open—leave the "Issue Tracker" entry unchanged.

Comment thread pyproject.toml

[tool.pytest.ini_options]
testpaths = ["test"]
timeout = 60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing pytest-timeout dependency.

The timeout = 60 option requires the pytest-timeout plugin, which is not listed in the dev dependencies (only pytest and pytest-cov are included). Without it, this setting will be silently ignored.

🔧 Proposed fix
 dev = [
     "ovoscope[audio,pydantic]",
     "pytest",
     "pytest-cov",
+    "pytest-timeout",
 ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyproject.toml` at line 57, The pyproject setting "timeout = 60" relies on
the pytest-timeout plugin but that package is not listed alongside the existing
dev dependencies "pytest" and "pytest-cov"; add "pytest-timeout" to the
project's dev dependencies (the same section where "pytest" and "pytest-cov" are
declared) so the timeout option is honored by test runs, specifying an
appropriate version constraint.

Comment on lines +32 to +58
def _make_mock_client(messages_to_emit: List[Message]) -> MagicMock:
"""Build a mock MessageBusClient that emits *messages_to_emit* when subscribed."""
client = MagicMock()
client.connected_event = threading.Event()
client.connected_event.set()

handlers: dict = {}

def on_side_effect(event_type: str, handler: Any) -> None:
handlers.setdefault(event_type, []).append(handler)

def remove_side_effect(event_type: str, handler: Any) -> None:
if event_type in handlers:
try:
handlers[event_type].remove(handler)
except ValueError:
pass

def emit_side_effect(msg: Message) -> None:
# Deliver mocked response messages to subscribed handlers
for m in messages_to_emit:
for h in list(handlers.get("message", [])):
h(m.serialize())

client.on.side_effect = on_side_effect
client.remove.side_effect = remove_side_effect
client.emit.side_effect = emit_side_effect

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make the mock client replay scripted responses only once.

emit_side_effect() pushes the full messages_to_emit sequence on every client.emit() call. If record() starts emitting an extra message or retrying internally, these tests still pass with duplicated responses instead of catching the regression.

💡 Minimal tightening
+    delivered = False
     def emit_side_effect(msg: Message) -> None:
+        nonlocal delivered
+        if delivered:
+            return
+        delivered = True
         # Deliver mocked response messages to subscribed handlers
         for m in messages_to_emit:
             for h in list(handlers.get("message", [])):
                 h(m.serialize())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_remote_recorder.py` around lines 32 - 58, The mock's
emit_side_effect currently replays the entire messages_to_emit list on every
client.emit call, masking duplicate-emission bugs; modify _make_mock_client so
emit_side_effect consumes the scripted messages only once (e.g., capture
messages_to_emit into a local queue/iterator and on the first client.emit
deliver each message to handlers, then no-op on subsequent emits), keeping
references to handlers and using the same handler invocation logic as before.

@JarbasAl JarbasAl changed the title feat: Bus coverage feat: Bus coverage + media testing Mar 13, 2026
@github-actions github-actions Bot added feature and removed feature labels Mar 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ovoscope/__init__.py (1)

774-800: ⚠️ Potential issue | 🟠 Major

Missing try/finally around bus coverage tracking — bus.emit may remain wrapped on failure.

If capture.capture() or capture.finish() raises an exception, _bus_tracker.stop_tracking() is never called, leaving bus.emit wrapped for all subsequent tests using the same MiniCroft instance. This matches the concern raised in the past review.

🛡️ Proposed fix: wrap tracking in try/finally
         # bus coverage tracking (optional)
         _bus_tracker = None
         if self.track_bus_coverage:
             from ovoscope.bus_coverage import BusCoverageTracker
             _bus_tracker = BusCoverageTracker(self.minicroft.bus, self.minicroft)
             _bus_tracker.snapshot_listeners()
             _bus_tracker.start_tracking()

-        # the capture session will store all messages until capture.finish()
-        #  even if multiple messages are emitted
-        capture = CaptureSession(self.minicroft, eof_msgs=self.eof_msgs,
-                                 ignore_messages=self.ignore_messages,
-                                 async_messages=self.async_messages)
-        for idx, source_message in enumerate(self.source_message):
-            if "session" not in source_message.context and len(capture.responses):
-                # propagate session updates as a client would do
-                source_message.context["session"] = capture.responses[-1].context["session"]
-            capture.capture(source_message, timeout)
-
-        # final message list
-        messages = capture.finish()
-
-        if _bus_tracker is not None:
-            _bus_tracker.stop_tracking()
-            all_responses = messages + list(getattr(capture, "async_responses", []))
-            _bus_tracker.record_session(all_responses, self.expected_messages)
-            self.bus_coverage_report = _bus_tracker.build_report()
+        try:
+            # the capture session will store all messages until capture.finish()
+            #  even if multiple messages are emitted
+            capture = CaptureSession(self.minicroft, eof_msgs=self.eof_msgs,
+                                     ignore_messages=self.ignore_messages,
+                                     async_messages=self.async_messages)
+            for idx, source_message in enumerate(self.source_message):
+                if "session" not in source_message.context and len(capture.responses):
+                    # propagate session updates as a client would do
+                    source_message.context["session"] = capture.responses[-1].context["session"]
+                capture.capture(source_message, timeout)
+
+            # final message list
+            messages = capture.finish()
+        finally:
+            if _bus_tracker is not None:
+                _bus_tracker.stop_tracking()
+
+        if _bus_tracker is not None:
+            all_responses = messages + list(getattr(capture, "async_responses", []))
+            _bus_tracker.record_session(all_responses, self.expected_messages)
+            self.bus_coverage_report = _bus_tracker.build_report()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovoscope/__init__.py` around lines 774 - 800, Wrap the BusCoverageTracker
usage in a try/finally so stop_tracking() always runs even if capture.capture()
or capture.finish() raises: after creating _bus_tracker and starting it, run the
capture loop and messages = capture.finish() inside a try block, then in finally
call _bus_tracker.stop_tracking() if _bus_tracker is not None; after the
try/finally (or inside finally if you need session data) call
_bus_tracker.record_session(...) and set self.bus_coverage_report =
_bus_tracker.build_report() only when _bus_tracker exists, and re-raise any
exceptions so behavior is unchanged on error (ensure references: _bus_tracker,
BusCoverageTracker, capture.capture, capture.finish, stop_tracking,
record_session, build_report).
🧹 Nitpick comments (2)
ovoscope/pytest_plugin.py (1)

285-307: Code after yield in fixture executes after teardown, not after finally.

Lines 285-307 run after the finally block completes (after yield collector). This code accesses request.config and writes files after the session fixture has already restored global state. While this works, it may cause confusion because:

  1. The merged report is built from collector._reports which was populated during tests
  2. Global state (GLOBAL_BUS_COVERAGE, etc.) has already been reset to False/None

Consider moving report finalization (filtering, file saving, attaching to config) inside the try block before yield, or restructuring to clarify the lifecycle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovoscope/pytest_plugin.py` around lines 285 - 307, The post-yield code that
calls collector.merged_report(), filters it and writes the file executes after
teardown (after GLOBAL_BUS_COVERAGE and other globals are reset), which is
confusing; move the report finalization and file-write logic into the try block
before yield (i.e., after collector has been populated but before yield
collector) or otherwise restructure the fixture so that
collector.merged_report(), report.filter(...), attaching to
request.config._bus_coverage_reports and the cov_file write happen prior to
yield, referencing the existing collector.merged_report(), request.config and
cov_file handling so global state is still intact when building and saving the
report.
ovoscope/__init__.py (1)

150-228: Global monkey-patching at import time affects all FakeBus instances.

_patch_fakebus() is called unconditionally at module import (Line 227), modifying FakeBus.on, .once, .emit, and OVOSSkill.add_event, .bind globally. While the patches check GLOBAL_BUS_COVERAGE before recording, the method dispatch overhead exists for all users.

This is acceptable given the design goal of capturing boot-time events, but consider documenting this behavior prominently (e.g., in the module docstring or README) so users understand the import-time side effect.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovoscope/__init__.py` around lines 150 - 228, The module currently calls
_patch_fakebus() at import-time which monkey-patches FakeBus.on/once/emit and
OVOSSkill.add_event/bind globally and adds runtime dispatch overhead; update
documentation to clearly state this import-time side-effect and its scope by
adding a prominent note to the module-level docstring (near the top of
ovoscope/__init__.py) and to the project README explaining that importing
ovoscope will patch ovos_utils.fakebus and ovos_workshop globally, what
GLOBAL_BUS_COVERAGE gates do, and any performance/compatibility implications so
users are aware of the behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/bus-coverage.md`:
- Around line 143-151: Update the documentation's Source reference for
SkillBusCoverage: replace the incorrect `ovoscope/bus_coverage.py:118` with the
correct location for the SkillBusCoverage definition (lines 131–132) or remove
the specific line number and simply reference `ovoscope/bus_coverage.py` to
avoid future drift; ensure the symbol name `SkillBusCoverage` remains in the
Source line so readers can locate the class.

---

Outside diff comments:
In `@ovoscope/__init__.py`:
- Around line 774-800: Wrap the BusCoverageTracker usage in a try/finally so
stop_tracking() always runs even if capture.capture() or capture.finish()
raises: after creating _bus_tracker and starting it, run the capture loop and
messages = capture.finish() inside a try block, then in finally call
_bus_tracker.stop_tracking() if _bus_tracker is not None; after the try/finally
(or inside finally if you need session data) call
_bus_tracker.record_session(...) and set self.bus_coverage_report =
_bus_tracker.build_report() only when _bus_tracker exists, and re-raise any
exceptions so behavior is unchanged on error (ensure references: _bus_tracker,
BusCoverageTracker, capture.capture, capture.finish, stop_tracking,
record_session, build_report).

---

Nitpick comments:
In `@ovoscope/__init__.py`:
- Around line 150-228: The module currently calls _patch_fakebus() at
import-time which monkey-patches FakeBus.on/once/emit and
OVOSSkill.add_event/bind globally and adds runtime dispatch overhead; update
documentation to clearly state this import-time side-effect and its scope by
adding a prominent note to the module-level docstring (near the top of
ovoscope/__init__.py) and to the project README explaining that importing
ovoscope will patch ovos_utils.fakebus and ovos_workshop globally, what
GLOBAL_BUS_COVERAGE gates do, and any performance/compatibility implications so
users are aware of the behavior.

In `@ovoscope/pytest_plugin.py`:
- Around line 285-307: The post-yield code that calls collector.merged_report(),
filters it and writes the file executes after teardown (after
GLOBAL_BUS_COVERAGE and other globals are reset), which is confusing; move the
report finalization and file-write logic into the try block before yield (i.e.,
after collector has been populated but before yield collector) or otherwise
restructure the fixture so that collector.merged_report(), report.filter(...),
attaching to request.config._bus_coverage_reports and the cov_file write happen
prior to yield, referencing the existing collector.merged_report(),
request.config and cov_file handling so global state is still intact when
building and saving the report.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77b35d06-be18-4d49-bbf6-1f204037c061

📥 Commits

Reviewing files that changed from the base of the PR and between 448d310 and ad741e5.

📒 Files selected for processing (8)
  • FAQ.md
  • MAINTENANCE_REPORT.md
  • SKILL.md
  • docs/bus-coverage.md
  • docs/gui-testing.md
  • ovoscope/__init__.py
  • ovoscope/bus_coverage.py
  • ovoscope/pytest_plugin.py
✅ Files skipped from review due to trivial changes (1)
  • SKILL.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • FAQ.md

Comment thread docs/bus-coverage.md
Comment on lines +143 to +151
## Calculations API

If you are building custom tooling, you can access these values via `SkillBusCoverage` properties:

* `listener_coverage_pct`: `(covered_listeners / total_listeners) * 100`
* `observed_emitter_pct`: `(observed_emitters / total_emitters) * 100`
* `asserted_emitter_pct`: `(asserted_emitters / total_emitters) * 100`

Source: `SkillBusCoverage` — `ovoscope/bus_coverage.py:118`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: Incorrect source line reference.

The documentation states SkillBusCoverage is at ovoscope/bus_coverage.py:118, but looking at the source, SkillBusCoverage is defined at line 131-132.

 Source: `SkillBusCoverage` — `ovoscope/bus_coverage.py:118`
+Source: `SkillBusCoverage` — `ovoscope/bus_coverage.py:131`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Calculations API
If you are building custom tooling, you can access these values via `SkillBusCoverage` properties:
* `listener_coverage_pct`: `(covered_listeners / total_listeners) * 100`
* `observed_emitter_pct`: `(observed_emitters / total_emitters) * 100`
* `asserted_emitter_pct`: `(asserted_emitters / total_emitters) * 100`
Source: `SkillBusCoverage``ovoscope/bus_coverage.py:118`
## Calculations API
If you are building custom tooling, you can access these values via `SkillBusCoverage` properties:
* `listener_coverage_pct`: `(covered_listeners / total_listeners) * 100`
* `observed_emitter_pct`: `(observed_emitters / total_emitters) * 100`
* `asserted_emitter_pct`: `(asserted_emitters / total_emitters) * 100`
Source: `SkillBusCoverage``ovoscope/bus_coverage.py:131`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/bus-coverage.md` around lines 143 - 151, Update the documentation's
Source reference for SkillBusCoverage: replace the incorrect
`ovoscope/bus_coverage.py:118` with the correct location for the
SkillBusCoverage definition (lines 131–132) or remove the specific line number
and simply reference `ovoscope/bus_coverage.py` to avoid future drift; ensure
the symbol name `SkillBusCoverage` remains in the Source line so readers can
locate the class.

- test_global_bus_coverage.py: Unit tests for global bus coverage tracking
- test_gui_capture.py: Unit tests for GUI capture session

These files were created locally but not committed to the PR branch.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@github-actions github-actions Bot added feature and removed feature labels Mar 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
test/unittests/test_global_bus_coverage.py (1)

26-38: Consider resetting GLOBAL_BUS_COVERAGE_FILE for completeness.

The fixture resets GLOBAL_BUS_COVERAGE and GLOBAL_BUS_COVERAGE_COLLECTOR, but per the global state defined in ovoscope/__init__.py:104-147, there's also GLOBAL_BUS_COVERAGE_FILE that could leak between tests if future tests modify it.

🔧 Suggested improvement
     def setup_globals(self):
         """Reset global state before and after each test."""
         orig_enabled = ovoscope.GLOBAL_BUS_COVERAGE
         orig_collector = ovoscope.GLOBAL_BUS_COVERAGE_COLLECTOR
+        orig_file = ovoscope.GLOBAL_BUS_COVERAGE_FILE
         
         ovoscope.GLOBAL_BUS_COVERAGE = False
         ovoscope.GLOBAL_BUS_COVERAGE_COLLECTOR = None
+        ovoscope.GLOBAL_BUS_COVERAGE_FILE = None
         
         yield
         
         ovoscope.GLOBAL_BUS_COVERAGE = orig_enabled
         ovoscope.GLOBAL_BUS_COVERAGE_COLLECTOR = orig_collector
+        ovoscope.GLOBAL_BUS_COVERAGE_FILE = orig_file
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_global_bus_coverage.py` around lines 26 - 38, The fixture
setup_globals should also save and restore ovoscope.GLOBAL_BUS_COVERAGE_FILE to
avoid test leakage; capture the original value (orig_file =
ovoscope.GLOBAL_BUS_COVERAGE_FILE) at the start, set
ovoscope.GLOBAL_BUS_COVERAGE_FILE to None (or the appropriate neutral value)
alongside GLOBAL_BUS_COVERAGE and GLOBAL_BUS_COVERAGE_COLLECTOR before yield,
and restore ovoscope.GLOBAL_BUS_COVERAGE_FILE = orig_file after yield
(referencing the setup_globals fixture and the GLOBAL_BUS_COVERAGE_FILE symbol).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/unittests/test_global_bus_coverage.py`:
- Around line 26-38: The fixture setup_globals should also save and restore
ovoscope.GLOBAL_BUS_COVERAGE_FILE to avoid test leakage; capture the original
value (orig_file = ovoscope.GLOBAL_BUS_COVERAGE_FILE) at the start, set
ovoscope.GLOBAL_BUS_COVERAGE_FILE to None (or the appropriate neutral value)
alongside GLOBAL_BUS_COVERAGE and GLOBAL_BUS_COVERAGE_COLLECTOR before yield,
and restore ovoscope.GLOBAL_BUS_COVERAGE_FILE = orig_file after yield
(referencing the setup_globals fixture and the GLOBAL_BUS_COVERAGE_FILE symbol).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b91a575-61b7-44ae-be84-365f177a62f7

📥 Commits

Reviewing files that changed from the base of the PR and between ad741e5 and bc41498.

📒 Files selected for processing (2)
  • test/unittests/test_global_bus_coverage.py
  • test/unittests/test_gui_capture.py

@github-actions github-actions Bot added feature and removed feature labels Mar 13, 2026
@JarbasAl JarbasAl merged commit 82114ee into dev Mar 13, 2026
15 checks passed
@JarbasAl JarbasAl deleted the bus_coverage branch March 13, 2026 22:15
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.

2 participants