Fix #433: allow test_context() for sink-less (non-yielding) agents - #699
Merged
Conversation
Agent.test_context() wraps the agent in an AgentTestWrapper that always
added an internal results sink. For an agent that never yields, that sink
tripped _prepare_actor's ImproperlyConfigured('Agent must yield to use
sinks') at startup, so test_context() could not be used to unit-test
sink-less agents at all.
Detect whether the wrapped agent yields (inspect.isasyncgenfunction on
its function). Only attach the results sink when it does; for a non-yielding
agent, observe processed values with a stream processor instead. The
processor records each value and wakes any put(wait=True) caller, so the
test wrapper behaves the same from the caller's point of view without
forcing the agent to yield.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHPL4VFWQRQPpjR1gXSKyL
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #699 +/- ##
=======================================
Coverage 95.98% 95.98%
=======================================
Files 103 103
Lines 11071 11072 +1
Branches 1191 1191
=======================================
+ Hits 10627 10628 +1
Misses 350 350
Partials 94 94 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
wbarnha
added this pull request to the merge queue
Jul 23, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Jul 23, 2026
The merge of master into this branch produced a semantic conflict that git could not detect: this branch's new test_context__sinkless_agent carried a @pytest.mark.skipif(platform.python_implementation() == "PyPy", ...) guard, while master's #716 ("full PyPy support -- all PyPy skips removed") deleted every PyPy skip in this file along with the now-unused `import platform`. Both sides touched different lines, so the merge applied cleanly and left a use of `platform` with no import. The decorator is evaluated at class-body/import time, so pytest aborted during collection with NameError: name 'platform' is not defined, taking down the whole suite (exit 2) on every aiokafka leg, and flake8 flagged F821 in the lint job. Remove the decorator rather than re-adding the import: #716 made PyPy a fully supported target, so this test should run there like the rest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017K8xAH8Z3xWKHhNRCG2mg1
wbarnha
pushed a commit
that referenced
this pull request
Aug 4, 2026
master's #716 ("full PyPy support -- all PyPy skips removed") deleted every PyPy skip in tests/functional/test_streams.py together with the now-unused `import platform`. This branch's new test_take__records_event_runtime still carried a @pytest.mark.skipif(platform.python_implementation() == "PyPy", ...) guard. Because the two sides touch different lines, merging master would have applied cleanly and left a use of `platform` with no import -- the decorator is evaluated at import time, so pytest would abort during collection with NameError and take the whole suite down on every aiokafka leg, plus F821 in the lint job. (PR #699 hit exactly this after its master merge.) Remove the decorator rather than keeping the import: #716 made PyPy a fully supported target, so this test should run there like the rest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017K8xAH8Z3xWKHhNRCG2mg1
The testing guide only ever demonstrated agents that ``yield``, and read results back with ``agent.results``. Nothing covered the sink-less agents this PR makes testable -- pure consumers that update a table, call a service or forward to another topic and never yield -- so there was no guidance for exactly the case #433 unblocks. Add two sections to docs/userguide/testing.rst: - "Setting up a test suite": the ``app`` fixture a suite needs, explaining what each line is for (re-binding the app to the test's event loop, the in-memory store, resuming flow control -- omit it and ``put()`` hangs), plus the ``asyncio_mode`` setting, and why every outbound dependency needs mocking (an unmocked ``send`` reaches for a real broker and fails with KafkaConnectionError). Notes that the existing ``test_app`` fixture's ``event_loop`` argument is deprecated by recent pytest-asyncio. - "Testing agents that don't yield": a complete example app whose agent is a pure consumer, and the test suite for it -- asserting on the table it wrote, the service it called and the message it forwarded, rather than on a return value. Two behaviours are documented because they are easy to get wrong, and both were verified by running them: - ``agent.results`` means different things per agent: for a yielding agent it holds the value yielded (output), for a sink-less agent the value sent in (input), since there is no output to capture. - ``await put(...)`` returns with side effects already applied when the agent body does not await mid-loop, but can return early when it does; wait for the effect or leave the ``async with`` block instead of asserting blindly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017K8xAH8Z3xWKHhNRCG2mg1
The guide described the pieces of a test suite in prose, but nobody could lift them straight into a project: the conftest was split across two code blocks, the files were never named, and the reader had to stitch the app, the fixtures and the tests together themselves. Restructure "Setting up a test suite" around four complete, named files -- myapp.py, pytest.ini, conftest.py and test_myapp.py -- each in its own subsection, in the order you would create them. Copy the four blocks into an empty directory, install faust-streaming/pytest/pytest-asyncio, run pytest, and four tests pass with nothing else to fill in. The app under test keeps the sink-less agent, so the copy-paste suite doubles as the worked example for the case this PR unblocks: it updates a table, calls a service and forwards to a topic without ever yielding. The "Testing agents that don't yield" section now explains the semantics (agent.results holding input rather than output, and when put() returns) and points at those tests by name instead of repeating them, so there is one example rather than two half-examples. The code in the docs is the code that was run: the blocks were generated from files verified with pytest, then extracted back out of the .rst and run again from a clean directory -- byte-identical both ways, 4 passed. Sphinx builds the page cleanly and the new :ref: to the sink-less section resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017K8xAH8Z3xWKHhNRCG2mg1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Agent.test_context()wraps the agent in anAgentTestWrapperthat unconditionally added an internal "results" sink. For an agent that neveryields, that sink tripped_prepare_actor's guard:…which is raised at agent startup, so
test_context()could not be used to unit-test sink-less agents at all — you had to add a throwawayyieldjust to make the harness work.Fixes #433.
How
Detect whether the wrapped agent actually yields, via
inspect.isasyncgenfunction(self.fun):_on_value_processed), which records yielded values and wakesput(wait=True).ImproperlyConfiguredguard is not tripped) and instead attach a stream processor. The processor records each incoming value intoresultsand notifiesnew_value_processed, soput(wait=True)still returns after the value is processed. The processor returns the value unchanged, leaving the agent's own iteration untouched.This mirrors the approach a maintainer suggested on the issue ("check in test_context if it is already yielding and if not … [handle it another way]"), without requiring the user to change their agent.
Test
Added
test_context__sinkless_agentintests/unit/agents/test_agent.py(alongside the existingtest_context_calls_sink): defines a non-yielding agent, enterstest_context(),await agent.put("hello"), and asserts the value was processed and recorded — a case that previously raisedImproperlyConfiguredat startup. Fulltests/unit/agents/test_agent.pypasses (73 passed, 1 skipped).🤖 Generated with Claude Code
Generated by Claude Code