Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions tests/unittests/cli/plugins/test_replay_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ async def _make_invocation(
return invocation_context, contexts


@pytest.mark.asyncio
async def test_before_run_without_replay_config_leaves_plugin_inert(tmp_path):
"""No replay config means the plugin must not intercept anything."""
plugin = ReplayPlugin()
Expand All @@ -131,6 +132,7 @@ async def test_before_run_without_replay_config_leaves_plugin_inert(tmp_path):
assert tool.live_calls == []


@pytest.mark.asyncio
async def test_before_run_with_partial_replay_config_leaves_plugin_inert(
tmp_path,
):
Expand All @@ -152,6 +154,7 @@ async def test_before_run_with_partial_replay_config_leaves_plugin_inert(
assert tool.live_calls == []


@pytest.mark.asyncio
async def test_before_tool_returns_recorded_response_not_live_result(tmp_path):
"""The recorded response wins over whatever the live tool returns."""
_write_recordings(tmp_path, [_recording(response={'result': 4})])
Expand All @@ -167,6 +170,7 @@ async def test_before_tool_returns_recorded_response_not_live_result(tmp_path):
assert replayed == {'result': 4}


@pytest.mark.asyncio
async def test_before_tool_still_executes_the_underlying_tool(tmp_path):
"""Replay verifies the tool runs; only its response is substituted."""
_write_recordings(tmp_path, [_recording(args={'sides': 6})])
Expand All @@ -182,6 +186,7 @@ async def test_before_tool_still_executes_the_underlying_tool(tmp_path):
assert tool.live_calls == [{'sides': 6}]


@pytest.mark.asyncio
async def test_before_run_reads_the_sse_file_in_sse_streaming_mode(tmp_path):
"""streaming_mode selects which recordings file is authoritative."""
_write_recordings(
Expand Down Expand Up @@ -209,6 +214,7 @@ async def test_before_run_reads_the_sse_file_in_sse_streaming_mode(tmp_path):
assert replayed == {'result': 'streaming'}


@pytest.mark.asyncio
async def test_before_run_reads_the_plain_file_in_non_streaming_mode(tmp_path):
"""The mirror of the sse case, so a swapped file name cannot pass both."""
_write_recordings(
Expand Down Expand Up @@ -236,6 +242,7 @@ async def test_before_run_reads_the_plain_file_in_non_streaming_mode(tmp_path):
assert replayed == {'result': 'non-streaming'}


@pytest.mark.asyncio
async def test_before_run_unsupported_streaming_mode_raises_value_error(
tmp_path,
):
Expand All @@ -250,6 +257,7 @@ async def test_before_run_unsupported_streaming_mode_raises_value_error(
await plugin.before_run_callback(invocation_context=invocation_context)


@pytest.mark.asyncio
async def test_before_run_missing_recordings_file_raises_config_error(
tmp_path,
):
Expand All @@ -261,6 +269,7 @@ async def test_before_run_missing_recordings_file_raises_config_error(
await plugin.before_run_callback(invocation_context=invocation_context)


@pytest.mark.asyncio
async def test_before_run_unparsable_recordings_raise_config_error(tmp_path):
"""Schema violations surface as ReplayConfigError, not a pydantic error."""
(tmp_path / _NON_STREAMING_FILE).write_text(
Expand All @@ -275,6 +284,7 @@ async def test_before_run_unparsable_recordings_raise_config_error(tmp_path):
await plugin.before_run_callback(invocation_context=invocation_context)


@pytest.mark.asyncio
async def test_before_tool_without_loaded_state_raises_config_error(tmp_path):
"""Replaying without a preceding before_run is a misuse, not a silent pass."""
_write_recordings(tmp_path, [_recording()])
Expand All @@ -289,6 +299,7 @@ async def test_before_tool_without_loaded_state_raises_config_error(tmp_path):
)


@pytest.mark.asyncio
async def test_before_tool_tool_name_mismatch_raises_verification_error(
tmp_path,
):
Expand All @@ -311,6 +322,7 @@ async def test_before_tool_tool_name_mismatch_raises_verification_error(
assert 'flip_coin' in message


@pytest.mark.asyncio
async def test_before_tool_args_mismatch_raises_verification_error(tmp_path):
"""The recorded args must match exactly, not just the tool name."""
_write_recordings(tmp_path, [_recording(args={'sides': 6})])
Expand All @@ -330,6 +342,7 @@ async def test_before_tool_args_mismatch_raises_verification_error(tmp_path):
assert "'sides': 20" in message


@pytest.mark.asyncio
async def test_before_tool_beyond_recorded_calls_raises_verification_error(
tmp_path,
):
Expand All @@ -354,6 +367,7 @@ async def test_before_tool_beyond_recorded_calls_raises_verification_error(
assert 'Expected 1' in message


@pytest.mark.asyncio
async def test_before_tool_advances_a_separate_index_per_agent(tmp_path):
"""Each agent has its own replay index; a sibling's call must not shift it."""
_write_recordings(
Expand Down Expand Up @@ -394,6 +408,7 @@ async def test_before_tool_advances_a_separate_index_per_agent(tmp_path):
]


@pytest.mark.asyncio
async def test_before_tool_ignores_recordings_for_other_user_messages(
tmp_path,
):
Expand Down Expand Up @@ -432,6 +447,7 @@ async def test_before_tool_ignores_recordings_for_other_user_messages(
)


@pytest.mark.asyncio
async def test_after_run_discards_the_invocation_state(tmp_path):
"""Cleanup is observable: a later tool call no longer finds replay state."""
_write_recordings(tmp_path, [_recording(), _recording(call_id='fc-2')])
Expand Down
7 changes: 7 additions & 0 deletions tests/unittests/cli/test_cleanup_unused_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from unittest import mock

from google.adk.cli.built_in_agents.tools.cleanup_unused_files import cleanup_unused_files
import pytest


def _tool_context(root: Path) -> mock.MagicMock:
Expand All @@ -41,6 +42,7 @@ def _unused(result, root: Path) -> list[str]:
)


@pytest.mark.asyncio
async def test_cleanup_unused_files_reports_python_files_not_in_use(tmp_path):
_populate(tmp_path, ["used.py", "orphan.py", "pkg/nested_orphan.py"])

Expand All @@ -56,6 +58,7 @@ async def test_cleanup_unused_files_reports_python_files_not_in_use(tmp_path):
assert result["total_freed_space"] == 0


@pytest.mark.asyncio
async def test_cleanup_unused_files_applies_the_default_exclusions(tmp_path):
_populate(
tmp_path,
Expand All @@ -77,6 +80,7 @@ async def test_cleanup_unused_files_applies_the_default_exclusions(tmp_path):
assert _unused(result, tmp_path) == ["orphan.py"]


@pytest.mark.asyncio
async def test_cleanup_unused_files_honors_custom_patterns(tmp_path):
_populate(tmp_path, ["a.yaml", "b.yaml", "keep.py", "__init__.py"])

Expand All @@ -90,6 +94,7 @@ async def test_cleanup_unused_files_honors_custom_patterns(tmp_path):
assert _unused(result, tmp_path) == ["b.yaml"]


@pytest.mark.asyncio
async def test_cleanup_unused_files_matches_used_files_after_resolution(
tmp_path,
):
Expand All @@ -104,6 +109,7 @@ async def test_cleanup_unused_files_matches_used_files_after_resolution(
assert result["unused_files"] == []


@pytest.mark.asyncio
async def test_cleanup_unused_files_reports_a_missing_root_directory(tmp_path):
missing = tmp_path / "does_not_exist"

Expand All @@ -117,6 +123,7 @@ async def test_cleanup_unused_files_reports_a_missing_root_directory(tmp_path):
assert result["unused_files"] == []


@pytest.mark.asyncio
async def test_cleanup_unused_files_fails_closed_on_a_used_file_escape(
tmp_path,
):
Expand Down
1 change: 1 addition & 0 deletions tests/unittests/plugins/test_auto_tracing_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ def add_one(x: int) -> int:
assert span.attributes == {'adk.fn.arg.x': '3', 'adk.fn.return': '4'}


@pytest.mark.asyncio
async def test_build_tracing_wrapper_records_awaited_result():
span = _FakeSpan()

Expand Down
9 changes: 9 additions & 0 deletions tests/unittests/plugins/test_logging_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def _tool(name: str) -> BaseTool:
return tool


@pytest.mark.asyncio
async def test_before_model_callback_truncates_long_system_instruction(
plugin, callback_context, capsys
):
Expand All @@ -78,6 +79,7 @@ async def test_before_model_callback_truncates_long_system_instruction(
assert 'Z' not in out


@pytest.mark.asyncio
async def test_before_model_callback_keeps_system_instruction_at_budget(
plugin, callback_context, capsys
):
Expand All @@ -94,6 +96,7 @@ async def test_before_model_callback_keeps_system_instruction_at_budget(
assert f"System Instruction: '{'a' * 200}'" in out


@pytest.mark.asyncio
async def test_before_model_callback_lists_available_tool_names(
plugin, callback_context, capsys
):
Expand All @@ -109,6 +112,7 @@ async def test_before_model_callback_lists_available_tool_names(
assert 'Model: test-model' in out


@pytest.mark.asyncio
async def test_after_model_callback_logs_error_instead_of_content(
plugin, callback_context, capsys
):
Expand All @@ -132,6 +136,7 @@ async def test_after_model_callback_logs_error_instead_of_content(
assert 'Content:' not in out


@pytest.mark.asyncio
async def test_after_model_callback_logs_content_and_token_usage(
plugin, callback_context, capsys
):
Expand All @@ -151,6 +156,7 @@ async def test_after_model_callback_logs_content_and_token_usage(
assert 'Token Usage - Input: 11, Output: 7' in out


@pytest.mark.asyncio
async def test_on_event_callback_summarizes_function_parts(plugin, capsys):
event = Event(
author='test-agent',
Expand All @@ -173,6 +179,7 @@ async def test_on_event_callback_summarizes_function_parts(plugin, capsys):
assert "Function Responses: ['do_thing']" in out


@pytest.mark.asyncio
async def test_on_event_callback_renders_absent_content_as_none(plugin, capsys):
event = Event(author='test-agent', content=None)

Expand All @@ -182,6 +189,7 @@ async def test_on_event_callback_renders_absent_content_as_none(plugin, capsys):
assert 'Content: None' in out


@pytest.mark.asyncio
async def test_on_event_callback_truncates_long_text_part(plugin, capsys):
event = Event(
author='test-agent',
Expand All @@ -195,6 +203,7 @@ async def test_on_event_callback_truncates_long_text_part(plugin, capsys):
assert 'Z' not in out


@pytest.mark.asyncio
async def test_before_tool_callback_truncates_long_arguments(
plugin, tool_context, capsys
):
Expand Down
4 changes: 4 additions & 0 deletions tests/unittests/plugins/test_reflect_retry_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class _OtherScope(enum.Enum):
)


@pytest.mark.asyncio
async def test_tracker_increment_returns_running_count_per_item():
tracker = _reflect_retry_utils.ScopedFailureTracker()

Expand All @@ -72,6 +73,7 @@ async def test_tracker_increment_returns_running_count_per_item():
assert other_tool == 1


@pytest.mark.asyncio
async def test_tracker_keeps_scopes_independent():
tracker = _reflect_retry_utils.ScopedFailureTracker()

Expand All @@ -82,6 +84,7 @@ async def test_tracker_keeps_scopes_independent():
assert await tracker.increment('scope_a', 'tool') == 3


@pytest.mark.asyncio
async def test_tracker_reset_clears_only_the_named_item():
tracker = _reflect_retry_utils.ScopedFailureTracker()
await tracker.increment('scope', 'tool_a')
Expand All @@ -94,6 +97,7 @@ async def test_tracker_reset_clears_only_the_named_item():
assert await tracker.increment('scope', 'tool_b') == 3


@pytest.mark.asyncio
async def test_tracker_reset_of_unseen_scope_is_a_noop():
tracker = _reflect_retry_utils.ScopedFailureTracker()

Expand Down
10 changes: 10 additions & 0 deletions tests/unittests/skills/test__utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ def test__load_skills_from_dir_errors(tmp_path):
]


@pytest.mark.asyncio
@pytest.mark.parametrize("async_name, sync_name, args", _ASYNC_WRAPPERS)
async def test_async_wrapper_runs_blocking_call_off_event_loop(
monkeypatch, async_name, sync_name, args
Expand Down Expand Up @@ -643,6 +644,7 @@ def _record_thread(*call_args, **call_kwargs):
assert result == "sentinel-result"


@pytest.mark.asyncio
async def test_async_wrapper_keeps_event_loop_responsive(monkeypatch):
"""The event loop must keep scheduling tasks while a wrapper is in flight.

Expand Down Expand Up @@ -679,6 +681,7 @@ async def _release_once_entered():
assert results[0] == "loaded"


@pytest.mark.asyncio
async def test_async_wrappers_run_concurrently(monkeypatch):
"""Independent loads must overlap rather than serialize on the event loop."""
barrier = threading.Barrier(3, timeout=_BLOCKED_LOOP_TIMEOUT_SEC)
Expand All @@ -699,6 +702,7 @@ def _rendezvous(skill_dir):
assert results == ["s0", "s1", "s2"]


@pytest.mark.asyncio
async def test_load_skill_from_dir_async(tmp_path):
"""Tests loading a skill from a directory asynchronously."""
skill_dir = tmp_path / "test-skill"
Expand All @@ -725,12 +729,14 @@ async def test_load_skill_from_dir_async(tmp_path):
assert skill.resources.get_reference("ref1.md") == "ref1 content"


@pytest.mark.asyncio
async def test_load_skill_from_dir_async_propagates_errors(tmp_path):
"""Errors raised in the worker thread must surface to the caller."""
with pytest.raises(FileNotFoundError):
await _load_skill_from_dir_async(tmp_path / "nonexistent")


@pytest.mark.asyncio
async def test_load_skills_from_dir_async(tmp_path):
"""Tests loading every skill in a directory asynchronously."""
skills_dir = tmp_path / "skills"
Expand All @@ -751,12 +757,14 @@ async def test_load_skills_from_dir_async(tmp_path):
assert skills[0].instructions == "body skill-a"


@pytest.mark.asyncio
async def test_load_skills_from_dir_async_propagates_errors(tmp_path):
"""Errors raised in the worker thread must surface to the caller."""
with pytest.raises(FileNotFoundError, match="does not exist"):
await _load_skills_from_dir_async(tmp_path / "nonexistent")


@pytest.mark.asyncio
async def test_list_skills_in_dir_async(tmp_path):
"""Tests listing skills in a directory asynchronously."""
skills_dir = tmp_path / "skills"
Expand All @@ -776,6 +784,7 @@ async def test_list_skills_in_dir_async(tmp_path):
assert skills["skill1"].name == "skill1"


@pytest.mark.asyncio
@mock.patch("google.cloud.storage.Client")
async def test_load_skill_from_gcs_dir_async(mock_client_class):
"""Tests loading a skill from GCS asynchronously."""
Expand Down Expand Up @@ -820,6 +829,7 @@ def list_blobs_side_effect(prefix=None):
mock_bucket.blob.assert_any_call("skills/my-skill/SKILL.md")


@pytest.mark.asyncio
@mock.patch("google.cloud.storage.Client")
async def test_list_skills_in_gcs_dir_async(mock_client_class):
"""Tests listing skills in GCS asynchronously."""
Expand Down
Loading