Skip to content

Fix #433: allow test_context() for sink-less (non-yielding) agents - #699

Merged
wbarnha merged 5 commits into
masterfrom
claude/fix-433-sinkless-test-context
Aug 4, 2026
Merged

Fix #433: allow test_context() for sink-less (non-yielding) agents#699
wbarnha merged 5 commits into
masterfrom
claude/fix-433-sinkless-test-context

Conversation

@wbarnha

@wbarnha wbarnha commented Jul 19, 2026

Copy link
Copy Markdown
Member

What

Agent.test_context() wraps the agent in an AgentTestWrapper that unconditionally added an internal "results" sink. For an agent that never yields, that sink tripped _prepare_actor's guard:

faust.exceptions.ImproperlyConfigured: Agent must yield to use sinks

…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 throwaway yield just to make the harness work.

Fixes #433.

How

Detect whether the wrapped agent actually yields, via inspect.isasyncgenfunction(self.fun):

  • Yielding agent — unchanged: attach the results sink (_on_value_processed), which records yielded values and wakes put(wait=True).
  • Non-yielding agent — skip the sink (so the ImproperlyConfigured guard is not tripped) and instead attach a stream processor. The processor records each incoming value into results and notifies new_value_processed, so put(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_agent in tests/unit/agents/test_agent.py (alongside the existing test_context_calls_sink): defines a non-yielding agent, enters test_context(), await agent.put("hello"), and asserts the value was processed and recorded — a case that previously raised ImproperlyConfigured at startup. Full tests/unit/agents/test_agent.py passes (73 passed, 1 skipped).

🤖 Generated with Claude Code


Generated by Claude Code

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

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.98%. Comparing base (7456b62) to head (13eb70e).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@wbarnha
wbarnha added this pull request to the merge queue Jul 23, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 23, 2026
wbarnha and others added 2 commits August 3, 2026 16:47
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
@wbarnha
wbarnha merged commit 4acc180 into master Aug 4, 2026
30 checks passed
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.

Can't use agent.test_context() for sink-less agents

2 participants