Skip to content

fix: Han audit round 1 — teardown safety, session isolation, harness lifecycle - #118

Merged
JarbasAl merged 16 commits into
devfrom
audit/han-round1
Jul 31, 2026
Merged

fix: Han audit round 1 — teardown safety, session isolation, harness lifecycle#118
JarbasAl merged 16 commits into
devfrom
audit/han-round1

Conversation

@JarbasAl

@JarbasAl JarbasAl commented Jul 31, 2026

Copy link
Copy Markdown
Member

Fixes 16 defect groups found by an automated expert audit (behavioral, concurrency, and resilience analysts) of the ovoscope package:

Teardown & global state

  • End2EndTest.execute() / from_message() / MockOCPTest now tear down MiniCroft in finally — a failing assertion no longer leaks patched process-globals (SessionManager.bus, Configuration, pipeline/blacklists) into later tests
  • The process-wide default Session is snapshotted at boot and fully restored in stop() (incl. active_skills mutated via inject_active or session-less messages); fixes the pre-existing test_default_pipeline_overrides_default_session failure
  • Mock-TTS timers are tracked, daemonized, and cancelled in stop() — no more orphaned timers emitting on a closed bus and corrupting the global SessionManager mid-later-test
  • _shared_minicroft (intent_cases) keeps at most one live cached instance and stops them at exit; m2v sync listeners are removed and the 3.5s pad only applies when no activity was seen

