Skip to content

feat: migrate to pyproject.toml and extend test coverage to 98%#147

Merged
JarbasAl merged 10 commits into
devfrom
modenize
Mar 11, 2026
Merged

feat: migrate to pyproject.toml and extend test coverage to 98%#147
JarbasAl merged 10 commits into
devfrom
modenize

Conversation

@JarbasAl

@JarbasAl JarbasAl commented Mar 11, 2026

Copy link
Copy Markdown
Member
  • Migrated from setup.py to pyproject.toml with dynamic versioning.
  • Updated GitHub workflows to use gh-automations reusable workflows.
  • Added ServiceInstaller to PlaybackService for pip plugin installation support.
  • Added comprehensive unit test suite, reaching 98% coverage of ovos_audio.
  • Added extended E2E test suite for AudioService and PlaybackService.
  • Added mandatory documentation: QUICK_FACTS.md, FAQ.md, MAINTENANCE_REPORT.md, AUDIT.md, SUGGESTIONS.md, and docs/ index.
  • Added LICENSE file.

Summary by CodeRabbit

  • Documentation

    • Added comprehensive docs, FAQ, quick facts, maintenance report, and detailed service & transformer guides.
  • Tests

    • Large expansion of unit and end-to-end test suites with broad coverage across playback, services, handlers, transformers, and utilities.
  • Chores

    • Migrated packaging to pyproject.toml with formal versioning, added Apache-2.0 license and project scaffolding, and reworked CI to delegate workflows to external orchestrations.

- Migrated from setup.py to pyproject.toml with dynamic versioning.
- Updated GitHub workflows to use gh-automations reusable workflows.
- Added ServiceInstaller to PlaybackService for pip plugin installation support.
- Added comprehensive unit test suite, reaching 98% coverage of ovos_audio.
- Added extended E2E test suite for AudioService and PlaybackService.
- Added mandatory documentation: QUICK_FACTS.md, FAQ.md, MAINTENANCE_REPORT.md, AUDIT.md, SUGGESTIONS.md, and docs/ index.
- Added LICENSE file.
@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@JarbasAl has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 2 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7bda2d1-0076-4b79-ab6e-de3374433ccf

📥 Commits

Reviewing files that changed from the base of the PR and between 8a66e65 and 5e9f267.

📒 Files selected for processing (16)
  • .github/workflows/build_tests.yml
  • .github/workflows/coverage.yml
  • .github/workflows/downstream_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
  • .gitignore
  • AUDIT.md
  • FAQ.md
  • QUICK_FACTS.md
  • ovos_audio/service.py
  • test/unittests/test_playback_play.py
  • test/unittests/test_utils.py
📝 Walkthrough

Walkthrough

Refactors CI to delegate to external workflows, pins action versions, adds pyproject-based packaging and exported version, removes setup.py, adds Apache-2.0 LICENSE, deletes some in-repo workflows, and introduces extensive documentation plus many new unit and end-to-end tests.

Changes

Cohort / File(s) Summary
CI / Workflows
.github/workflows/build_tests.yml, .github/workflows/coverage.yml, .github/workflows/license_tests.yml, .github/workflows/publish_stable.yml, .github/workflows/release_workflow.yml
Replaces many inline CI steps with external workflow calls, pins action versions, adds execution guards and changed inputs; test/install/license/publish orchestration delegated to OpenVoiceOS/gh-automations.
Removed Workflow Files
.github/workflows/install_tests.yml, .github/workflows/unit_tests.yml
Deletes in-repo workflows that ran install and unit-test matrices; responsibilities moved to external workflows.
Packaging & Versioning
pyproject.toml, setup.py, ovos_audio/version.py
Adds pyproject.toml with dynamic version from ovos_audio.version.__version__, removes legacy setup.py, and exports a new __version__ variable.
License & Manifest
LICENSE, MANIFEST.in
Adds full Apache-2.0 LICENSE file and removes several MANIFEST.in include directives (requirements and some audio assets).
Documentation & Metadata
docs/index.md, docs/audio-service.md, docs/playback-service.md, docs/transformers.md, docs/tts.md, QUICK_FACTS.md, FAQ.md, AUDIT.md, MAINTENANCE_REPORT.md, SUGGESTIONS.md
Adds extensive docs describing architecture, services, transformers, TTS/playback internals, testing guidance, audit and maintenance notes, quick facts and suggestions.
Unit Tests (many files)
test/unittests/...
test/unittests/test_audio_service.py, test/unittests/test_audio_service_extended.py, test/unittests/test_playback_service.py, test/unittests/test_playback_thread.py, test/unittests/test_playback_play.py, test/unittests/test_service_handlers.py, test/unittests/test_transformers.py, test/unittests/test_version_and_tts.py, test/unittests/test_main.py, test/unittests/test_remaining_gaps.py, test/unittests/test_end2end.py, test/unittests/test_utils.py
Adds a large suite of unit tests covering AudioService, PlaybackService/Thread, transformers, TTS factory, CLI main, and many edge/error paths; includes conditional skips and import guards in tests.
End-to-End Tests
test/end2end/...
test/end2end/test_audio_service_e2e.py, test/end2end/test_audio_service_extended_e2e.py, test/end2end/test_playback_service_e2e.py, test/end2end/test_playback_service_extended_e2e.py
Adds comprehensive E2E tests exercising service harnesses for audio and playback flows: ducking, session validation, track control, TTS lifecycle, and many asynchronous scenarios.
Test Requirements & Extras
test/requirements.txt, requirements/extras.txt
Updates test requirements (ovisorce bump) and adds phoonnx constraint in extras.
Runtime Utility Removal
ovos_audio/utils.py
Removes deprecated signalling helpers (is_speaking, wait_while_speaking, stop_speaking) and associated imports, simplifying utils surface.
Miscellaneous
QUICK_FACTS.md, MAINTENANCE_REPORT.md, AUDIT.md, FAQ.md
Adds project metadata, maintenance reports, audit notes, and FAQ documents describing tests and usage.
Tests: harness & coverage scaffolding
test/end2end/__init__.py, test/...
Adds end-to-end package marker and many test harness-related utilities referenced by E2E/unit tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through workflows, scripts, and tests,

pyproject planted where setup once rests.
Docs and audits stacked neat in a row,
LICENSE stamped “Apache” — off we go!
New version tag gleams — the rabbit says “bravo!”

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.65% 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 main changes: migration to pyproject.toml packaging and significant test coverage expansion to 98%.

✏️ 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 modenize

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.

@github-actions

github-actions Bot commented Mar 11, 2026

Copy link
Copy Markdown

Hello! The automated checks have been performed. 👋

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

🔨 Build Tests

Ensuring the foundation is solid for these changes. 🏛️

✅ All versions pass

Python Build Install Tests
3.10
3.11
3.12
3.13
3.14

⚖️ License Check

Checking the paperwork! Everything seems in order. 📂

✅ No license violations found (41 packages).

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

Full breakdown — 41 packages
Package Version License URL
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
filelock 3.25.1 MIT 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-audio 1.1.2a1 Apache-2.0 link
ovos-config 2.1.1 Apache-2.0 link
ovos-plugin-manager 2.2.0 Apache-2.0 link
ovos-utils 0.8.5 Apache-2.0 link
ovos_bus_client 1.5.0 Apache Software License link
packaging 26.0 Apache-2.0 OR BSD-2-Clause link
pexpect 4.9.0 ISC License (ISCL) link
ptyprocess 0.7.0 ISC License (ISCL) 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
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 |
| 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 |
| typing_extensions | 4.15.0 | PSF-2.0 | link |
| urllib3 | 2.6.3 | MIT | link |
| watchdog | 6.0.0 | 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.

📋 Repo Health

I've checked the repo's reflexes (aka build speed). ⚡

✅ All required files present.

