From f7e4c3a135f9cfad5e308817dde592dabdd4194a Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Tue, 28 Apr 2026 07:59:20 +0000 Subject: [PATCH 1/6] tests: extend install smoke test with a runtime call This a follow-up for #1019 to prevent similar regressions. Existing install smoke tests are updated to actually run some code with the dependencies listed in the profile. This is different from the unit tests which are executed with dev deps and all extras. In addition this PR restructures this test harness and moves it under `tests/` folder. Although those files do not use pytest and are not executed by pytest their current location under `scripts/` makes less sense as other files in this folder are helpers for local runs. A proper readme is added to clarify how it should be used. --- .github/workflows/install-smoke.yml | 6 +- scripts/test_install_smoke.sh | 14 ++- tests/install_smoke/README.md | 39 +++++++ tests/install_smoke/__init__.py | 0 .../install_smoke/__main__.py | 104 +++++++++++------- tests/install_smoke/runtime/__init__.py | 0 .../runtime/base_send_message.py | 88 +++++++++++++++ 7 files changed, 201 insertions(+), 50 deletions(-) create mode 100644 tests/install_smoke/README.md create mode 100644 tests/install_smoke/__init__.py rename scripts/test_install_smoke.py => tests/install_smoke/__main__.py (57%) mode change 100755 => 100644 create mode 100644 tests/install_smoke/runtime/__init__.py create mode 100644 tests/install_smoke/runtime/base_send_message.py diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index ace3ff072..faa19f701 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -8,7 +8,7 @@ on: - 'src/**' - 'pyproject.toml' - 'uv.lock' - - 'scripts/test_install_smoke.py' + - 'tests/install_smoke/**' - 'scripts/test_install_smoke.sh' # Self-callout: re-run when this workflow changes so YAML edits are validated in PRs. - '.github/workflows/install-smoke.yml' @@ -58,5 +58,5 @@ jobs: - name: List installed packages run: VIRTUAL_ENV=.venv-smoke uv pip list - - name: Run import smoke test - run: .venv-smoke/bin/python scripts/test_install_smoke.py ${{ matrix.profile.name }} + - name: Run smoke test (imports + runtime checks) + run: .venv-smoke/bin/python -m tests.install_smoke ${{ matrix.profile.name }} diff --git a/scripts/test_install_smoke.sh b/scripts/test_install_smoke.sh index 9f0a45fbd..2bd3edced 100755 --- a/scripts/test_install_smoke.sh +++ b/scripts/test_install_smoke.sh @@ -2,11 +2,11 @@ # Local equivalent of .github/workflows/install-smoke.yml. # # For each install profile, builds the wheel and installs it into a -# clean venv (no dev deps), then runs the import smoke test for that -# profile. By default runs every known profile; pass a profile name -# to run just one. +# clean venv (no dev deps), then runs the smoke test for that profile +# (imports + any per-profile runtime checks). By default runs every +# known profile; pass a profile name to run just one. # -# Available profiles (must match those in scripts/test_install_smoke.py): +# Available profiles (must match those in tests/install_smoke/profiles.py): # base -- `pip install a2a-sdk` # http-server -- `pip install a2a-sdk[http-server]` # grpc -- `pip install a2a-sdk[grpc]` @@ -87,8 +87,10 @@ for profile in "${PROFILES[@]}"; do echo "--- Installed packages ---" VIRTUAL_ENV="$venv_dir" uv pip list - echo "--- Running import smoke test ---" - if ! "$venv_dir/bin/python" scripts/test_install_smoke.py "$profile"; then + echo "--- Running smoke test (imports + runtime checks) ---" + # Invoked as `python -m` from REPO_ROOT so the harness package is + # importable from the smoke venv (which has no pytest / dev deps). + if ! "$venv_dir/bin/python" -m tests.install_smoke "$profile"; then FAILED_PROFILES+=("$profile") fi done diff --git a/tests/install_smoke/README.md b/tests/install_smoke/README.md new file mode 100644 index 000000000..91af5160a --- /dev/null +++ b/tests/install_smoke/README.md @@ -0,0 +1,39 @@ +# Install-smoke harness + +This package is **not** a pytest test suite. It is invoked as a +standalone module from a freshly-installed, dev-deps-free venv: + +```bash +python -m tests.install_smoke +``` + +The smoke venv is created by either +[`scripts/test_install_smoke.sh`](../../scripts/test_install_smoke.sh) +(local) or +[`.github/workflows/install-smoke.yml`](../../.github/workflows/install-smoke.yml) +(CI). The harness has no pytest dependency and uses only the Python +standard library plus the freshly-installed `a2a-sdk` wheel for the +profile under test. + +For a given install profile (`base`, `http-server`, `grpc`, +`telemetry`, `sql`) it runs two phases: + +1. **Imports** — every module listed for the profile in `__main__.py` + must import cleanly. Catches missing deps and accidental top-level + imports of optional extras. +2. **Runtime checks** (`runtime/`) — small public-API exercises that + actually call into the SDK. These catch regressions where imports + succeed but a real call fails. + +## Adding a new runtime check + +1. Drop a module under `tests/install_smoke/runtime/` exposing two + names: + - `NAME: str` — short human-readable label. + - `check() -> None` — callable that raises on failure. +2. Register it in `RUNTIME_CHECKS` in + [`__main__.py`](./__main__.py) under each profile whose extras it + needs. + +Use only the dependencies guaranteed by the target profile. Do not import +`pytest` or any dev-deps. diff --git a/tests/install_smoke/__init__.py b/tests/install_smoke/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/test_install_smoke.py b/tests/install_smoke/__main__.py old mode 100755 new mode 100644 similarity index 57% rename from scripts/test_install_smoke.py rename to tests/install_smoke/__main__.py index 41ad029bb..dfd688bb1 --- a/scripts/test_install_smoke.py +++ b/tests/install_smoke/__main__.py @@ -1,27 +1,10 @@ -#!/usr/bin/env python3 -"""Smoke test for installations of a2a-sdk with various extras. - -This script verifies that the public API modules associated with a -given installation profile can be imported without pulling in modules -that belong to other (uninstalled) optional extras. - -It is designed to run WITHOUT pytest or any dev dependencies -- just -a clean venv with `pip install a2a-sdk[]`. - -Usage: - python scripts/test_install_smoke.py [profile] - - profile defaults to "base" and selects which set of modules to - smoke-test. Available profiles: - base -- `pip install a2a-sdk` - http-server -- `pip install a2a-sdk[http-server]` - grpc -- `pip install a2a-sdk[grpc]` - telemetry -- `pip install a2a-sdk[telemetry]` - sql -- `pip install a2a-sdk[sql]` +"""Entry point: ``python -m tests.install_smoke ``. Exit codes: - 0 - All imports for the profile succeeded - 1 - One or more imports failed + 0 - All imports and runtime checks for the profile succeeded. + 1 - One or more imports or runtime checks failed. + +See README.md for design notes and how to add new runtime checks. """ from __future__ import annotations @@ -29,15 +12,23 @@ import importlib import sys +from typing import TYPE_CHECKING + +from tests.install_smoke.runtime import base_send_message + + +if TYPE_CHECKING: + from collections.abc import Callable + # Core modules that MUST be importable with only base dependencies. # These are the public API surface that every user gets with # `pip install a2a-sdk` (no extras). # # Do NOT add modules here that require optional extras (grpc, -# http-server, sql, signing, telemetry, vertex, etc.). -# Those modules are expected to fail without their extras installed -# and should use try/except ImportError guards internally. +# http-server, sql, signing, telemetry, vertex, etc.). Those modules +# are expected to fail without their extras installed and should use +# try/except ImportError guards internally. CORE_MODULES = [ 'a2a', 'a2a.client', @@ -69,10 +60,6 @@ # Modules that MUST be importable with only the base + `http-server` # extras installed (no `grpc`, `sql`, `signing`, `telemetry`, etc.). -# -# A user building a Starlette/FastAPI A2A server with -# `pip install a2a-sdk[http-server]` should be able to import these -# without the gRPC stack being present on the system. HTTP_SERVER_MODULES = [ 'a2a.server.routes', 'a2a.server.routes.agent_card_routes', @@ -116,37 +103,72 @@ } -def main() -> int: - profile = sys.argv[1] if len(sys.argv) > 1 else 'base' +# Per-profile runtime exercises. Each callable raises on failure and +# returns None on success. These run after the import smoke succeeds +# and are meant to invoke real public-API code paths against the +# dependency versions resolved at install time. +# +# To add a new check: drop a module under `tests.install_smoke.runtime` +# exposing `NAME` and `check()`, then add a tuple here for the +# profile(s) whose extras it needs. See README.md. +RUNTIME_CHECKS: dict[str, list[tuple[str, Callable[[], None]]]] = { + 'base': [ + (base_send_message.NAME, base_send_message.check), + ], +} + + +def main(argv: list[str]) -> int: + profile = argv[1] if len(argv) > 1 else 'base' if profile not in PROFILES: print(f'Unknown profile {profile!r}. Available: {sorted(PROFILES)}') return 1 modules = PROFILES[profile] - failures: list[str] = [] - successes: list[str] = [] - + import_failures: list[str] = [] for module_name in modules: try: importlib.import_module(module_name) - successes.append(module_name) except Exception as e: # noqa: BLE001, PERF203 - failures.append(f'{module_name}: {e}') + import_failures.append(f'{module_name}: {e}') print(f'Profile: {profile}') print(f'Tested {len(modules)} modules') - print(f' Passed: {len(successes)}') - print(f' Failed: {len(failures)}') + print(f' Passed: {len(modules) - len(import_failures)}') + print(f' Failed: {len(import_failures)}') - if failures: + if import_failures: print('\nFAILED imports:') - for failure in failures: + for failure in import_failures: print(f' - {failure}') return 1 print('\nAll modules imported successfully.') + + runtime_checks = RUNTIME_CHECKS.get(profile, []) + if not runtime_checks: + return 0 + + print(f'\nRunning {len(runtime_checks)} runtime check(s):') + runtime_failures: list[str] = [] + for name, check in runtime_checks: + try: + check() + except Exception as e: # noqa: BLE001, PERF203 + runtime_failures.append(f'{name}: {type(e).__name__}: {e}') + print(f' - FAIL: {name}') + else: + print(f' - OK: {name}') + + if runtime_failures: + print('\nFAILED runtime checks:') + for failure in runtime_failures: + print(f' - {failure}') + return 1 + + print('\nAll runtime checks passed.') return 0 if __name__ == '__main__': - sys.exit(main()) + sys.exit(main(sys.argv)) diff --git a/tests/install_smoke/runtime/__init__.py b/tests/install_smoke/runtime/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/install_smoke/runtime/base_send_message.py b/tests/install_smoke/runtime/base_send_message.py new file mode 100644 index 000000000..bc6a1db91 --- /dev/null +++ b/tests/install_smoke/runtime/base_send_message.py @@ -0,0 +1,88 @@ +"""Drive DefaultRequestHandler.on_message_send end-to-end with no transport. + +Uses only base dependencies (no http-server / gRPC transport). +""" + +from __future__ import annotations + +import asyncio + +from a2a.helpers.proto_helpers import new_task_from_user_message +from a2a.server.agent_execution import AgentExecutor +from a2a.server.context import ServerCallContext +from a2a.server.request_handlers import DefaultRequestHandler +from a2a.server.tasks import InMemoryTaskStore +from a2a.server.tasks.task_updater import TaskUpdater +from a2a.types.a2a_pb2 import ( + AgentCapabilities, + AgentCard, + Message, + Part, + Role, + SendMessageConfiguration, + SendMessageRequest, + Task, + TaskState, +) + +NAME = 'DefaultRequestHandler.on_message_send roundtrip' + + +class _HelloAgentExecutor(AgentExecutor): + async def execute(self, context, event_queue) -> None: # type: ignore[no-untyped-def] + task = context.current_task + if not task: + assert context.message is not None + task = new_task_from_user_message(context.message) + await event_queue.enqueue_event(task) + updater = TaskUpdater(event_queue, task.id, task.context_id) + await updater.update_status( + TaskState.TASK_STATE_WORKING, + message=updater.new_agent_message([Part(text='I am working')]), + ) + await updater.add_artifact( + [Part(text='Hello world!')], name='conversion_result' + ) + await updater.complete() + + async def cancel(self, context, event_queue) -> None: # type: ignore[no-untyped-def] + pass + + +async def _run() -> None: + handler = DefaultRequestHandler( + agent_executor=_HelloAgentExecutor(), + task_store=InMemoryTaskStore(), + agent_card=AgentCard( + name='smoke', + version='1.0', + capabilities=AgentCapabilities( + streaming=True, push_notifications=False + ), + ), + ) + params = SendMessageRequest( + message=Message( + role=Role.ROLE_USER, + message_id='m1', + parts=[Part(text='hi')], + ), + configuration=SendMessageConfiguration( + accepted_output_modes=['text/plain'] + ), + ) + result = await handler.on_message_send(params, ServerCallContext()) + if not isinstance(result, Task): + raise AssertionError( # noqa: TRY004 + f'expected Task result, got {type(result).__name__}' + ) + if result.status.state != TaskState.TASK_STATE_COMPLETED: + raise AssertionError( + f'expected TASK_STATE_COMPLETED, got ' + f'{TaskState.Name(result.status.state)}' + ) + + +def check() -> None: + """Run the roundtrip; raises on failure.""" + asyncio.run(_run()) From aa7d08172e7b5b3215464c433f20d1fe8f94c3f4 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Tue, 28 Apr 2026 08:01:18 +0000 Subject: [PATCH 2/6] Update --- tests/install_smoke/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/install_smoke/README.md b/tests/install_smoke/README.md index 91af5160a..c0ff85960 100644 --- a/tests/install_smoke/README.md +++ b/tests/install_smoke/README.md @@ -18,10 +18,10 @@ profile under test. For a given install profile (`base`, `http-server`, `grpc`, `telemetry`, `sql`) it runs two phases: -1. **Imports** — every module listed for the profile in `__main__.py` +1. **Imports**: every module listed for the profile in `__main__.py` must import cleanly. Catches missing deps and accidental top-level imports of optional extras. -2. **Runtime checks** (`runtime/`) — small public-API exercises that +2. **Runtime checks**: small public-API exercises that actually call into the SDK. These catch regressions where imports succeed but a real call fails. From 6aa30d04b04e908278d77c8f11f26e2d0af6ca94 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Tue, 28 Apr 2026 08:01:39 +0000 Subject: [PATCH 3/6] Update --- tests/install_smoke/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/install_smoke/README.md b/tests/install_smoke/README.md index c0ff85960..6cc14a012 100644 --- a/tests/install_smoke/README.md +++ b/tests/install_smoke/README.md @@ -25,7 +25,7 @@ For a given install profile (`base`, `http-server`, `grpc`, actually call into the SDK. These catch regressions where imports succeed but a real call fails. -## Adding a new runtime check +#### Adding a new runtime check 1. Drop a module under `tests/install_smoke/runtime/` exposing two names: From dd566a8140e6da2c1031740af2cc7e0b30449c8c Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Tue, 28 Apr 2026 08:04:03 +0000 Subject: [PATCH 4/6] Update --- scripts/test_install_smoke.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/test_install_smoke.sh b/scripts/test_install_smoke.sh index 2bd3edced..c647fed33 100755 --- a/scripts/test_install_smoke.sh +++ b/scripts/test_install_smoke.sh @@ -88,8 +88,6 @@ for profile in "${PROFILES[@]}"; do VIRTUAL_ENV="$venv_dir" uv pip list echo "--- Running smoke test (imports + runtime checks) ---" - # Invoked as `python -m` from REPO_ROOT so the harness package is - # importable from the smoke venv (which has no pytest / dev deps). if ! "$venv_dir/bin/python" -m tests.install_smoke "$profile"; then FAILED_PROFILES+=("$profile") fi From a71397d2ff06fa579bf47be2d63094f9f1f6135d Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Tue, 28 Apr 2026 08:15:00 +0000 Subject: [PATCH 5/6] Update --- scripts/test_install_smoke.sh | 2 +- tests/install_smoke/__main__.py | 37 +++---------------- .../runtime/base_send_message.py | 6 ++- 3 files changed, 10 insertions(+), 35 deletions(-) diff --git a/scripts/test_install_smoke.sh b/scripts/test_install_smoke.sh index c647fed33..98df79211 100755 --- a/scripts/test_install_smoke.sh +++ b/scripts/test_install_smoke.sh @@ -6,7 +6,7 @@ # (imports + any per-profile runtime checks). By default runs every # known profile; pass a profile name to run just one. # -# Available profiles (must match those in tests/install_smoke/profiles.py): +# Available profiles (must match those in tests/install_smoke/__main__.py): # base -- `pip install a2a-sdk` # http-server -- `pip install a2a-sdk[http-server]` # grpc -- `pip install a2a-sdk[grpc]` diff --git a/tests/install_smoke/__main__.py b/tests/install_smoke/__main__.py index dfd688bb1..12a5e677a 100644 --- a/tests/install_smoke/__main__.py +++ b/tests/install_smoke/__main__.py @@ -1,11 +1,4 @@ -"""Entry point: ``python -m tests.install_smoke ``. - -Exit codes: - 0 - All imports and runtime checks for the profile succeeded. - 1 - One or more imports or runtime checks failed. - -See README.md for design notes and how to add new runtime checks. -""" +"""Entry point for the install-smoke harness. See README.md.""" from __future__ import annotations @@ -21,14 +14,10 @@ from collections.abc import Callable -# Core modules that MUST be importable with only base dependencies. -# These are the public API surface that every user gets with -# `pip install a2a-sdk` (no extras). -# -# Do NOT add modules here that require optional extras (grpc, -# http-server, sql, signing, telemetry, vertex, etc.). Those modules -# are expected to fail without their extras installed and should use -# try/except ImportError guards internally. +# Modules under each list MUST be importable with only that profile's +# extras installed -- no leakage from other extras (grpc, http-server, +# sql, signing, telemetry, vertex, etc.). Modules that require +# optional extras must use try/except ImportError guards internally. CORE_MODULES = [ 'a2a', 'a2a.client', @@ -58,8 +47,6 @@ 'a2a.helpers.proto_helpers', ] -# Modules that MUST be importable with only the base + `http-server` -# extras installed (no `grpc`, `sql`, `signing`, `telemetry`, etc.). HTTP_SERVER_MODULES = [ 'a2a.server.routes', 'a2a.server.routes.agent_card_routes', @@ -70,8 +57,6 @@ 'a2a.server.routes.rest_routes', ] -# Modules that MUST be importable with only the base + `grpc` extras -# installed (no `http-server`, `sql`, `signing`, `telemetry`, etc.). GRPC_MODULES = [ 'a2a.server.request_handlers.grpc_handler', 'a2a.client.transports.grpc', @@ -79,14 +64,10 @@ 'a2a.compat.v0_3.grpc_transport', ] -# Modules that MUST be importable with only the base + `telemetry` -# extras installed. TELEMETRY_MODULES = [ 'a2a.utils.telemetry', ] -# Modules that MUST be importable with only the base + `sql` extras -# installed (covers postgresql/mysql/sqlite drivers via SQLAlchemy). SQL_MODULES = [ 'a2a.server.models', 'a2a.server.tasks.database_task_store', @@ -103,14 +84,6 @@ } -# Per-profile runtime exercises. Each callable raises on failure and -# returns None on success. These run after the import smoke succeeds -# and are meant to invoke real public-API code paths against the -# dependency versions resolved at install time. -# -# To add a new check: drop a module under `tests.install_smoke.runtime` -# exposing `NAME` and `check()`, then add a tuple here for the -# profile(s) whose extras it needs. See README.md. RUNTIME_CHECKS: dict[str, list[tuple[str, Callable[[], None]]]] = { 'base': [ (base_send_message.NAME, base_send_message.check), diff --git a/tests/install_smoke/runtime/base_send_message.py b/tests/install_smoke/runtime/base_send_message.py index bc6a1db91..9d15460e0 100644 --- a/tests/install_smoke/runtime/base_send_message.py +++ b/tests/install_smoke/runtime/base_send_message.py @@ -1,6 +1,9 @@ """Drive DefaultRequestHandler.on_message_send end-to-end with no transport. -Uses only base dependencies (no http-server / gRPC transport). +Exercises the request-validation path that regressed in +https://github.com/a2aproject/a2a-python/pull/1019: a real proto +instance flows through `validate_proto_required_fields`, which broke +on protobuf 7. Pure imports cannot catch this class of regression. """ from __future__ import annotations @@ -84,5 +87,4 @@ async def _run() -> None: def check() -> None: - """Run the roundtrip; raises on failure.""" asyncio.run(_run()) From 092907c92a05a0ca77345cdedb75b8119b3ea1ba Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Tue, 28 Apr 2026 08:18:56 +0000 Subject: [PATCH 6/6] Update --- tests/install_smoke/__main__.py | 29 ++++++++----------- .../runtime/base_send_message.py | 10 +++++-- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/tests/install_smoke/__main__.py b/tests/install_smoke/__main__.py index 12a5e677a..76354028d 100644 --- a/tests/install_smoke/__main__.py +++ b/tests/install_smoke/__main__.py @@ -5,14 +5,6 @@ import importlib import sys -from typing import TYPE_CHECKING - -from tests.install_smoke.runtime import base_send_message - - -if TYPE_CHECKING: - from collections.abc import Callable - # Modules under each list MUST be importable with only that profile's # extras installed -- no leakage from other extras (grpc, http-server, @@ -84,10 +76,10 @@ } -RUNTIME_CHECKS: dict[str, list[tuple[str, Callable[[], None]]]] = { - 'base': [ - (base_send_message.NAME, base_send_message.check), - ], +# Imported lazily in `main()` so a check that needs one profile's +# extras can't break the harness when running a different profile. +RUNTIME_CHECKS: dict[str, list[str]] = { + 'base': ['tests.install_smoke.runtime.base_send_message'], } @@ -124,14 +116,17 @@ def main(argv: list[str]) -> int: print(f'\nRunning {len(runtime_checks)} runtime check(s):') runtime_failures: list[str] = [] - for name, check in runtime_checks: + for module_path in runtime_checks: + label = module_path try: - check() + module = importlib.import_module(module_path) + label = module.NAME + module.check() except Exception as e: # noqa: BLE001, PERF203 - runtime_failures.append(f'{name}: {type(e).__name__}: {e}') - print(f' - FAIL: {name}') + runtime_failures.append(f'{label}: {type(e).__name__}: {e}') + print(f' - FAIL: {label}') else: - print(f' - OK: {name}') + print(f' - OK: {label}') if runtime_failures: print('\nFAILED runtime checks:') diff --git a/tests/install_smoke/runtime/base_send_message.py b/tests/install_smoke/runtime/base_send_message.py index 9d15460e0..837ec48d4 100644 --- a/tests/install_smoke/runtime/base_send_message.py +++ b/tests/install_smoke/runtime/base_send_message.py @@ -12,7 +12,9 @@ from a2a.helpers.proto_helpers import new_task_from_user_message from a2a.server.agent_execution import AgentExecutor +from a2a.server.agent_execution.context import RequestContext from a2a.server.context import ServerCallContext +from a2a.server.events.event_queue import EventQueue from a2a.server.request_handlers import DefaultRequestHandler from a2a.server.tasks import InMemoryTaskStore from a2a.server.tasks.task_updater import TaskUpdater @@ -32,7 +34,9 @@ class _HelloAgentExecutor(AgentExecutor): - async def execute(self, context, event_queue) -> None: # type: ignore[no-untyped-def] + async def execute( + self, context: RequestContext, event_queue: EventQueue + ) -> None: task = context.current_task if not task: assert context.message is not None @@ -48,7 +52,9 @@ async def execute(self, context, event_queue) -> None: # type: ignore[no-untype ) await updater.complete() - async def cancel(self, context, event_queue) -> None: # type: ignore[no-untyped-def] + async def cancel( + self, context: RequestContext, event_queue: EventQueue + ) -> None: pass