Skip to content

feat(#160): test(ipc): create MockRuntime for deterministic agent testing#195

Merged
xsovad06 merged 5 commits into
mainfrom
feat/issue-160
Jun 19, 2026
Merged

feat(#160): test(ipc): create MockRuntime for deterministic agent testing#195
xsovad06 merged 5 commits into
mainfrom
feat/issue-160

Conversation

@xsovad06

Copy link
Copy Markdown
Owner

Summary

  • Created MockRuntime implementation of the AgentRuntime ABC to enable deterministic, subprocess-free testing of agent lifecycle management
  • Eliminates repetitive MagicMock + _empty_async_iter boilerplate scattered across dashboard and IPC tests
  • Provides configurable scenarios (success, failure, hang, custom output) through a clean MockAgentProcess interface that duck-types AgentProcess

Changes

New Test Infrastructure (sova/ipc/testing.py)

  • MockAgentProcess: AgentProcess-compatible mock with configurable exit codes, output streams, duration simulation, and hang scenarios using asyncio.Event
  • MockRuntime: Full AgentRuntime ABC implementation returning MockAgentProcess instances, with spawn tracking (spawned_processes) and prompt capture (last_prompt) for test assertions
  • All public methods match real AgentProcess interface: pid, is_running, returncode, wait(), stop(), stdout_lines(), stderr_lines(), classify_exit(), wait_classified()

Runtime Factory Registration (sova/ipc/runtime.py)

  • Registered "mock" in _RUNTIMES dict with lazy import to avoid test-only code in production paths
  • Modified create_runtime() to handle callable factory functions for lazy initialization

Test Coverage (tests/test_ipc.py)

  • TestMockAgentProcess: 6 scenarios covering immediate success, delayed completion, crash exit codes, hung process + stop, stdout/stderr streaming, static classify_exit()
  • TestMockRuntime: 5 scenarios covering factory creation, spawn tracking, prompt capture, parse_output(), check_available()

Review Guidance

  • Duck typing vs inheritance: MockAgentProcess does NOT subclass AgentProcess because the real class requires an asyncio.subprocess.Process in __init__. Verify the public interface match is complete (all 10 members).
  • Lazy import pattern: Check that _RUNTIMES["mock"] uses a lambda wrapper to defer import of testing.py until create_runtime("mock") is called -- avoids test-only imports in production.
  • Hang simulation correctness: Verify should_hang=True + stop() interaction -- wait() must block on an asyncio.Event that stop() releases immediately, with no real signal handling or deadlock risk.
  • Stream simulation: Confirm stdout_lines() / stderr_lines() async iterators return immediately when data lists are empty or None, matching real AgentProcess behavior.

Test Plan

  • All new tests pass: pytest tests/test_ipc.py::TestMockAgentProcess tests/test_ipc.py::TestMockRuntime (13 test methods)
  • Factory integration works: create_runtime("mock") returns MockRuntime instance
  • No lint warnings: make lint clean
  • Full suite passes: make check (1652+ tests)
  • Manual verification of hang scenario: asyncio.wait_for(process.wait(), timeout=0.5) raises TimeoutError when should_hang=True, completes after stop() called

Closes #160

@xsovad06 xsovad06 self-assigned this Jun 18, 2026
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds sova/ipc/testing.py with MockAgentProcess and MockRuntime for deterministic agent lifecycle testing without real subprocesses. Registers "mock" as a valid runtime type in create_runtime(). Adds TestMockAgentProcess and TestMockRuntime test classes covering all major behaviors.

Changes

MockRuntime testing utility

Layer / File(s) Summary
MockAgentProcess and MockRuntime implementation
sova/ipc/testing.py
New module with MockAgentProcess (async wait/stop, configurable duration/hang, stdout/stderr generators, exit classification) and MockRuntime (records prompts, returns MockAgentProcess from spawn, parses output into StreamEvent, always reports available).
create_runtime mock branch
sova/ipc/runtime.py
create_runtime() adds a "mock" special-case branch with lazy import of MockRuntime; updates the unknown-runtime error message to include "mock" in the available list.
TestMockAgentProcess and TestMockRuntime
tests/test_ipc.py
Two new async test classes covering MockAgentProcess lifecycle, exit classification, streaming, stop/hang unblock, and MockRuntime spawn tracking, prompt recording, output parsing, availability, factory creation via create_runtime("mock"), and per-spawn stdout isolation.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • xsovad06/sova#181: Both PRs modify sova/ipc/runtime.py's create_runtime() dispatch logic; this PR adds a "mock" runtime case extending the runtime selection abstraction.
  • xsovad06/sova#192: Both PRs center on the AgentRuntime/create_runtime() factory in sova/ipc/runtime.py; this PR extends factory support with the MockRuntime type.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding MockRuntime for deterministic agent testing, with issue reference.
Description check ✅ Passed The description covers summary, changes, review guidance, and test plan. All required template sections are present and substantively filled.
Linked Issues check ✅ Passed All #160 objectives are met: MockAgentProcess and MockRuntime classes created with full interface match, factory registration with lazy import added, comprehensive tests included, and boilerplate elimination addressed.
Out of Scope Changes check ✅ Passed All changes directly support #160: new testing module, runtime factory registration, and test coverage. No unrelated modifications detected.

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


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

coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 18, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sova/ipc/testing.py`:
- Around line 58-69: The issue is that when wait() is sleeping on
_duration_seconds and stop() is called, the sleep continues uninterrupted and
blocks until the full duration expires, even though _returncode has already been
set. To fix this, introduce a cancellation event (similar to the existing
_hang_release event) that can interrupt the sleep. In the wait() method, replace
the await asyncio.sleep(_duration_seconds) call with asyncio.wait() using both
the duration timeout and the new cancellation event, so the sleep can be
interrupted early. In the stop() method, set this cancellation event to wake up
any in-flight wait() call, allowing it to return immediately instead of blocking
on the remaining sleep duration.

In `@tests/test_ipc.py`:
- Around line 1333-1335: The stopper task created with
asyncio.create_task(stopper()) is not being tracked or awaited, which can hide
exceptions and leave background tasks in the event loop. Store the task returned
by asyncio.create_task(stopper()) in a variable before the proc.wait() call,
then await that task variable after the assertion to ensure it completes
properly and any exceptions are surfaced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f8dd9f94-6d0d-49c0-b13f-c1ab710e836c

📥 Commits

Reviewing files that changed from the base of the PR and between e0230ea and 4280eee.

⛔ Files ignored due to path filters (2)
  • .githooks/pre-push is excluded by none and included by none
  • docs/roadmap.html is excluded by !docs/** and included by none
📒 Files selected for processing (3)
  • sova/ipc/runtime.py
  • sova/ipc/testing.py
  • tests/test_ipc.py

Comment thread sova/ipc/testing.py
Comment thread tests/test_ipc.py Outdated

@xsovad06 xsovad06 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Code Review for PR #195

Critical bug: MockAgentProcess.stop() fails to interrupt wait() when duration_seconds > 0, causing stop() to appear non-functional for timed processes. The wait() continues sleeping for the full duration instead of returning immediately when stopped. This violates the spec and lacks test coverage. Additional issues: spawned_processes returns mutable internal state, and _finished event is dead code.

4 findings (4 inline, 0 in summary)

Sev Category File Finding
8/10 bug sova/ipc/testing.py:58 MockAgentProcess.stop() does not interrupt wait() when duration_seconds > 0 and should_hang=False. If wait() is sleeping via asyncio.sleep(), calling stop() sets _returncode but doesn't interrupt the sleep. The wait() continues for the full duration_seconds instead of returning immediately, violating the spec requirement that stop() 'just unblocks wait()'.
6/10 testing tests/test_ipc.py:1328 Missing test case for stop() interrupting wait() when duration_seconds > 0. The test suite covers should_hang=True (test_hang_and_stop) but not the scenario where a process with duration_seconds > 0 is stopped mid-sleep. This gap masks the bug where stop() doesn't actually interrupt the sleep.
4/10 design sova/ipc/testing.py:150 spawned_processes property returns the internal _spawned list directly, allowing external code to mutate the runtime's state (e.g. test could call rt.spawned_processes.clear()). Best practice for collection properties is to return a copy.
2/10 design sova/ipc/testing.py:43 _finished asyncio.Event is created and set in wait() and stop() but never awaited anywhere. This is dead code with no observable effect.

Assessment: BLOCK -- critical issues must be fixed before merge

Comment thread sova/ipc/testing.py
Comment thread tests/test_ipc.py
Comment thread sova/ipc/testing.py
Comment thread sova/ipc/testing.py Outdated
@xsovad06 xsovad06 dismissed coderabbitai[bot]’s stale review June 19, 2026 07:51

Findings addressed in latest push.

coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 19, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sova/ipc/testing.py`:
- Around line 75-80: The `timeout` parameter in the `MockAgentProcess.stop()`
method is unused and flagged by Ruff with ARG002. To fix this while maintaining
interface compatibility, prefix the parameter name with an underscore (rename
`timeout` to `_timeout`) to explicitly signal to Ruff that this parameter is
intentionally unused.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 14fffd32-c85a-4075-bac0-8d1c93acf7d5

📥 Commits

Reviewing files that changed from the base of the PR and between 4280eee and 1561fb2.

📒 Files selected for processing (2)
  • sova/ipc/testing.py
  • tests/test_ipc.py

Comment thread sova/ipc/testing.py
@xsovad06 xsovad06 dismissed coderabbitai[bot]’s stale review June 19, 2026 10:08

Findings addressed in latest push.

xsovad06 added 5 commits June 19, 2026 16:22
The pre-push hook ran 5 checks (ShellCheck, Ruff, format, commands,
permissions) but CI runs 7 additional invariant scripts. This gap
allowed a commit-format violation to pass locally but fail in CI.
Add all invariant scripts as check 6/6 in the pre-push hook.
Replace --> with ~~> in HTML comments to prevent Mermaid from
interpreting instruction examples as actual graph edges.
@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sova/ipc/testing.py`:
- Around line 126-144: In the spawn method of the MockAgentRuntime class, prefix
the unused parameters cwd, env, model, and max_budget_usd with an underscore
(e.g., _cwd, _env, _model, _max_budget_usd) to signal to Ruff that these
parameters are intentionally unused while maintaining interface compatibility
with the AgentRuntime.spawn() abstract method signature. This suppresses the
ARG002 violations without changing the method's functionality.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 77c123db-9d2b-434e-8f7d-13f8b5c1ca8c

📥 Commits

Reviewing files that changed from the base of the PR and between 0fe6a5c and c87f73e.

⛔ Files ignored due to path filters (2)
  • .githooks/pre-push is excluded by none and included by none
  • docs/roadmap.html is excluded by !docs/** and included by none
📒 Files selected for processing (3)
  • sova/ipc/runtime.py
  • sova/ipc/testing.py
  • tests/test_ipc.py

Comment thread sova/ipc/testing.py
Comment on lines +126 to +144
async def spawn(
self,
prompt: str,
cwd: str | Path,
*,
env: dict[str, str] | None = None,
model: str | None = None,
max_budget_usd: Decimal | None = None,
) -> MockAgentProcess:
self._last_prompt = prompt
process = MockAgentProcess(
stdout_lines_data=list(self._stdout_lines) if self._stdout_lines else None,
stderr_lines_data=list(self._stderr_lines) if self._stderr_lines else None,
exit_code=self._exit_code,
duration_seconds=self._duration_seconds,
should_hang=self._should_hang,
)
self._spawned.append(process)
return process

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Unused parameters flagged by Ruff but required for interface compatibility.

The parameters cwd, env, model, and max_budget_usd are unused in this mock implementation but required by the AgentRuntime.spawn() abstract method signature. Ruff flags these as ARG002 violations. Prefix them with underscore to signal they're intentionally unused while maintaining interface compliance.

♻️ Proposed fix
     async def spawn(
         self,
         prompt: str,
-        cwd: str | Path,
+        _cwd: str | Path,
         *,
-        env: dict[str, str] | None = None,
-        model: str | None = None,
-        max_budget_usd: Decimal | None = None,
+        _env: dict[str, str] | None = None,
+        _model: str | None = None,
+        _max_budget_usd: Decimal | None = None,
     ) -> MockAgentProcess:
         self._last_prompt = prompt
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 129-129: Unused method argument: cwd

(ARG002)


[warning] 131-131: Unused method argument: env

(ARG002)


[warning] 132-132: Unused method argument: model

(ARG002)


[warning] 133-133: Unused method argument: max_budget_usd

(ARG002)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sova/ipc/testing.py` around lines 126 - 144, In the spawn method of the
MockAgentRuntime class, prefix the unused parameters cwd, env, model, and
max_budget_usd with an underscore (e.g., _cwd, _env, _model, _max_budget_usd) to
signal to Ruff that these parameters are intentionally unused while maintaining
interface compatibility with the AgentRuntime.spawn() abstract method signature.
This suppresses the ARG002 violations without changing the method's
functionality.

Source: Linters/SAST tools

@xsovad06 xsovad06 merged commit 04b8152 into main Jun 19, 2026
17 checks passed
@xsovad06 xsovad06 deleted the feat/issue-160 branch June 19, 2026 14:26
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.

test(ipc): create MockRuntime for deterministic agent testing

1 participant