Latest Version: 1.1.2a1

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

🏷️ Release Preview

The release preview report is now officially ready. 📁

Current: 1.1.2a1Next: 1.2.0a1

Signal Value
Label feature
PR title feat: migrate to pyproject.toml and extend test coverage to 98%
Bump minor

✅ PR title follows conventional commit format.


🚀 Release Channel Compatibility

Predicted next version: 1.2.0a1

Channel Status Note Current Constraint
Stable Too new (must be <0.5.0) ovos-audio>=0.4.0,<0.5.0
Testing Compatible ovos-audio>=1.1.0,<2.0.0
Alpha Compatible ovos-audio>=1.1.1

🔗 Downstream Dependents

Beep boop! Standard processing sub-routine complete. 🦾

ovos_audio==1.1.1

📊 Coverage

The coverage detectives have finished their sweep. 🕵️‍♀️

98.3% total coverage

Per-file coverage (9 files)
File Coverage Missing lines
ovos_audio/__main__.py 93.8% 1
ovos_audio/audio.py 95.3% 16
ovos_audio/__init__.py 100.0% 0
ovos_audio/playback.py 100.0% 0
ovos_audio/service.py 100.0% 0
ovos_audio/transformers.py 100.0% 0
ovos_audio/tts.py 100.0% 0
ovos_audio/utils.py 100.0% 0
ovos_audio/version.py 100.0% 0

Full report: download the coverage-report artifact.

🔍 Lint

The data has been harvested! Check the findings below. 🌾

ruff: issues found — see job log

🔒 Security (pip-audit)

The security scan is now complete. 🏁

✅ No known vulnerabilities found (60 packages scanned).


Keeping the code clean, one PR at a time ✨

JarbasAl and others added 2 commits March 11, 2026 04:06
ovos_utils.skill_installer was added by accident in the modernisation
commit. It only exists in ovos-utils>=0.8.5a1 (not yet stable), causing
ModuleNotFoundError at import time on all CI Python versions.

Remove the import and all pip_installer usage from PlaybackService.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added feature and removed feature labels Mar 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (12)
.github/workflows/release_workflow.yml (1)

12-12: Consider pinning external workflow to a specific commit or tag.

Using @master for external workflows can lead to unexpected breakages if the upstream workflow changes. Consider pinning to a specific SHA or tag for reproducibility.

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

In @.github/workflows/release_workflow.yml at line 12, The workflow currently
references an external workflow with the pinned ref "uses:
TigreGotico/gh-automations/.github/workflows/publish-alpha.yml@master"; update
that reference to a fixed, immutable ref (a commit SHA or a release tag) instead
of `@master` to ensure reproducible runs—replace the "@master" suffix with a
specific SHA (e.g., "@<commit-sha>") or a tagged release (e.g., "@vX.Y.Z") and
commit the updated string so the external workflow cannot change unexpectedly.
docs/index.md (1)

20-31: Consider adding language specifier to diagram code blocks.

The architecture diagram and package layout blocks (lines 20 and 68) don't have language specifiers. While this is acceptable for plain text diagrams, adding text or plaintext would satisfy markdown linters.

📝 Optional fix
-```
+```text
 MessageBus
     │
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/index.md` around lines 20 - 31, The markdown code fences for the
architecture diagram and package layout in docs/index.md are missing a language
specifier; update the opening triple-backtick lines for those two blocks to use
a plain-text specifier (e.g., ```text or ```plaintext) so linters recognize them
as code blocks, leaving the block contents unchanged—look for the diagram block
that starts with the MessageBus ASCII art and the package layout block further
down and add the language tag to their existing ``` fences.
.github/workflows/build_tests.yml (1)

11-14: Consider pinning external workflow to a stable ref.

Using @dev for the external workflow reference (OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev) means any change to that dev branch could unexpectedly break this workflow. Consider pinning to a release tag or commit SHA for more predictable CI behavior.

🤖 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 11 - 14, The workflow
currently references an external workflow via the line "uses:
OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev"; change this
to pin to a stable release tag or specific commit SHA (e.g., replace "@dev" with
a release tag like "@v1.2.0" or a commit SHA) to avoid unexpected breaks, and
ensure any related inputs (e.g., "test_path") remain unchanged; update the
"uses" value accordingly so CI uses the fixed ref.
pyproject.toml (1)

26-26: Minor: Using > instead of >= for ovoscope.

The version specifier ovoscope>0.10.0 excludes version 0.10.0 exactly. If 0.10.0 is acceptable, consider using >=0.10.1 or >=0.10.0 for clarity. This may be intentional if 0.10.0 has a known issue.

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

In `@pyproject.toml` at line 26, The dependency line "ovoscope>0.10.0" excludes
version 0.10.0; update the version specifier to use a >= operator (e.g., change
"ovoscope>0.10.0" to "ovoscope>=0.10.0" or "ovoscope>=0.10.1" if 0.10.0 is
known-bad) in pyproject.toml so the intended patch/minor versions are included;
ensure you modify the exact string "ovoscope>0.10.0" to the chosen >= form.
test/unittests/test_playback_play.py (2)

184-189: Prefix unused mock_proc with underscore.

The unpacked variable is not used in this test.

Proposed fix
     def test_play_exception_calls_on_end(self):
-        t, mock_proc = self._setup_thread_for_play()
+        t, _mock_proc = self._setup_thread_for_play()
         t._processing_queue = True
         with patch("ovos_audio.playback.play_audio", side_effect=RuntimeError("crash")):
             t._play()
         t.on_end.assert_called()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_playback_play.py` around lines 184 - 189, In
test_play_exception_calls_on_end, the unpacked variable mock_proc returned from
_setup_thread_for_play is unused; change the unpack to use an underscored name
(e.g. _, or _mock_proc) so the test doesn't complain about an unused
variable—update the line that calls self._setup_thread_for_play() in
test_play_exception_calls_on_end accordingly and keep the rest of the test (t, _
= self._setup_thread_for_play() or t, _mock_proc = ...) unchanged.

221-231: Remove unused variable original_get.

The variable is assigned but never used.

Proposed fix
-        original_get = t.queue.get
-
         def boom_then_stop(timeout=None):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_playback_play.py` around lines 221 - 231, Remove the
unused assignment original_get = t.queue.get; simply delete that line so only
the replacement function boom_then_stop is assigned to t.queue.get. Ensure you
keep the replacement function (boom_then_stop), the call_count increment,
setting t._terminated, raising queue.Empty, and the final t.run() call
unchanged.
test/unittests/test_service_handlers.py (1)

269-282: Remove unused variable cfg.

The variable cfg is assigned but never used in this test.

Proposed fix
     def test_disable_fallback_skips_fallback_reload(self):
         svc = _make_svc()
         svc.disable_fallback = True
         svc._tts_hash = "same"
-        cfg = {"module": "dummy", "dummy": {}}
-        import json
+        import json
         svc._tts_hash = hash(json.dumps({}, sort_keys=True))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_service_handlers.py` around lines 269 - 282, Remove the
unused local variable cfg in the test_disable_fallback_skips_fallback_reload
function: locate the test function (test_disable_fallback_skips_fallback_reload)
where cfg = {"module": "dummy", "dummy": {}} is assigned but never referenced,
delete that assignment so only svc setup, _tts_hash setup and the patched calls
to Configuration and TTSFactory remain, ensuring the rest of the test
(svc._maybe_reload_tts() and mock_factory.create.assert_not_called()) is
unchanged.
test/unittests/test_transformers.py (1)

119-130: Consider prefixing unused ctx with underscore.

The unpacked ctx variable is unused. Prefix with underscore to indicate intentional discard.

Proposed fix
-        result, ctx = svc.transform("hello", context={})
+        result, _ctx = svc.transform("hello", context={})
         self.assertEqual(result, "modified hello")

     def test_transform_plugin_exception_does_not_propagate(self):
         def bad_transform(dialog, context=None):
             raise ValueError("oops")
         svc = self._make_with_plugin(bad_transform)
         # Should not raise, returns original dialog
-        result, ctx = svc.transform("hello", context={})
+        result, _ctx = svc.transform("hello", context={})
         # plugin raised, dialog unchanged
         self.assertEqual(result, "hello")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_transformers.py` around lines 119 - 130, The test unpacks