Correctness of verdicts and reports

  • Bus-coverage reports now use a per-test delta instead of session-cumulative counts (later tests were inflated by earlier tests' traffic)
  • PipelineHarness.match(): mycroft.skill.handler.start no longer treated as failure (it fires on success); real matches win over concurrent failure signals; the 20Hz-forever watcher thread is gone; assert_no_match now fails on timeout instead of passing vacuously
  • CaptureSession: atomic eof-counter reset, capture timeout surfaced as an explicit "capture timed out" assertion instead of a baffling count mismatch, finish() returns a copy
  • cmd_run reuses the already-booted MiniCroft instead of booting a second one over the same globals
  • OCP HTTP mock now sees the request URL (previously no configured URL ever matched); OCP execute waits on the query response event instead of sleeping half the timeout

Error propagation & resource lifecycle

  • PHAL plugin load failures raise by default (tolerate_load_errors=False), with load errors quoted in assert failures when tolerated
  • wait_for_match gained an emit= parameter (docstring previously described an impossible single-threaded call order); match/failure race hardened
  • Harness __exit__s close buses and remove capture handlers even when shutdown raises (audio, PHAL, voice_loop, listener)
  • MiniSimpleListener.feed_file replaces a wedged listener thread instead of reusing it (silent cross-run message pollution)
  • coverage.py records pyproject/setup parse failures in the report instead of silently understating coverage
  • RemoteRecorder.connect() closes the bus client on timeout instead of leaking a reconnecting thread

Tests: 40 adversarial regression tests (one per defect, written to fail against pre-fix code). Note: the local shared venv gained unrelated editable plugin installs mid-campaign that make some MiniCroft boots slow/flaky locally (fails on pristine dev too); clean CI is the arbiter here. Known upstream follow-up tracked for round 2: MiniCroft instances retain ~650MB after stop() (reference leak).

Bugs found by automated expert audit; fixes implemented by Claude (opus), orchestrated by Claude Fable.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added clearer pipeline outcomes for matches, explicit no-match results, and timeouts.
    • Added configurable handling for plugin load errors.
    • Coverage reports now include parsing errors.
    • Added support for TOML parsing on Python 3.10.
  • Bug Fixes

    • Improved timeout, event matching, playback, listener, recording, and teardown reliability.
    • Prevented state, messages, timers, and coverage data from leaking between test runs.
    • Improved HTTP mocking and synchronous response handling.
    • Added comprehensive regression coverage for lifecycle and error scenarios.

JarbasAl and others added 14 commits July 31, 2026 11:24
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>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b97cfb42-6594-4573-a639-babf7b0ea64c

📥 Commits

Reviewing files that changed from the base of the PR and between 4d4d58e and e51811a.

📒 Files selected for processing (19)
  • CHANGELOG.md
  • ovoscope/__init__.py
  • ovoscope/audio.py
  • ovoscope/bus_coverage.py
  • ovoscope/cli.py
  • ovoscope/coverage.py
  • ovoscope/e2e.py
  • ovoscope/intent_cases.py
  • ovoscope/listener.py
  • ovoscope/ocp.py
  • ovoscope/phal.py
  • ovoscope/pipeline.py
  • ovoscope/remote_recorder.py
  • ovoscope/simple_listener.py
  • ovoscope/voice_loop.py
  • pyproject.toml
  • test/unittests/test_audit_round1.py
  • test/unittests/test_global_bus_coverage.py
  • test/unittests/test_phal.py

📝 Walkthrough

Walkthrough

The PR hardens MiniCroft and harness lifecycle cleanup, improves event matching and timeout reporting, isolates coverage tracking, exposes parser failures, clarifies PHAL load-error handling, and adds regression tests for these behaviors.

Changes

Harness reliability

Layer / File(s) Summary
Core lifecycle and capture state
ovoscope/__init__.py, ovoscope/cli.py, ovoscope/intent_cases.py, test/unittests/test_audit_round1.py, CHANGELOG.md
MiniCroft restores session state and cancels TTS timers. Capture reports timeout state and returns response snapshots. End-to-end execution guarantees teardown. Shared instances are replaced and stopped safely.
Harness resource cleanup
ovoscope/audio.py, ovoscope/listener.py, ovoscope/simple_listener.py, ovoscope/voice_loop.py, ovoscope/remote_recorder.py, test/unittests/test_audit_round1.py
Harnesses restore queues, close buses, detach listeners, replace wedged listener threads, and clear timed-out recorder clients.
Event matching and OCP responses
ovoscope/pipeline.py, ovoscope/e2e.py, ovoscope/ocp.py, test/unittests/test_audit_round1.py
Matching distinguishes matched, explicit failure, and timeout outcomes. Event registration precedes optional emission. OCP responses use synchronized listeners and URL-aware mocks.
Coverage and parser diagnostics
ovoscope/bus_coverage.py, ovoscope/coverage.py, pyproject.toml, test/unittests/test_audit_round1.py, test/unittests/test_global_bus_coverage.py
Coverage uses invocation deltas and reports manifest parse errors. TOML parsing supports tomli on Python versions below 3.11.
PHAL loading policy
ovoscope/phal.py, test/unittests/test_audit_round1.py, test/unittests/test_phal.py
Plugin load failures now fail by default or are recorded and warned about when tolerated. Teardown attempts each cleanup operation independently.
M2V synchronization
ovoscope/intent_cases.py
M2V warmup uses named listeners, quiet-period detection, a no-event fallback delay, and guaranteed listener removal.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PipelineHarness
  participant MessageBus
  participant wait_for_match
  participant OCPTest
  PipelineHarness->>MessageBus: Emit utterance
  PipelineHarness->>wait_for_match: Wait for expected messages
  wait_for_match->>MessageBus: Register handlers
  MessageBus-->>wait_for_match: Return match or failure
  OCPTest->>MessageBus: Register query listeners
  MessageBus-->>OCPTest: Return query response
Loading

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch audit/han-round1

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

❤️ Share

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

@github-actions github-actions Bot added the fix label Jul 31, 2026
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

Tada! The results of the latest automation run are here. 🎉

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

📋 Repo Health

Ensuring the repo's skin is clear (aka linting errors). ✨

✅ All required files present.

Latest Version: 1.6.1a1

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

🔍 Lint

A quick update on the status of your PR. 🔔

ruff: issues found — see job log

🔒 Security (pip-audit)

The security sentinel has finished its patrol. 💂‍♂️

✅ No known vulnerabilities found (79 packages scanned).

🏷️ Release Preview

I've checked the 'Security Updates' section. 🛡️

Current: 1.6.1a1Next: 1.6.2a1

Signal Value
Label fix
PR title fix: Han audit round 1 — teardown safety, session isolation, harness lifecycle
Bump build

✅ PR title follows conventional commit format.


🚀 Release Channel Compatibility

Predicted next version: 1.6.2a1

Channel Status Note Current Constraint
Stable Not in channel -
Testing Too new (must be <1.0.0) ovoscope>=0.7.2,<1.0.0
Alpha Compatible ovoscope>=1.6.1a1

🔨 Build Tests

Construction of your features is officially finished. 🏠

✅ All versions pass

Python Build Install Tests
3.10
3.11
3.12
3.13
3.14

📊 Coverage

The coverage report is now available for inspection. 📋

56.6% total coverage

Files below 80% coverage (18 files)
File Coverage Missing lines
ovoscope/simple_listener.py 0.0% 63
ovoscope/tts_intelligibility.py 0.0% 190
ovoscope/version.py 0.0% 5
ovoscope/classic_listener.py 18.2% 117
ovoscope/intent_cases.py 21.4% 151
ovoscope/cli.py 39.9% 146
ovoscope/pytest_plugin.py 41.4% 222
ovoscope/e2e.py 43.7% 80
ovoscope/ocp.py 47.9% 61
ovoscope/listener.py 56.9% 125
ovoscope/media.py 57.2% 101
ovoscope/voice_loop.py 58.9% 118
ovoscope/__init__.py 60.1% 335
ovoscope/pipeline.py 64.1% 46
ovoscope/media_provider.py 64.9% 20
ovoscope/audio.py 65.0% 117
ovoscope/coverage.py 72.2% 57
ovoscope/wakeword_probe.py 75.0% 20

Full report: download the coverage-report artifact.

⚖️ License Check

I've verified the license compliance for your changes. ✅

✅ No license violations found.

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


An automated high-five for your latest changes! 🖐️

JarbasAl and others added 2 commits July 31, 2026 15:57
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>
@JarbasAl
JarbasAl marked this pull request as ready for review July 31, 2026 15:45
@JarbasAl
JarbasAl merged commit f20f6fc into dev Jul 31, 2026
13 checks passed
@JarbasAl
JarbasAl deleted the audit/han-round1 branch July 31, 2026 15:45
@github-actions github-actions Bot added fix and removed fix labels Jul 31, 2026
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