Testing Skills with ovoscope¶
In a nutshell
ovoscope is the official tool for testing OpenVoiceOS skills and core parts. It runs a small,
fast, pretend version of the whole assistant in one process. This lets you confirm a request is
understood and answered correctly without real hardware. "End-to-end" just means it checks the
whole journey, from what the user said to what the assistant did. This page gets you started.
The full, always-current reference lives in the ovoscope repo's docs/. See the Glossary.
This page is a guide; the reference is in the repo
ovoscope is actively developed, so its detailed per-harness reference is kept in the tool's
own docs/ rather than copied here
(which would drift out of sync). This page gives you the concept and a working first test, then
links to the authoritative reference for each harness.
ovoscope provides a lightweight, in-process environment for running End-to-End (E2E) tests
without a full system installation. Every official OVOS skill is required to pass ovoscope E2E
tests.
Why E2E testing?¶
Traditional unit tests miss integration issues (for example, a skill that loads fine but fails to match a
Padacioso intent). ovoscope solves this by running a MiniCroft instance, a real in-process
SkillManager:
- Real intent matching: uses the actual Adapt and Padacioso engines.
- Bus-level verification: asserts the correct
speak/gui.page.showmessages are emitted. - No hardware: uses a
FakeBusand mocked audio/hardware layers. - CI ready: designed to run in GitHub Actions on every pull request.
Padatious is opt-in in CI
A skill-intent test suite does not boot Padatious by default. The shared ovoscope CI workflow
installs plain ovos-core plus Padacioso, and only pulls in ovos-padatious when the caller
sets require_padatious: true — see Test Your Skill.
A suite that genuinely needs Padatious opts in with that input, plus the matching plugin in its
own [test] extras.
Your first test¶
End2EndTest is declarative: give it the skill(s) to load, the utterance to send, and the messages
you expect back. The simplest assertion is assert_spoke(). It runs the interaction and checks the
skill spoke a given line, without spelling out the full message sequence:
from ovos_bus_client.message import Message
from ovos_bus_client.session import Session
from ovoscope import End2EndTest
SKILL_ID = "ovos-skill-hello-world.openvoiceos"
def test_hello_world():
session = Session("test-1")
session.pipeline = ["ovos-padacioso-pipeline-plugin"]
utterance = Message(
"recognizer_loop:utterance",
{"utterances": ["hello world"], "lang": "en-US"},
{"session": session.serialize(), "source": "A", "destination": "B"},
)
test = End2EndTest(
skill_ids=[SKILL_ID],
source_message=utterance,
expected_messages=[], # we are not checking the full sequence
test_message_number=False, # so don't assert on the message count
)
test.assert_spoke("Hello world") # runs execute() internally, then scans for speak
assert_spoke(text, lang="en-US", timeout=30) runs execute() and then raises AssertionError if
no speak message with that exact utterance (and lang) was emitted. Because it still runs the full
execute(), leave expected_messages empty and set test_message_number=False. Otherwise
execute() first asserts the received-message count equals the (empty) expected list. It then fails before
the speak is ever checked.
For full sequence assertions — message types, ordering, routing, and session state — populate
expected_messages and call test.execute() directly (see the
usage guide).
ovoscope also ships ready-made pipeline-stage lists, so you do not have to hand-write
session.pipeline = [...] for common cases: ADAPT_PIPELINE, PADACIOSO_PIPELINE,
FALLBACK_PIPELINE, PERSONA_PIPELINE, and DEFAULT_TEST_PIPELINE (a deterministic mix that
deliberately excludes persona/Ollama/OCP/m2v plugins). Import them from ovoscope and assign
to session.pipeline. See end2end-test.md
for the full list.
Testing skills still in development, and non-utterance triggers¶
Pass extra_skills={skill_id: SkillClass} to MiniCroft to load a skill class directly, with no
PyPI entry point required. This is useful for testing a skill before it is packaged.
To drive a non-utterance handler — a GUI event, a timer firing, an API call — call
MiniCroft.inject_message() instead of sending an utterance through the pipeline.
What ovoscope does not do¶
ovoscope tests at the recognizer_loop:utterance level and in-process. It does not:
- open a real WebSocket bus
- run the PHAL or audio services
- render the GUI
- perform real TTS
Keep this in mind when a test needs one of these — it needs the real stack (or a dedicated
harness), not ovoscope.
The harnesses (full reference in the repo)¶
ovoscope ships a harness per subsystem. Each is documented in the tool's
docs/:
| Harness / topic | What it tests | Reference |
|---|---|---|
MiniCroft |
The in-process core that loads your skills | minicroft.md |
End2EndTest |
Declarative single/multi-turn interactions | end2end-test.md · usage-guide.md |
CaptureSession |
Recording bus messages for assertion/replay | capture-session.md |
| Pipeline testing | Intent-pipeline matching | pipeline.md |
| OCP testing | Media ("play …") skills | ocp.md · media-testing.md |
| Audio service testing | TTS / sound playback lifecycle | audio-testing.md |
| Listener testing | The speech/listener service | listener.md · voice-loop.md |
WakeWordProbe |
Streams a single audio clip through a real HotWordEngine with listener-style silence priming, for per-clip wake-word accuracy checks |
ovoscope/wakeword_probe.py |
| PHAL testing | Hardware-abstraction plugins | phal.md |
| GUI testing | gui.page.show and GUI state |
gui-testing.md |
| Pydantic integration | Typed message assertions | pydantic-integration.md |
| Bus coverage | Which message types your test exercised | bus-coverage.md |
| CLI | The ovoscope command-line runner |
cli.md |
ovoscope-setup |
Companion command that installs the ovoscope skill for AI coding assistants (--claude, --gemini) |
repo docs |
The low-level FakeBus it builds on lives in ovos-utils (ovos_utils.fakebus).
MiniCroft training timeouts¶
get_minicroft() waits for mycroft.skills.trained to go quiet before returning. If training
never completes within OVOSCOPE_TRAINED_TIMEOUT seconds, it raises RuntimeError. That
timeout defaults to 180s when the CI environment variable is set, and 5s otherwise.
Override either default with the env var. Fleet CI runs under coverage instrumentation on
throttled 2-core runners, and a large single-skill intent set has exceeded 60s in the field.
Set pytest-timeout to at least the trained-wait ceiling plus 120s on any test suite that
uses get_minicroft. For single-language suites on the 180s CI default, use 300s or more.
For multilingual suites, set pytest-timeout to at least max_wait + 120s. A pytest-timeout
at or below the ceiling kills setup mid-wait, and the failure looks like a boot failure
instead of a timeout. This applies to the global timeout in pytest.ini or pyproject.toml
too, not just a per-test @pytest.mark.timeout: a low global floor kills setUpClass before
get_minicroft even returns, regardless of any per-test marker. Full detail:
minicroft.md.
Running in CI¶
Add the shared ovoscope workflow to your skill repo so the tests run on every pull request — see
CI Integration and the
manual's gh-automations page for the OVOS CI conventions.
Source code & full reference: OpenVoiceOS/ovoscope. See its docs/.
Read next: Skill Development Overview Related: Test Your Skill · Fallback Skill · Common Query Framework · Developer FAQ