Skip to content

Release 1.6.11a1 - #145

Open
github-actions[bot] wants to merge 78 commits into
masterfrom
release-1.6.11a1
Open

Release 1.6.11a1#145
github-actions[bot] wants to merge 78 commits into
masterfrom
release-1.6.11a1

Conversation

@github-actions

Copy link
Copy Markdown

Human review requested!

JarbasAl and others added 30 commits July 2, 2026 19:04
Added AI disclosure section to README.md outlining the use of AI tools in project development and the maintenance of public records.
Add a self-contained probe that drives a real HotWordEngine over a single clip
the way the listening loop does: a few seconds of leading silence to warm the
engine's rolling feature window (openWakeWord et al. only emit once it is full,
~2.5 s — too little lead silently drops short positives and biases false
rejects), then the clip streamed frame by frame, returning a detection decision
plus latency and frames-to-detection. Unlike MiniVoiceLoop it needs no bus or
[listener] extra — just the [bench] extra (numpy). Tolerates the
HotWordEngine(lang) signature and the vestigial found_wake_word(frame) arg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: per-clip WakeWordProbe for benchmark/test harnesses
…us-on-stop

fix: restore SessionManager.bus when MiniCroft stops
End2EndTest.execute() and from_message() only stopped the MiniCroft on the
success path, so a failing assertion left SessionManager.bus,
default_session and Configuration patched for every later test. Both now
run stop() from a finally block.

MiniCroft snapshots the whole default Session at boot and restores it in
stop(), so inject_active activations and wire-folded session values no
longer outlive the test that made them.

Mock-TTS unduck timers are tracked, made daemon and cancelled in stop().
An orphaned timer could otherwise emit onto a closed bus and fold a stale
session onto the global SessionManager during a later test.

CaptureSession resets its eof state atomically, records a timed_out flag,
and returns a copy from finish(). A capture timeout now fails with a clear
message instead of surfacing as a message-count mismatch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BusCoverageTracker snapshotted the session-cumulative global collector and
added it into per-test counts, so every later test inherited the
invocations of every earlier one. The snapshot is now a baseline and the
report uses the delta over the tracker's own lifetime, frozen at
start_tracking() so the tracking window is not counted twice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cmd_run booted a MiniCroft but never assigned it to the test, so execute()
booted a second managed one and both patched the same globals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
match() treated mycroft.skill.handler.start as a failure signal, but it
fires on a SUCCESSFUL match — so a successful match returned None. It also
checked the failure flag before the captured message and spun a watcher
thread that polled at 20Hz forever after a timeout.

match_result() now returns a discriminated matched/no-match/timeout
outcome and waits on the events directly. assert_no_match() fails on a
timeout instead of passing vacuously; match() keeps its old signature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The docstring told callers to emit after calling the helper, which is
impossible single-threaded because the helper blocks. It now takes an
optional emit= message and sends it once the handlers are in place.

A match that raced an intent failure could also be dropped; appends are
guarded by a lock and re-read once before giving up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The side effect inspected `mock.url` on a MagicMock, so no configured URL
ever matched and json() always returned {}. It now lives on the patched
GET, which receives the URL.

OCPTest also waits for ovos.common_play.query.response instead of sleeping
half the timeout, and stops the MiniCroft from a finally block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AudioServiceHarness.__exit__ skipped bus.close() when shutdown() raised.
ListenerHarness and MiniListener left their wildcard "message" capture
handler on the bus, so a shared bus kept feeding a dead harness.

PlaybackServiceHarness now restores the TTS.queue object it replaced and
refuses a second concurrent harness, because TTS.queue is process-wide
class state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A load failure was warned about and skipped, then resurfaced much later as
an unrelated assert_emitted timeout. Loading now raises by default; pass
tolerate_load_errors=True to keep going, in which case the errors are kept
in load_errors and quoted in assert_emitted failures.

MiniPHAL.__exit__ also detaches its capture handler and closes the bus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cached MiniCrofts were never stopped and two could be live at once, each
clobbering the same globals. At most one stays live now, and an atexit
hook stops the rest.