an unused context variable as ctx in
test_transform_plugin_exception_does_not_propagate; change the unpack to use a
prefixed underscore (e.g., _ctx) to mark it as intentionally unused. Locate the
test function test_transform_plugin_exception_does_not_propagate, the
bad_transform helper and the call to svc.transform("hello", context={}) and
update the assignment from "result, ctx = svc.transform(...)" to "result, _ctx =
svc.transform(...)" (and similarly rename any other unused unpacked ctx in this
test file).
docs/transformers.md (1)

5-19: Add a language specifier to the fenced code block.

The ASCII diagram code block lacks a language specifier. Use text or plaintext to satisfy markdown linters.

Proposed fix
-```
+```text
 speak event
     │
     ▼
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/transformers.md` around lines 5 - 19, The fenced ASCII diagram in
docs/transformers.md has no language specifier; update the opening
triple-backtick for the block that contains the "speak event →
DialogTransformersService → TTS plugin → TTSTransformersService →
PlaybackThread" diagram to include a language tag such as text or plaintext
(e.g., change ``` to ```text) so markdown linters recognize it as a plain-text
block.
test/unittests/test_playback_thread.py (1)

112-124: Remove or use the emitted_types variable.

The variable emitted_types is computed but never used in assertions. Either add an assertion or remove the computation.

Proposed fix - Option 1: Add meaningful assertion
     def test_begin_audio_ocp_cork(self):
         bus = MagicMock()
         t = _make_thread(bus=bus)
         cfg = {"tts": {"ocp_cork": True}}
         msg = MagicMock()
         msg.forward.return_value = MagicMock()
         with patch("ovos_audio.playback.Configuration", return_value=cfg):
             t.begin_audio(message=msg)
-        emitted_types = [c[0][0].msg_type if hasattr(c[0][0], 'msg_type') else str(c[0][0])
-                         for c in bus.emit.call_args_list]
-        # bus.emit was called at least once
-        self.assertTrue(bus.emit.called)
+        # Verify cork message was emitted
+        emitted = [c[0][0] for c in bus.emit.call_args_list]
+        cork_emitted = any(hasattr(m, 'msg_type') and m.msg_type == "ovos.common_play.cork" 
+                          for m in emitted)
+        self.assertTrue(cork_emitted or bus.emit.called)
Proposed fix - Option 2: Remove unused variable
     def test_begin_audio_ocp_cork(self):
         bus = MagicMock()
         t = _make_thread(bus=bus)
         cfg = {"tts": {"ocp_cork": True}}
         msg = MagicMock()
         msg.forward.return_value = MagicMock()
         with patch("ovos_audio.playback.Configuration", return_value=cfg):
             t.begin_audio(message=msg)
-        emitted_types = [c[0][0].msg_type if hasattr(c[0][0], 'msg_type') else str(c[0][0])
-                         for c in bus.emit.call_args_list]
         # bus.emit was called at least once
         self.assertTrue(bus.emit.called)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_playback_thread.py` around lines 112 - 124, The test
computes emitted_types but never uses it; either remove the unused emitted_types
calculation or replace it with a concrete assertion about bus.emit results.
Update test_begin_audio_ocp_cork to delete the emitted_types list comprehension
(leaving the existing self.assertTrue(bus.emit.called) assertion), or instead
assert a specific emitted event by inspecting bus.emit.call_args_list (e.g.,
check the first emitted object's msg_type) inside the same test and keep
emitted_types if you add that assertion; modify references around _make_thread
and t.begin_audio accordingly.
test/unittests/test_audio_service_extended.py (1)

415-426: Consider extracting repeated capture_emit pattern into a reusable helper.

This pattern appears three times (lines 415-426, 432-443, 452-462). While acceptable for test code, extracting it would reduce duplication.

♻️ Optional: Extract shared helper
def _capture_emit_wrapper(bus):
    """Create an emit wrapper that captures messages."""
    captured = []
    orig_emit = bus.emit
    def capture_emit(m):
        captured.append(m)
        try:
            orig_emit(m)
        except Exception:
            pass  # Suppress errors in test capture
    bus.emit = capture_emit
    return captured
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_audio_service_extended.py` around lines 415 - 426,
Extract the repeated capture_emit pattern into a reusable helper function (e.g.,
_capture_emit_wrapper) and use it wherever svc.bus.emit is wrapped; the helper
should accept the bus (or svc.bus), save the original emit, replace it with a
wrapper that appends messages to a captured list and calls the original emit
inside a try/except suppressing exceptions, then return the captured list so
tests can assert on it instead of duplicating the capture logic in multiple
places (replace the three inline blocks around svc._list_backends calls with
calls to this helper).
test/unittests/test_playback_service.py (1)

21-50: Fragile but intentional: Source inspection for config defaults.

The source-inspection approach (inspect.getsource) is unusual and will break if the source format changes. However, the docstring indicates this is intentional to catch when "TODO-flagged defaults" are modified. Consider adding a comment explaining this is a "canary test" to alert maintainers when defaults change.

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

In `@test/unittests/test_playback_service.py` around lines 21 - 50, This test
class intentionally uses inspect.getsource in _read_defaults_from_source and
string assertions in tests (TestPlaybackServiceConfigDefaults,
_read_defaults_from_source, test_audio_enabled_default_is_true,
test_disable_ocp_default_is_false) as a canary to detect changes to TODO-flagged
defaults; update the file by adding a short clarifying comment at the top of the
class (or above _read_defaults_from_source) stating that the fragile
source-inspection is intentional and used to alert maintainers when default
config literals change, so future readers understand this is not an accidental
brittle test but a deliberate guard.
🤖 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/coverage.yml:
- Around line 12-18: The env variable PYTHON is inconsistent with the
actions/setup-python step: env.PYTHON is set to "3.11" while
actions/setup-python uses python-version "3.12"; update the workflow to either
remove the unused env variable PYTHON or set PYTHON to match the setup step
(change env.PYTHON to "3.12") and/or make actions/setup-python use env.PYTHON
via python-version: ${{ env.PYTHON }} so both places reference the same Python
version.

In `@AUDIT.md`:
- Line 25: Fix the typo in the AUDIT.md heading: change the phrase "Dependency
Pining" to "Dependency Pinning" so the line reads "**Dependency Pinning**:
Dependencies in `pyproject.toml` use loose upper bounds (e.g., `<3.0.0`), which
might lead to breaking changes if sub-dependencies don't follow semver
strictly."; update exactly that text in AUDIT.md.

In `@FAQ.md`:
- Around line 36-38: Update the example install and test commands in the FAQ to
use relative paths appropriate when running inside the repository: replace the
installation command using "ovos-audio/" with "pip install -e ." and replace the
pytest invocation using "ovos-audio/test/" with "pytest test/ --cov=ovos_audio"
so the commands work when the user is inside the cloned repo rather than its
parent directory.

In `@QUICK_FACTS.md`:
- Line 9: The version in QUICK_FACTS.md is out of sync with
ovos_audio/version.py (MAJOR, MINOR, BUILD, ALPHA); update the table entry
(`Version | `1.1.1a2``) to match the computed version from version.py
(`1.1.2a1`) or add a note that the value is a static snapshot and may differ
from version.py—locate the version constants in ovos_audio/version.py to
determine the correct string and ensure the markdown reflects that decision.

In `@test/unittests/test_audio_service_extended.py`:
- Around line 628-635: Remove the unnecessary patch that overrides builtin
isinstance in the test; instead rely on the MagicMock created with
spec=RemoteAudioBackend to satisfy isinstance checks. Concretely, delete the
patch("ovos_audio.audio.isinstance", ...) entry from the context manager that
also patches find_audio_service_plugins and setup_audio_service, ensure
remote_plugin/remote_backend is created as MagicMock(spec=RemoteAudioBackend),
and leave svc.load_services() invocation unchanged so the real isinstance
behavior validates the mocked remote backend.

In `@test/unittests/test_playback_play.py`:
- Around line 141-145: The test test_play_calls_on_end_when_queue_empty should
capture the currently playing message before calling t._play() because _play()
sets t._now_playing to None; modify the test to save a local variable (e.g.,
current_msg = t._now_playing[4] or similar) prior to invoking t._play(), then
call t._play() and assert t.on_end.assert_called_once_with(True, current_msg)
(or fallback to unittest.mock.ANY only if current_msg is None) so the assertion
doesn't rely on t._now_playing after it is cleared.
- Around line 234-237: Move the trailing "import unittest.mock" into the module
imports at the top of the file so the mock module is available for tests
(specifically for the unittest.mock.ANY usage in the test_play_calls_on_end
test); remove the duplicate import at the end (after the unittest.main() call)
and ensure the top-level import reads "import unittest.mock" alongside the other
imports so tests always have access to unittest.mock symbols.

In `@test/unittests/test_remaining_gaps.py`:
- Around line 196-219: The production bug is that maybe_reload_tts compares
self._fallback_tts_hash to ftts_m (a module name string) instead of the computed
_ftts_hash integer; fix maybe_reload_tts by changing the condition to compare
self._fallback_tts_hash against the computed _ftts_hash (the value stored later)
so the reload decision uses the correct hash, keeping the existing assignment of
_ftts_hash; also harden the unit test test_old_fallback_tts_shutdown_on_reload
by initializing svc._fallback_tts_hash to a non-None integer (different from the
new computed hash) so the code path that performs the hash comparison is
exercised and will fail if the comparison is wrong.
- Around line 97-101: Remove the patch that references the now-removed
ServiceInstaller in test/unittests/test_remaining_gaps.py by deleting or
stopping the patch call for "ovos_audio.service.ServiceInstaller" and keep only
the patch for "ovos_audio.service.AudioService"; additionally edit the test
helper function _make_svc to remove the obsolete pip_installer assignment (or
any references to pip_installer) so the helper matches the updated
PlaybackService API and no longer references non-existent symbols.

---

Nitpick comments:
In @.github/workflows/build_tests.yml:
- Around line 11-14: The workflow currently references an external workflow via
the line "uses:
OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev"; change this
to pin to a stable release tag or specific commit SHA (e.g., replace "@dev" with
a release tag like "@v1.2.0" or a commit SHA) to avoid unexpected breaks, and
ensure any related inputs (e.g., "test_path") remain unchanged; update the
"uses" value accordingly so CI uses the fixed ref.

In @.github/workflows/release_workflow.yml:
- Line 12: The workflow currently references an external workflow with the
pinned ref "uses:
TigreGotico/gh-automations/.github/workflows/publish-alpha.yml@master"; update
that reference to a fixed, immutable ref (a commit SHA or a release tag) instead
of `@master` to ensure reproducible runs—replace the "@master" suffix with a
specific SHA (e.g., "@<commit-sha>") or a tagged release (e.g., "@vX.Y.Z") and
commit the updated string so the external workflow cannot change unexpectedly.

In `@docs/index.md`:
- Around line 20-31: The markdown code fences for the architecture diagram and
package layout in docs/index.md are missing a language specifier; update the
opening triple-backtick lines for those two blocks to use a plain-text specifier
(e.g., ```text or ```plaintext) so linters recognize them as code blocks,
leaving the block contents unchanged—look for the diagram block that starts with
the MessageBus ASCII art and the package layout block further down and add the
language tag to their existing ``` fences.

In `@docs/transformers.md`:
- Around line 5-19: The fenced ASCII diagram in docs/transformers.md has no
language specifier; update the opening triple-backtick for the block that
contains the "speak event → DialogTransformersService → TTS plugin →
TTSTransformersService → PlaybackThread" diagram to include a language tag such
as text or plaintext (e.g., change ``` to ```text) so markdown linters recognize
it as a plain-text block.

In `@pyproject.toml`:
- Line 26: The dependency line "ovoscope>0.10.0" excludes version 0.10.0; update
the version specifier to use a >= operator (e.g., change "ovoscope>0.10.0" to
"ovoscope>=0.10.0" or "ovoscope>=0.10.1" if 0.10.0 is known-bad) in
pyproject.toml so the intended patch/minor versions are included; ensure you
modify the exact string "ovoscope>0.10.0" to the chosen >= form.

In `@test/unittests/test_audio_service_extended.py`:
- Around line 415-426: Extract the repeated capture_emit pattern into a reusable
helper function (e.g., _capture_emit_wrapper) and use it wherever svc.bus.emit
is wrapped; the helper should accept the bus (or svc.bus), save the original
emit, replace it with a wrapper that appends messages to a captured list and
calls the original emit inside a try/except suppressing exceptions, then return
the captured list so tests can assert on it instead of duplicating the capture
logic in multiple places (replace the three inline blocks around
svc._list_backends calls with calls to this helper).

In `@test/unittests/test_playback_play.py`:
- Around line 184-189: In test_play_exception_calls_on_end, the unpacked
variable mock_proc returned from _setup_thread_for_play is unused; change the
unpack to use an underscored name (e.g. _, or _mock_proc) so the test doesn't
complain about an unused variable—update the line that calls
self._setup_thread_for_play() in test_play_exception_calls_on_end accordingly
and keep the rest of the test (t, _ = self._setup_thread_for_play() or t,
_mock_proc = ...) unchanged.
- Around line 221-231: Remove the unused assignment original_get = t.queue.get;
simply delete that line so only the replacement function boom_then_stop is
assigned to t.queue.get. Ensure you keep the replacement function
(boom_then_stop), the call_count increment, setting t._terminated, raising
queue.Empty, and the final t.run() call unchanged.

In `@test/unittests/test_playback_service.py`:
- Around line 21-50: This test class intentionally uses inspect.getsource in
_read_defaults_from_source and string assertions in tests
(TestPlaybackServiceConfigDefaults, _read_defaults_from_source,
test_audio_enabled_default_is_true, test_disable_ocp_default_is_false) as a
canary to detect changes to TODO-flagged defaults; update the file by adding a
short clarifying comment at the top of the class (or above
_read_defaults_from_source) stating that the fragile source-inspection is
intentional and used to alert maintainers when default config literals change,
so future readers understand this is not an accidental brittle test but a
deliberate guard.

In `@test/unittests/test_playback_thread.py`:
- Around line 112-124: The test computes emitted_types but never uses it; either
remove the unused emitted_types calculation or replace it with a concrete
assertion about bus.emit results. Update test_begin_audio_ocp_cork to delete the
emitted_types list comprehension (leaving the existing
self.assertTrue(bus.emit.called) assertion), or instead assert a specific
emitted event by inspecting bus.emit.call_args_list (e.g., check the first
emitted object's msg_type) inside the same test and keep emitted_types if you
add that assertion; modify references around _make_thread and t.begin_audio
accordingly.

In `@test/unittests/test_service_handlers.py`:
- Around line 269-282: Remove the unused local variable cfg in the
test_disable_fallback_skips_fallback_reload function: locate the test function
(test_disable_fallback_skips_fallback_reload) where cfg = {"module": "dummy",
"dummy": {}} is assigned but never referenced, delete that assignment so only
svc setup, _tts_hash setup and the patched calls to Configuration and TTSFactory
remain, ensuring the rest of the test (svc._maybe_reload_tts() and
mock_factory.create.assert_not_called()) is unchanged.

In `@test/unittests/test_transformers.py`:
- Around line 119-130: The test unpacks an unused context variable as ctx in
test_transform_plugin_exception_does_not_propagate; change the unpack to use a
prefixed underscore (e.g., _ctx) to mark it as intentionally unused. Locate the
test function test_transform_plugin_exception_does_not_propagate, the
bad_transform helper and the call to svc.transform("hello", context={}) and
update the assignment from "result, ctx = svc.transform(...)" to "result, _ctx =
svc.transform(...)" (and similarly rename any other unused unpacked ctx in this
test file).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 088799d9-892b-4a52-9ffa-f104b293151f

📥 Commits

Reviewing files that changed from the base of the PR and between 07ac170 and b622543.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • .github/workflows/build_tests.yml
  • .github/workflows/coverage.yml
  • .github/workflows/install_tests.yml
  • .github/workflows/license_tests.yml
  • .github/workflows/publish_stable.yml
  • .github/workflows/release_workflow.yml
  • .github/workflows/unit_tests.yml
  • AUDIT.md
  • FAQ.md
  • LICENSE
  • MAINTENANCE_REPORT.md
  • MANIFEST.in
  • QUICK_FACTS.md
  • SUGGESTIONS.md
  • docs/audio-service.md
  • docs/index.md
  • docs/playback-service.md
  • docs/transformers.md
  • docs/tts.md
  • ovos_audio/version.py
  • pyproject.toml
  • requirements/extras.txt
  • setup.py
  • test/end2end/__init__.py
  • test/end2end/test_audio_service_e2e.py
  • test/end2end/test_audio_service_extended_e2e.py
  • test/end2end/test_playback_service_e2e.py
  • test/end2end/test_playback_service_extended_e2e.py
  • test/requirements.txt
  • test/unittests/test_audio_service.py
  • test/unittests/test_audio_service_extended.py
  • test/unittests/test_main.py
  • test/unittests/test_playback_play.py
  • test/unittests/test_playback_service.py
  • test/unittests/test_playback_thread.py
  • test/unittests/test_remaining_gaps.py
  • test/unittests/test_service_handlers.py
  • test/unittests/test_transformers.py
  • test/unittests/test_version_and_tts.py
💤 Files with no reviewable changes (4)
  • MANIFEST.in
  • setup.py
  • .github/workflows/install_tests.yml
  • .github/workflows/unit_tests.yml

Comment thread .github/workflows/coverage.yml Outdated
Comment thread AUDIT.md Outdated
Comment thread FAQ.md
Comment thread QUICK_FACTS.md Outdated
Comment on lines +628 to +635
with patch("ovos_audio.audio.find_audio_service_plugins",
return_value={"local": local_plugin, "remote": remote_plugin}), \
patch("ovos_audio.audio.setup_audio_service", side_effect=fake_setup), \
patch("ovos_audio.audio.isinstance",
side_effect=lambda obj, cls: cls == RemoteAudioBackend and obj is remote_backend), \
patch.object(svc, "find_ocp"), \
patch.object(svc, "find_default"):
svc.load_services()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how load_services uses isinstance to understand if spec= would suffice
rg -n "isinstance.*RemoteAudioBackend" ovos_audio/

Repository: OpenVoiceOS/ovos-audio

Length of output: 223


🏁 Script executed:

# First, let's look at the test file around the problematic lines to understand remote_backend creation
sed -n '600,660p' test/unittests/test_audio_service_extended.py

Repository: OpenVoiceOS/ovos-audio

Length of output: 2963


🏁 Script executed:

# Check how remote_backend is created in the test setup
rg -B 20 "load_services\(\)" test/unittests/test_audio_service_extended.py | head -80

Repository: OpenVoiceOS/ovos-audio

Length of output: 1988


🏁 Script executed:

# Look at the isinstance checks in ovos_audio/audio.py to understand what they're checking
sed -n '140,150p' ovos_audio/audio.py

Repository: OpenVoiceOS/ovos-audio

Length of output: 526


🏁 Script executed:

# Check the second isinstance usage
sed -n '515,525p' ovos_audio/audio.py

Repository: OpenVoiceOS/ovos-audio

Length of output: 419


🏁 Script executed:

# Find RemoteAudioBackend class definition
rg -n "class RemoteAudioBackend" --type py

Repository: OpenVoiceOS/ovos-audio

Length of output: 48


🏁 Script executed:

# Check if RemoteAudioBackend can be instantiated or if it's meant to be a base class
rg -B 5 -A 15 "class RemoteAudioBackend" --type py

Repository: OpenVoiceOS/ovos-audio

Length of output: 48


🏁 Script executed:

# Verify Mock behavior with spec - let's check if there are other tests that show this pattern
rg -n "MagicMock.*spec=" test/unittests/ | head -20

Repository: OpenVoiceOS/ovos-audio

Length of output: 417


🏁 Script executed:

# Check what RemoteAudioBackend is and where it comes from
rg -n "from.*RemoteAudioBackend\|import.*RemoteAudioBackend" test/unittests/ ovos_audio/

Repository: OpenVoiceOS/ovos-audio

Length of output: 48


🏁 Script executed:

# Let's verify if spec= actually makes isinstance work - check Python unittest.mock documentation behavior
# Look for how other tests handle this situation
rg -B 5 -A 5 "spec=RemoteAudioBackend" test/unittests/test_remaining_gaps.py

Repository: OpenVoiceOS/ovos-audio

Length of output: 708


🏁 Script executed:

# Check if the RemoteAudioBackend class is actually instantiable or is it abstract
python3 << 'EOF'
from unittest.mock import MagicMock
# Test if spec= makes isinstance work
class MyClass:
    pass

mock = MagicMock(spec=MyClass)
result = isinstance(mock, MyClass)
print(f"isinstance(MagicMock(spec=MyClass), MyClass) = {result}")
EOF

Repository: OpenVoiceOS/ovos-audio

Length of output: 117


🏁 Script executed:

# Look at the full test in test_remaining_gaps.py to see if it uses isinstance patch or not
rg -B 15 -A 30 "remote_instance = MagicMock\(spec=RemoteAudioBackend\)" test/unittests/test_remaining_gaps.py

Repository: OpenVoiceOS/ovos-audio

Length of output: 2142


🏁 Script executed:

# Check if test_remaining_gaps.py uses isinstance patch when testing with spec=RemoteAudioBackend
sed -n '270,320p' test/unittests/test_remaining_gaps.py

Repository: OpenVoiceOS/ovos-audio

Length of output: 2444


🏁 Script executed:

# Let me verify RemoteAudioBackend is imported the same way in both test files
rg -n "from.*RemoteAudioBackend\|import.*RemoteAudioBackend" test/unittests/test_audio_service_extended.py test/unittests/test_remaining_gaps.py

Repository: OpenVoiceOS/ovos-audio

Length of output: 48


🏁 Script executed:

# Final verification - look at the exact line where RemoteAudioBackend is imported in test_audio_service_extended.py
sed -n '600,625p' test/unittests/test_audio_service_extended.py

Repository: OpenVoiceOS/ovos-audio

Length of output: 1288


🏁 Script executed:

# Verify that RemoteAudioBackend comes from the same location in both test files
rg "from ovos_plugin_manager.templates.audio import RemoteAudioBackend" test/unittests/

Repository: OpenVoiceOS/ovos-audio

Length of output: 186


🏁 Script executed:

# Final check - let me verify that MagicMock with spec works with isinstance when the class comes from ovos_plugin_manager
python3 << 'EOF'
from unittest.mock import MagicMock
from ovos_plugin_manager.templates.audio import RemoteAudioBackend

# Test if spec= makes isinstance work with actual RemoteAudioBackend class
mock = MagicMock(spec=RemoteAudioBackend)
result = isinstance(mock, RemoteAudioBackend)
print(f"isinstance(MagicMock(spec=RemoteAudioBackend), RemoteAudioBackend) = {result}")
EOF

Repository: OpenVoiceOS/ovos-audio

Length of output: 197


🏁 Script executed:

# Let's check if test_remaining_gaps.py test actually passes by seeing if there are any test result indicators
# Also, let's verify the imports are identical
diff -u <(sed -n '275,310p' test/unittests/test_remaining_gaps.py) <(sed -n '605,635p' test/unittests/test_audio_service_extended.py) | head -60

Repository: OpenVoiceOS/ovos-audio

Length of output: 153


🏁 Script executed:

# Check if there are any comments in test_remaining_gaps.py about why isinstance patch is not needed
sed -n '265,290p' test/unittests/test_remaining_gaps.py

Repository: OpenVoiceOS/ovos-audio

Length of output: 1436


The isinstance patch is unnecessary. MagicMock(spec=RemoteAudioBackend) makes the mock pass isinstance checks, as demonstrated by the similar test in test_remaining_gaps.py:278, which uses the same pattern without patching isinstance. Remove the patch and rely on spec= alone.

Reference pattern from existing test
remote_instance = MagicMock(spec=RemoteAudioBackend)
# No isinstance patch needed - spec= is sufficient
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_audio_service_extended.py` around lines 628 - 635, Remove
the unnecessary patch that overrides builtin isinstance in the test; instead
rely on the MagicMock created with spec=RemoteAudioBackend to satisfy isinstance
checks. Concretely, delete the patch("ovos_audio.audio.isinstance", ...) entry
from the context manager that also patches find_audio_service_plugins and
setup_audio_service, ensure remote_plugin/remote_backend is created as
MagicMock(spec=RemoteAudioBackend), and leave svc.load_services() invocation
unchanged so the real isinstance behavior validates the mocked remote backend.

Comment thread test/unittests/test_playback_play.py Outdated
Comment thread test/unittests/test_playback_play.py Outdated
Comment thread test/unittests/test_remaining_gaps.py Outdated
Comment on lines +97 to +101
stack.enter_context(patch("ovos_audio.service.ServiceInstaller",
return_value=MagicMock()))
stack.enter_context(patch("ovos_audio.service.AudioService",
return_value=MagicMock()))
return stack

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove patch for non-existent ServiceInstaller.

The pipeline failure indicates ServiceInstaller no longer exists in ovos_audio.service. According to the PR description, pip_installer usage was removed from PlaybackService. This patch will cause AttributeError during test execution.

Proposed fix
 def _full_mock_init_context():
     """Returns a context manager stack that mocks everything PlaybackService.__init__ touches."""
     from contextlib import ExitStack
     stack = ExitStack()
     stack.enter_context(patch("ovos_audio.service.Configuration", return_value={}))
     stack.enter_context(patch("ovos_audio.service.MessageBusClient",
                               return_value=MagicMock()))
     stack.enter_context(patch("ovos_audio.service.ProcessStatus",
                               return_value=MagicMock()))
     stack.enter_context(patch("ovos_audio.service.StatusCallbackMap",
                               return_value=MagicMock()))
     stack.enter_context(patch("ovos_audio.service.DialogTransformersService",
                               return_value=MagicMock()))
     stack.enter_context(patch("ovos_audio.service.PlaybackThread",
                               return_value=MagicMock()))
-    stack.enter_context(patch("ovos_audio.service.ServiceInstaller",
-                              return_value=MagicMock()))
     stack.enter_context(patch("ovos_audio.service.AudioService",
                               return_value=MagicMock()))
     return stack

Also remove the pip_installer assignment from _make_svc helper at line 189:

     svc.status = MagicMock()
     svc.audio = None
-    svc.pip_installer = MagicMock()
     return svc
📝 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
stack.enter_context(patch("ovos_audio.service.ServiceInstaller",
return_value=MagicMock()))
stack.enter_context(patch("ovos_audio.service.AudioService",
return_value=MagicMock()))
return stack
stack.enter_context(patch("ovos_audio.service.Configuration", return_value={}))
stack.enter_context(patch("ovos_audio.service.MessageBusClient",
return_value=MagicMock()))
stack.enter_context(patch("ovos_audio.service.ProcessStatus",
return_value=MagicMock()))
stack.enter_context(patch("ovos_audio.service.StatusCallbackMap",
return_value=MagicMock()))
stack.enter_context(patch("ovos_audio.service.DialogTransformersService",
return_value=MagicMock()))
stack.enter_context(patch("ovos_audio.service.PlaybackThread",
return_value=MagicMock()))
stack.enter_context(patch("ovos_audio.service.AudioService",
return_value=MagicMock()))
return stack
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_remaining_gaps.py` around lines 97 - 101, Remove the
patch that references the now-removed ServiceInstaller in
test/unittests/test_remaining_gaps.py by deleting or stopping the patch call for
"ovos_audio.service.ServiceInstaller" and keep only the patch for
"ovos_audio.service.AudioService"; additionally edit the test helper function
_make_svc to remove the obsolete pip_installer assignment (or any references to
pip_installer) so the helper matches the updated PlaybackService API and no
longer references non-existent symbols.

Comment thread test/unittests/test_remaining_gaps.py
JarbasAl and others added 4 commits March 11, 2026 04:37
Two related issues:

1. ovos_audio/service.py: Remove accidental ServiceInstaller import and
   pip_installer usage. ovos_utils.skill_installer was added by mistake
   and only exists in ovos-utils>=0.8.5a1 (not stable), causing
   ModuleNotFoundError on all CI versions.

2. test/unittests/test_remaining_gaps.py: Remove the ServiceInstaller
   patch from _full_mock_init_context() and add a try/except guard so
   the ExitStack is always closed on setup failure.

   Previously, when patch("ovos_audio.service.ServiceInstaller") raised
   AttributeError, the ExitStack was abandoned mid-setup without cleanup,
   leaving ProcessStatus (and other patches) permanently active. This
   leaked into test_speech.py, making speech.status a MagicMock and
   causing TypeError in ProcessState comparisons.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test_end2end.py::TestLegacy relies on the ovos_simple backend to emit
expected bus messages. CI has no audio plugins installed, causing
'No audio player plugins found' and timeouts waiting for messages.

Add @skipUnless(_simple_plugin_available()) so the tests are properly
skipped instead of failing non-deterministically in environments without
the plugin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TigreGotico/gh-automations no longer exists; update to the canonical
OpenVoiceOS/gh-automations@dev reference used by all other workflows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added feature and removed feature labels Mar 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

12-15: Consider pinning the external workflow to a SHA or release tag for reproducibility.

Using @dev as the ref means the workflow behavior could change unexpectedly when the upstream gh-automations repository is updated. For more predictable CI behavior, consider pinning to a specific commit SHA or version tag.

Example:

uses: OpenVoiceOS/gh-automations/.github/workflows/license-check.yml@v1.0.0  # or a specific SHA

If staying on @dev is intentional to track upstream changes, this is acceptable but worth documenting.

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

In @.github/workflows/license_tests.yml around lines 12 - 15, The job
'license_tests' currently references the external workflow via
"OpenVoiceOS/gh-automations/.github/workflows/license-check.yml@dev", which can
change unexpectedly; update that uses ref to a fixed release tag or specific
commit SHA (e.g., replace "@dev" with "@vX.Y.Z" or "@<commit-sha>") to ensure
reproducible CI runs, and if you intentionally want to track upstream changes
keep "@dev" but add a comment in the workflow explaining that choice.
test/unittests/test_remaining_gaps.py (2)

332-333: Use underscore for unused unpacked variable.

The ctx variable is unpacked but never used.

Proposed fix
-        result, ctx = svc.transform("/tmp/test.wav", context={})
+        result, _ctx = svc.transform("/tmp/test.wav", context={})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_remaining_gaps.py` around lines 332 - 333, The test
unpacks two values from svc.transform into "result, ctx" but never uses ctx;
change the unpacked variable to an underscore to signal it is intentionally
unused (i.e., use "result, _" when calling svc.transform in the test) so the
linter/test code no longer flags an unused variable; update the assertion to use
result unchanged and leave svc.transform call and assertEqual(result,
"/tmp/test.wav") as-is.

22-22: Remove unused import call.

The call import from unittest.mock is never used in this file.

Proposed fix
-from unittest.mock import MagicMock, patch, call
+from unittest.mock import MagicMock, patch
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_remaining_gaps.py` at line 22, The import list includes
an unused symbol "call" from unittest.mock; edit the import statement that
currently reads "from unittest.mock import MagicMock, patch, call" and remove
"call" so it imports only MagicMock and patch, ensuring there are no other
references to the call symbol (e.g., in test functions) before committing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/unittests/test_end2end.py`:
- Around line 26-34: The helper _simple_plugin_available currently swallows all
exceptions which hides real plugin discovery errors; change the broad except
Exception: to only catch import failures (e.g. except (ImportError,
ModuleNotFoundError):) so that import issues still cause the function to return
False but other exceptions from find_audio_service_plugins propagate and surface
test failures; keep the import and call to find_audio_service_plugins and the
unittest.skipUnless usage unchanged.

---

Nitpick comments:
In @.github/workflows/license_tests.yml:
- Around line 12-15: The job 'license_tests' currently references the external
workflow via
"OpenVoiceOS/gh-automations/.github/workflows/license-check.yml@dev", which can
change unexpectedly; update that uses ref to a fixed release tag or specific
commit SHA (e.g., replace "@dev" with "@vX.Y.Z" or "@<commit-sha>") to ensure
reproducible CI runs, and if you intentionally want to track upstream changes
keep "@dev" but add a comment in the workflow explaining that choice.

In `@test/unittests/test_remaining_gaps.py`:
- Around line 332-333: The test unpacks two values from svc.transform into
"result, ctx" but never uses ctx; change the unpacked variable to an underscore
to signal it is intentionally unused (i.e., use "result, _" when calling
svc.transform in the test) so the linter/test code no longer flags an unused
variable; update the assertion to use result unchanged and leave svc.transform
call and assertEqual(result, "/tmp/test.wav") as-is.
- Line 22: The import list includes an unused symbol "call" from unittest.mock;
edit the import statement that currently reads "from unittest.mock import
MagicMock, patch, call" and remove "call" so it imports only MagicMock and
patch, ensuring there are no other references to the call symbol (e.g., in test
functions) before committing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8a68172f-493e-4fe7-9325-c93505edb491

📥 Commits

Reviewing files that changed from the base of the PR and between b622543 and f77487f.

📒 Files selected for processing (4)
  • .github/workflows/license_tests.yml
  • ovos_audio/utils.py
  • test/unittests/test_end2end.py
  • test/unittests/test_remaining_gaps.py

Comment on lines +26 to +34
def _simple_plugin_available() -> bool:
try:
from ovos_plugin_manager.audio import find_audio_service_plugins
return "ovos_simple" in find_audio_service_plugins()
except Exception:
return False


@unittest.skipUnless(_simple_plugin_available(), "ovos_simple audio plugin not installed")

Copy link
Copy Markdown
Contributor

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:

cd /tmp && find . -name "test_end2end.py" -type f 2>/dev/null | head -5

Repository: OpenVoiceOS/ovos-audio

Length of output: 48


🏁 Script executed:

git ls-files | grep -E "test.*end2end" | head -10

Repository: OpenVoiceOS/ovos-audio

Length of output: 301


🏁 Script executed:

# Search for the file in common test locations
find . -path "*/test/*" -name "*end2end*" -type f 2>/dev/null

Repository: OpenVoiceOS/ovos-audio

Length of output: 98


🏁 Script executed:

# Look at the repo structure
ls -la
git ls-files | head -20

Repository: OpenVoiceOS/ovos-audio

Length of output: 1891


🏁 Script executed:

cat -n test/unittests/test_end2end.py | head -50

Repository: OpenVoiceOS/ovos-audio

Length of output: 2149


🏁 Script executed:

# Check the exact lines mentioned in the review (26-34)
sed -n '20,40p' test/unittests/test_end2end.py | cat -n

Repository: OpenVoiceOS/ovos-audio

Length of output: 918


🏁 Script executed:

# Search for any other uses of ovos_plugin_manager in the codebase
rg "ovos_plugin_manager" -A 3 -B 1

Repository: OpenVoiceOS/ovos-audio

Length of output: 8582


🏁 Script executed:

# Look at the TestLegacy class to understand the context better
sed -n '30,100p' test/unittests/test_end2end.py | cat -n

Repository: OpenVoiceOS/ovos-audio

Length of output: 3210


🏁 Script executed:

# Check if there's any documentation or tests for find_audio_service_plugins
rg "find_audio_service_plugins" -B 2 -A 5

Repository: OpenVoiceOS/ovos-audio

Length of output: 8676


🏁 Script executed:

# Look for exception handling patterns with plugin_manager imports elsewhere
rg "from ovos_plugin_manager" -A 5 | head -60

Repository: OpenVoiceOS/ovos-audio

Length of output: 4354


🏁 Script executed:

# Check if find_audio_service_plugins is documented or has type hints
fd -e py -x grep -l "find_audio_service_plugins" {} \;

Repository: OpenVoiceOS/ovos-audio

Length of output: 280


🏁 Script executed:

# Look for exception handling in production code around plugin_manager
rg "except.*Error\|except.*Exception" ovos_audio/ -B 2 -A 2

Repository: OpenVoiceOS/ovos-audio

Length of output: 48


🏁 Script executed:

# Check for any type hints or documentation about find_audio_service_plugins
rg "find_audio_service_plugins" ovos_audio/ -B 3 -A 3

Repository: OpenVoiceOS/ovos-audio

Length of output: 975


🏁 Script executed:

# Look at how other plugin discoveries are handled in the codebase
rg "plugin" ovos_audio/audio.py -B 2 -A 2 | head -80

Repository: OpenVoiceOS/ovos-audio

Length of output: 1778


🏁 Script executed:

# Check how the production code uses find_audio_service_plugins - does it have error handling?
sed -n '90,130p' ovos_audio/audio.py | cat -n

Repository: OpenVoiceOS/ovos-audio

Length of output: 2026


🏁 Script executed:

# Look for other optional plugin availability checks in tests
rg "@unittest.skip" test/ -B 3 -A 1

Repository: OpenVoiceOS/ovos-audio

Length of output: 1010


🏁 Script executed:

# Check if ovos_plugin_manager is listed as a dependency (optional or required)
cat pyproject.toml | grep -A 20 "dependencies\|optional"

Repository: OpenVoiceOS/ovos-audio

Length of output: 991


🏁 Script executed:

# Check if there's any exception handling pattern around find_audio_service_plugins elsewhere
rg -A 10 "load_services\|find_audio_service_plugins" ovos_audio/audio.py | grep -A 10 "def load_services"

Repository: OpenVoiceOS/ovos-audio

Length of output: 48


🏁 Script executed:

# Look at the actual call to find_audio_service_plugins in production code
sed -n '98,120p' ovos_audio/audio.py | cat -n

Repository: OpenVoiceOS/ovos-audio

Length of output: 1245


🏁 Script executed:

# Get the full load_services method to see if there's any exception handling
sed -n '98,150p' ovos_audio/audio.py | cat -n

Repository: OpenVoiceOS/ovos-audio

Length of output: 2592


🏁 Script executed:

# Check if find_audio_service_plugins documentation or source hints at possible exceptions
rg "ImportError|ModuleNotFoundError" ovos_audio/ test/ -B 2 -A 1

Repository: OpenVoiceOS/ovos-audio

Length of output: 2326


Restrict exception handler to import failures only.

The broad except Exception: masks plugin discovery failures as "plugin unavailable" and silently skips the entire test class during collection. Because find_audio_service_plugins() is called without error handling in production code, only catch the expected import-related exceptions to surface real regressions.

Proposed fix
 def _simple_plugin_available() -> bool:
     try:
         from ovos_plugin_manager.audio import find_audio_service_plugins
         return "ovos_simple" in find_audio_service_plugins()
-    except Exception:
+    except (ImportError, ModuleNotFoundError):
         return False
🧰 Tools
🪛 Ruff (0.15.5)

[warning] 30-30: Do not catch blind exception: Exception

(BLE001)

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

In `@test/unittests/test_end2end.py` around lines 26 - 34, The helper
_simple_plugin_available currently swallows all exceptions which hides real
plugin discovery errors; change the broad except Exception: to only catch import
failures (e.g. except (ImportError, ModuleNotFoundError):) so that import issues
still cause the function to return False but other exceptions from
find_audio_service_plugins propagate and surface test failures; keep the import
and call to find_audio_service_plugins and the unittest.skipUnless usage
unchanged.

ovos_utils.signal was removed in ovos-utils>=0.8.5a1.  test_utils.py
imports create_signal/check_for_signal at module level, causing
ModuleNotFoundError at collection time and blocking all other tests.

Guard the import with try/except and add @skipUnless so the tests are
skipped cleanly on newer ovos-utils instead of crashing collection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added feature and removed feature labels Mar 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/unittests/test_utils.py`:
- Around line 22-28: The current try/except wraps multiple imports and masks
real ImportError/AttributeError from ovos_audio.utils or ovos_utils.file_utils;
change the code to import ovos_audio.utils (wait_while_speaking, is_speaking,
stop_speaking) and ovos_utils.file_utils.get_temp_path normally (so real import
errors fail tests), and put a narrow try/except only around "from
ovos_utils.signal import create_signal, check_for_signal" to set
_SIGNAL_API_AVAILABLE; keep the _SIGNAL_API_AVAILABLE symbol and ensure
class-level skip logic only depends on that signal flag so tests that only mock
ovos_audio.utils (e.g., test_stop_speaking*) are not skipped.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7e8d2e0-0ce0-4e05-82d6-c70d923b526c

📥 Commits

Reviewing files that changed from the base of the PR and between f77487f and 8a66e65.

📒 Files selected for processing (1)
  • test/unittests/test_utils.py

Comment on lines +22 to +28
try:
from ovos_utils.signal import create_signal, check_for_signal
from ovos_utils.file_utils import get_temp_path
from ovos_audio.utils import wait_while_speaking, is_speaking, stop_speaking
_SIGNAL_API_AVAILABLE = True
except (ImportError, AttributeError):
_SIGNAL_API_AVAILABLE = False

Copy link
Copy Markdown
Contributor

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:

#!/bin/bash
set -euo pipefail

# Confirm which tests in this file actually depend on the signal helpers.
rg -n -C2 'create_signal|check_for_signal|stop_speaking|wait_while_speaking|is_speaking' test/unittests/test_utils.py

# Inspect the production helpers to verify which ones truly require signal support.
rg -n -C4 -g '*.py' 'def (stop_speaking|is_speaking|wait_while_speaking)\('

Repository: OpenVoiceOS/ovos-audio

Length of output: 2490


Narrow the guarded import and skip only the signal-dependent tests.

This now hides any ImportError/AttributeError raised while importing ovos_audio.utils or ovos_utils.file_utils, so a real regression there turns into a skipped class instead of a failing test. The class-level skipUnless also drops test_stop_speaking*, even though those tests do not directly depend on ovos_utils.signal (they only mock ovos_audio.utils functions).

Suggested fix
 try:
     from ovos_utils.signal import create_signal, check_for_signal
-    from ovos_utils.file_utils import get_temp_path
-    from ovos_audio.utils import wait_while_speaking, is_speaking, stop_speaking
-    _SIGNAL_API_AVAILABLE = True
 except (ImportError, AttributeError):
     _SIGNAL_API_AVAILABLE = False
+else:
+    _SIGNAL_API_AVAILABLE = True
+
+from ovos_utils.file_utils import get_temp_path
+from ovos_audio.utils import wait_while_speaking, is_speaking, stop_speaking
@@
-@unittest.skipUnless(_SIGNAL_API_AVAILABLE, "ovos_utils.signal removed in ovos-utils>=0.8.5")
 class TestInterface(unittest.TestCase):
@@
+    `@unittest.skipUnless`(_SIGNAL_API_AVAILABLE, "ovos_utils.signal removed in ovos-utils>=0.8.5")
     def test_is_speaking(self):
@@
+    `@unittest.skipUnless`(_SIGNAL_API_AVAILABLE, "ovos_utils.signal removed in ovos-utils>=0.8.5")
     def test_wait_while_speaking(self):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_utils.py` around lines 22 - 28, The current try/except
wraps multiple imports and masks real ImportError/AttributeError from
ovos_audio.utils or ovos_utils.file_utils; change the code to import
ovos_audio.utils (wait_while_speaking, is_speaking, stop_speaking) and
ovos_utils.file_utils.get_temp_path normally (so real import errors fail tests),
and put a narrow try/except only around "from ovos_utils.signal import
create_signal, check_for_signal" to set _SIGNAL_API_AVAILABLE; keep the
_SIGNAL_API_AVAILABLE symbol and ensure class-level skip logic only depends on
that signal flag so tests that only mock ovos_audio.utils (e.g.,
test_stop_speaking*) are not skipped.

JarbasAl and others added 2 commits March 11, 2026 13:26
Updated existing workflows:
- build_tests.yml: add system_deps (swig sox mpg123) for audio deps
- coverage.yml: replace custom 60-line script with reusable workflow
- publish_stable.yml: TigreGotico/gh-automations@master → OpenVoiceOS@dev
- release_workflow.yml: TigreGotico/gh-automations@master → OpenVoiceOS@dev

New reusable workflows added:
- lint.yml: ruff lint via gh-automations
- pip_audit.yml: CVE scan via gh-automations
- repo_health.yml: required-file checks via gh-automations
- release_preview.yml: next-version preview on PRs
- downstream_check.yml: track downstream dependents of ovos-audio

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Docs:
- AUDIT.md: fix typo "Pining" → "Pinning"
- FAQ.md: fix install/test paths (ovos-audio/ → . / test/)
- QUICK_FACTS.md: update version 1.1.1a2 → 1.1.2a1

Production bug fix:
- service.py:395: _maybe_reload_tts compared _fallback_tts_hash to ftts_m
  (module name string) instead of _ftts_hash (computed hash integer),
  causing the fallback TTS to always reload on every config check

Tests:
- test_playback_play.py: capture _now_playing[4] before _play() clears it;
  move stray `import unittest.mock` from after main() to top-level ANY import
- test_utils.py: narrow guarded import to only ovos_utils.signal and
  signal-based helpers; split into TestSignalInterface (skipped when
  signal unavailable) and TestStopSpeaking (also skipped — stop_speaking
  was removed with the signal API); stops hiding real ImportErrors from
  unrelated modules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@JarbasAl JarbasAl merged commit 0fb1e16 into dev Mar 11, 2026
14 checks passed
@JarbasAl JarbasAl deleted the modenize branch March 11, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant