diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a7d7c671..b98494093 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,8 @@ jobs: # Build and test the project build-lint-test: strategy: - fail-fast: true + # Let every matrix job finish so one flaky job does not hide the others. + fail-fast: false matrix: python: ["3.10", "3.14"] os: [ubuntu-latest, macos-intel, macos-arm, windows-latest] diff --git a/bedrock_agentcore/strands_agent/pyproject.toml b/bedrock_agentcore/strands_agent/pyproject.toml index 9cb221cea..4d06a4f58 100644 --- a/bedrock_agentcore/strands_agent/pyproject.toml +++ b/bedrock_agentcore/strands_agent/pyproject.toml @@ -21,6 +21,7 @@ dev = [ "poethepoet>=0.36.0", "pytest>=7.1.2,<8", "pytest-asyncio>=0.18.3,<0.19", + "pytest-timeout>=2.2,<3", ] [build-system] @@ -47,6 +48,8 @@ lint-types = "uv run --all-groups mypy --check-untyped-defs --namespace-packages test = "uv run pytest" [tool.pytest.ini_options] +# Fail a test that hangs, with a stack dump, instead of stalling the job. +timeout = 300 asyncio_mode = "auto" [tool.ruff] diff --git a/bedrock_agentcore/strands_agent/uv.lock b/bedrock_agentcore/strands_agent/uv.lock index fe9374747..7149383fd 100644 --- a/bedrock_agentcore/strands_agent/uv.lock +++ b/bedrock_agentcore/strands_agent/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.15'", @@ -1885,6 +1885,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/4b/7c400506ec484ec999b10133aa8e31af39dfc727042dc6944cd45fd927d0/pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84", size = 14597, upload-time = "2022-03-25T09:43:57.106Z" }, ] +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2509,6 +2521,7 @@ dev = [ { name = "poethepoet" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-timeout" }, { name = "ruff" }, ] @@ -2527,6 +2540,7 @@ dev = [ { name = "poethepoet", specifier = ">=0.36.0" }, { name = "pytest", specifier = ">=7.1.2,<8" }, { name = "pytest-asyncio", specifier = ">=0.18.3,<0.19" }, + { name = "pytest-timeout", specifier = ">=2.2,<3" }, { name = "ruff", specifier = ">=0.5.0,<0.6" }, ] diff --git a/lambda_worker/pyproject.toml b/lambda_worker/pyproject.toml index bf473842a..ab5105560 100644 --- a/lambda_worker/pyproject.toml +++ b/lambda_worker/pyproject.toml @@ -15,6 +15,7 @@ dev = [ "poethepoet>=0.36.0", "pytest>=7.1.2,<8", "pytest-asyncio>=0.18.3,<0.19", + "pytest-timeout>=2.2,<3", ] [build-system] @@ -42,6 +43,8 @@ test = "uv run pytest" [tool.pytest.ini_options] asyncio_mode = "auto" +# Fail a test that hangs, with a stack dump, instead of stalling the job. +timeout = 300 [tool.ruff] target-version = "py310" diff --git a/lambda_worker/uv.lock b/lambda_worker/uv.lock index 7452a2abf..7068d705e 100644 --- a/lambda_worker/uv.lock +++ b/lambda_worker/uv.lock @@ -475,6 +475,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/4b/7c400506ec484ec999b10133aa8e31af39dfc727042dc6944cd45fd927d0/pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84", size = 14597, upload-time = "2022-03-25T09:43:57.106Z" }, ] +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -630,6 +642,7 @@ dev = [ { name = "poethepoet" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-timeout" }, { name = "ruff" }, ] @@ -642,6 +655,7 @@ dev = [ { name = "poethepoet", specifier = ">=0.36.0" }, { name = "pytest", specifier = ">=7.1.2,<8" }, { name = "pytest-asyncio", specifier = ">=0.18.3,<0.19" }, + { name = "pytest-timeout", specifier = ">=2.2,<3" }, { name = "ruff", specifier = ">=0.5.0,<0.6" }, ] diff --git a/openai_agents/mcp_v2/pyproject.toml b/openai_agents/mcp_v2/pyproject.toml index 5f8a1fe4b..c496ffc7b 100644 --- a/openai_agents/mcp_v2/pyproject.toml +++ b/openai_agents/mcp_v2/pyproject.toml @@ -13,6 +13,7 @@ dev = [ "mypy>=1.4.1,<2", "poethepoet>=0.36.0", "pytest>=9.0.3,<10", + "pytest-timeout>=2.2,<3", ] [tool.poe.tasks] @@ -35,5 +36,7 @@ target-version = "py310" ignore_missing_imports = true [tool.pytest.ini_options] +# Fail a test that hangs, with a stack dump, instead of stalling the job. +timeout = 300 pythonpath = ["."] testpaths = ["tests"] diff --git a/openai_agents/mcp_v2/uv.lock b/openai_agents/mcp_v2/uv.lock index ba27f40da..3da9f78d5 100644 --- a/openai_agents/mcp_v2/uv.lock +++ b/openai_agents/mcp_v2/uv.lock @@ -1248,6 +1248,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1795,6 +1807,7 @@ dev = [ { name = "mypy" }, { name = "poethepoet" }, { name = "pytest" }, + { name = "pytest-timeout" }, { name = "ruff" }, ] @@ -1809,6 +1822,7 @@ dev = [ { name = "mypy", specifier = ">=1.4.1,<2" }, { name = "poethepoet", specifier = ">=0.36.0" }, { name = "pytest", specifier = ">=9.0.3,<10" }, + { name = "pytest-timeout", specifier = ">=2.2,<3" }, { name = "ruff", specifier = ">=0.5.0,<0.6" }, ] diff --git a/pyproject.toml b/pyproject.toml index 744e0eb3c..78449a507 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dev = [ "mypy>=1.4.1,<2", "pytest>=7.1.2,<8", "pytest-asyncio>=0.18.3,<0.19", + "pytest-timeout>=2.2,<3", "fakeredis>=2,<3", "frozenlist>=1.4.0,<2", "pyright>=1.1.394", @@ -135,6 +136,8 @@ test = "uv run --all-groups --no-group cloud-export-to-parquet pytest" [tool.pytest.ini_options] asyncio_mode = "auto" +# Fail a test that hangs, with a stack dump, instead of stalling the job. +timeout = 300 log_cli = true log_cli_level = "INFO" log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" diff --git a/tests/sentry/test_interceptor.py b/tests/sentry/test_interceptor.py index f9f931e8f..d44ea4722 100644 --- a/tests/sentry/test_interceptor.py +++ b/tests/sentry/test_interceptor.py @@ -1,5 +1,7 @@ +import json import sys import unittest.mock +import uuid from collections import abc import pytest @@ -34,14 +36,18 @@ def transport() -> FakeSentryTransport: @pytest.fixture(autouse=True) -def sentry_init(transport: FakeSentryTransport) -> None: - """Initialize Sentry for testing.""" - sentry_sdk.init( - transport=transport, - integrations=[ - AsyncioIntegration(), - ], - ) +def sentry_init(transport: FakeSentryTransport) -> abc.Iterator[None]: + """Give each test its own Sentry client and empty scopes, so nothing an + earlier test captured or left as a breadcrumb leaks into this one.""" + for scope in ( + sentry_sdk.get_global_scope(), + sentry_sdk.get_isolation_scope(), + sentry_sdk.get_current_scope(), + ): + scope.clear() + sentry_sdk.init(transport=transport, integrations=[AsyncioIntegration()]) + yield + sentry_sdk.get_client().close() @pytest.fixture @@ -49,7 +55,7 @@ async def worker(client: Client) -> abc.AsyncIterator[Worker]: """Fixture to provide a worker for testing.""" async with Worker( client, - task_queue="sentry-task-queue", + task_queue=f"sentry-{uuid.uuid4()}", workflows=[SentryExampleWorkflow], activities=[broken_activity, working_activity], interceptors=[SentryInterceptor()], @@ -71,7 +77,7 @@ async def test_sentry_interceptor_reports_no_errors_when_workflow_succeeds( await client.execute_workflow( SentryExampleWorkflow.run, SentryExampleWorkflowInput(option="working"), - id="sentry-workflow-id", + id=f"sentry-workflow-{uuid.uuid4()}", task_queue=worker.task_queue, ) except Exception: @@ -85,12 +91,13 @@ async def test_sentry_interceptor_captures_errors( client: Client, worker: Worker, transport: FakeSentryTransport ) -> None: """Test that errors are captured with correct Sentry metadata.""" + workflow_id = f"sentry-workflow-{uuid.uuid4()}" # WHEN try: await client.execute_workflow( SentryExampleWorkflow.run, SentryExampleWorkflowInput(option="broken"), - id="sentry-workflow-id", + id=workflow_id, task_queue=worker.task_queue, ) pytest.fail("Workflow should raise an exception") @@ -99,7 +106,28 @@ async def test_sentry_interceptor_captures_errors( # THEN # there should be two events: one for the failed activity and one for the failed workflow - assert len(transport.events) == 2, "Two events should be captured" + assert len(transport.events) == 2, ( + "Two events should be captured; got: " + + json.dumps( + [ + { + k: dict(e).get(k) + for k in ( + "level", + "logger", + "message", + "logentry", + "exception", + "transaction", + "tags", + "extra", + ) + } + for e in transport.events + ], + default=str, + ) + ) # Check the first event - should be the activity exception # -------------------------------------------------------- @@ -114,10 +142,10 @@ async def test_sentry_interceptor_captures_errors( "temporal.execution_type": "activity", "module": "sentry.activity.broken_activity", "temporal.workflow.type": "SentryExampleWorkflow", - "temporal.workflow.id": "sentry-workflow-id", + "temporal.workflow.id": workflow_id, "temporal.activity.id": "1", "temporal.activity.type": "broken_activity", - "temporal.activity.task_queue": "sentry-task-queue", + "temporal.activity.task_queue": worker.task_queue, "temporal.workflow.namespace": "default", "temporal.workflow.run_id": unittest.mock.ANY, } @@ -146,8 +174,8 @@ async def test_sentry_interceptor_captures_errors( "temporal.execution_type": "workflow", "module": "sentry.workflow.SentryExampleWorkflow.run", "temporal.workflow.type": "SentryExampleWorkflow", - "temporal.workflow.id": "sentry-workflow-id", - "temporal.workflow.task_queue": "sentry-task-queue", + "temporal.workflow.id": workflow_id, + "temporal.workflow.task_queue": worker.task_queue, "temporal.workflow.namespace": "default", "temporal.workflow.run_id": unittest.mock.ANY, } diff --git a/uv.lock b/uv.lock index 4a0752208..a670f2126 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.15'", @@ -3935,6 +3935,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/85/2f97a1b65178b0f11c9c77c35417a4cc5b99a80db90dad4734a129844ea5/pytest_pretty-1.3.0-py3-none-any.whl", hash = "sha256:074b9d5783cef9571494543de07e768a4dda92a3e85118d6c7458c67297159b7", size = 5620, upload-time = "2025-06-04T12:54:36.229Z" }, ] +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -4803,6 +4815,7 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-pretty" }, + { name = "pytest-timeout" }, { name = "ruff" }, { name = "types-pyyaml" }, ] @@ -4930,6 +4943,7 @@ dev = [ { name = "pytest", specifier = ">=7.1.2,<8" }, { name = "pytest-asyncio", specifier = ">=0.18.3,<0.19" }, { name = "pytest-pretty", specifier = ">=1.3.0" }, + { name = "pytest-timeout", specifier = ">=2.2,<3" }, { name = "ruff", specifier = ">=0.5.0,<0.6" }, { name = "types-pyyaml", specifier = ">=6.0.12.20241230,<7" }, ]