_wait_for_m2v_sync removes its three listeners in a finally block and only
pays the 3.5s pad when no m2v activity was observed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feed_file ignored the join() result, so a listener thread that outlived
its stop() kept appending to _messages during the next run. A still-alive
thread is now logged and replaced with a fresh listener object.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`except (ImportError, Exception): pass` turned a malformed pyproject.toml
into an understated coverage number. TOMLDecodeError and OSError are now
caught explicitly and recorded in EcosystemCoverageReport.parse_errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The client was left in place on a ConnectionError, so its reconnect thread
lived for the rest of the process.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One test per defect, each written to fail against the pre-fix code:
teardown on the failure path, default-session isolation, TTS timer
lifecycle, bus-coverage deltas, CaptureSession races, pipeline match
verdicts, wait_for_match subscription order, the OCP HTTP mock, harness
teardown, PHAL load errors, coverage parse errors and the RemoteRecorder
connect leak.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
raise-by-default made the old warn-and-skip expectation wrong; cover
both the default raise and the tolerate_load_errors opt-out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI showed two gaps: the session restore bailed out when boot replaced
the default-session singleton, leaking exactly the state it exists to
scrub — restore now targets whatever object holds the role at stop()
time. And on Python 3.10 there is no stdlib tomllib, so a malformed
pyproject.toml was silently ignored — depend on the tomli backport
there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ovos-bus-client 1.x has serialize/deserialize, 2.x to_dict/from_dict;
the snapshot silently became None on 1.x and the restore no-opped.
Support both and warn instead of failing silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix: Han audit round 1 — teardown safety, session isolation, harness lifecycle
…n available

cmd_validate always ran _basic_validate, contradicting docs/cli.md which
documented it as preferring pydantic_helpers.validate_fixture when the
pydantic extra is importable. Match the code to the documented behaviour
instead of weakening the docs, and add tests covering both the
pydantic-available and fallback paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JarbasAl and others added 30 commits July 31, 2026 17:28
Round 2 switched the GUI assertions to exact matching with an opt-in
exact=False and added the gui.clear.namespace topic; this page still
taught the pre-fix substring behavior and omitted the parameter.

Found by the final adversarial verification (the only remaining
defect); fix by Claude Fable directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs: gui-testing.md matches the exact-match assertion semantics
The latest ovoscope forwards enable_installer/enable_file_watcher/
enable_intent_service/enable_event_scheduler/enable_skill_api to
SkillManager unconditionally. Older ovos-core releases (the stable
release channel ships 1.3.x) predate some of these keyword arguments,
so super().__init__ raised TypeError and MiniCroft could not boot at
all — making the latest ovoscope unusable against an older core, which
is exactly what the conformance harness exercises against the pinned
stable/testing stacks.

Forward only the enable_* flags the installed SkillManager actually
accepts (via signature introspection; a **kwargs signature accepts
all). One ovoscope now boots on every supported core.

Adds a regression test simulating an old SkillManager signature.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
) (#130)

OVOSSkill.get_response()/ask_yesno() (ovos-workshop 7.0.6) spawn a killable
background thread that re-prompts forever when num_retries=-1 (the default)
and nothing ever answers it. On a real voice satellite the listener/GUI
eventually emits mycroft.skills.abort_question on user silence; ovoscope's
synchronous FakeBus never generates one, so any skill handler that calls
get_response()/ask_yesno() without a queued follow-up utterance hangs the
calling thread indefinitely (OVOSSkill._wait_response(),
ovos_workshop/skills/ovos.py ~1802-1809 - "while not ans: time.sleep(0.1)",
no deadline).

MiniCroft now arms a short watchdog timer on
"skill.converse.get_response.enable" that emits the existing
"mycroft.skills.abort_question" bus message (no new message type) if
".disable" hasn't fired first - mirroring what a real listener/GUI would
send on silence. This lets get_response()/ask_yesno() resolve to None
promptly instead of looping every get_response_timeout (20s default)
forever.

Watchdog timers are keyed by (skill_id, session_id) - the same two-part
scope ovos-workshop's own @killable_event("mycroft.skills.abort_question",
check_skill_id=True) already uses to decide which stalled thread an abort
is for. An adversarial review of an earlier version of this fix caught a
real regression here: a flat list of pending timers meant ANY skill's
".disable" cancelled EVERY other skill's still-pending watchdog too, so in
a fleet-style MiniCroft running multiple skills concurrently, one skill
finishing its (answered) get_response() would silently disarm a different
skill's watchdog - if that second question was never answered, it hung
forever again, defeating the fix for exactly the multi-skill case it needs
to hold up under. Scoping by (skill_id, session_id) and cancelling only the
matching entry on ".disable" fixes it; all remaining timers are still
cancelled in stop() the same way the existing mock-TTS timers already are.

The existing nested-speak_dialog(wait=True) mock-TTS handshake
(recognizer_loop:audio_output_start/end) already resolves correctly for
nested calls via its own per-call Timer; a regression test pins that
behavior alongside the get_response fix and its concurrency scoping.

Field evidence: OpenVoiceOS/ovos-skill-alerts#138 "Update 3" - py-spy
captures showing OVOSSkill._real_wait_response threads parked forever on
the ovoscope FakeBus, and PR #123's CI job silently hanging to the
30-minute kill on 4 of 5 Python versions after the last test finished.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…deadlock (#132)

pyee's EventEmitter.remove_all_listeners() holds its internal, non-reentrant
lock while dropping self._events. If freeing the emitter's last reference to
a bound-method listener triggers the owner's __del__, and that __del__ calls
bus.remove()/remove_listener(), it re-enters the same lock from the same
thread and deadlocks permanently -- observed as 30-minute CI hangs in
skill-repo ovoscope teardowns.

MiniCroft.stop() now pops every key out of bus.ee._events itself, outside
pyee's lock, and runs gc.collect() before anything that can trigger
remove_all_listeners(), so any reentrant __del__ runs while the lock is
free. Same workaround as the minicroft fixture in
ovos-skill-application-launcher's test/end2end/test_intents_en_us.py
(merged PR #108, dev).

Added test/unittests/test_teardown_deadlock.py: a direct pyee-level test
proves the mechanism deadlocks without the drain and is fixed with it, and
a MiniCroft.stop() smoke guard reproduces the real hang against unpatched
code under full-suite state (though not reliably in isolation, since this
dependency stack's FakeBus.close() doesn't call remove_all_listeners() on
its own).
Producers emit canonical `ovos.*` spec topics since workshop#425, but
ovoscope's execute() captures via the bus catch-all, so a pre-spec
producer vintage on the wire can still deliver the legacy name instead.
Filters that hard-coded the legacy spelling against that captured
stream broke on canonical-only producers.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix: raise clear TypeError for bare-string expected_messages/expected_boot_sequence

execute() reads .msg_type/.data/.context/.serialize() off every entry of
expected_messages and expected_boot_sequence while walking the captured
message stream. Message objects have been the only accepted shape since
the first commit (git blame confirms `expected.msg_type == received.msg_type`
was already there in d3280ab), but nothing enforced it — a bare topic
string (e.g. expected_messages=["speak"]) fell through to an obscure
AttributeError deep in the assertion loop instead of failing at
construction with a message that names the actual mistake.

Checked real usage across the OpenVoiceOS org (gh search code) and the
docs/README/usage-guide examples: every current call site builds full
Message objects; the one example showing bare strings (SKILL.md) already
uses a stale/nonexistent kwarg shape and isn't representative. No API
restoration needed — just fail fast and clearly.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* docs: fix stale End2EndTest example in SKILL.md

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…142)

Real ovos-audio (ovos_audio/playback.py begin_audio/end_audio) emits
ovos.audio.output.started/ended unconditionally on every speech
begin/end. The harness's duck()/unduck() instead emitted the legacy
recognizer_loop:audio_output_start/end aliases, which was only ever
one consumer's compatibility binding and goes silent against an
ovos-media build that drops the legacy subscription.
…amespace bus surfaces (#144)

ovos-media is removing its in-core GUI integration and the per-namespace
service bus surface (ovos.{ns}.service.play routed through handle_play).
OCPMediaPlayer built without them no longer defines
ovos_media.player.GUIInterface or AudioService.handle_play, so the
harness's unconditional mock.patch()/bus.on() calls broke on start.
Guard both behind hasattr() checks so __enter__ (and playback through the
real-backend-factory path) succeeds against either generation of
ovos-media; pause/resume/stop remain plain methods on both generations
and are unaffected.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant