From 05acc26c40262e4c564c652cd917e00328c84897 Mon Sep 17 00:00:00 2001 From: HAL9000 Date: Fri, 8 May 2026 00:47:45 +0000 Subject: [PATCH 1/4] chore(deps): upgrade PyYAML to address known security vulnerability - Added `pyyaml>=6.0.3` dependency constraint to pyproject.toml after aiohttp to prevent installation of vulnerable older versions with known YAML parsing security issues. - Updated uv.lock package dependencies and requires-dist to include pyyaml entry with specifier '>=6.0.3'. - Added changelog Security section entry under [Unreleased] documenting the upgrade for issue #9055. - Updated CONTRIBUTORS.md with contribution note (PR #9244). - Added BDD test scenario verifying PyYAML security dependency constraint. ISSUES CLOSED: #9055 --- CHANGELOG.md | 5 + CONTRIBUTORS.md | 1 + features/security_pyyaml_dependency.feature | 23 +++ .../steps/security_pyyaml_dependency_steps.py | 132 ++++++++++++++++++ pyproject.toml | 1 + uv.lock | 2 + 6 files changed, 164 insertions(+) create mode 100644 features/security_pyyaml_dependency.feature create mode 100644 features/steps/security_pyyaml_dependency_steps.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b40ede8..3dda41d07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -144,6 +144,11 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Security +- **PyYAML upgraded to >=6.0.3 to address known security vulnerability** (#9055): + Added an explicit `pyyaml>=6.0.3` dependency constraint to `pyproject.toml` to + prevent installation of vulnerable older versions with known YAML parsing security + issues. + - **aiohttp upgraded to >=3.13.4 to remediate CVE-2026-34513 and CVE-2026-34515** (#1549, #1544): Added an explicit `aiohttp>=3.13.4` dependency constraint to `pyproject.toml` to remediate two high-severity open redirect vulnerabilities. Both CVEs affect the CleverAgents platform's diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b42a9f1a5..595cb3ac5 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -40,3 +40,4 @@ Below are some of the specific details of various contributions. * HAL 9000 has contributed the ACMS context path matching fix (PR #10975 / issue #10972): corrects `_path_matches()` and `_matches_pattern()` to properly match absolute fragment paths against relative glob patterns by auto-prefixing with `**/` before calling `PurePath.full_match()`, preventing silent inefficacy of include/exclude filters for absolute paths in fragment metadata. * HAL 9000 has contributed database resource types (PostgreSQL, SQLite) with transaction-based sandbox strategy: implemented ``DatabaseResourceHandler`` providing full CRUD operations (`read`, `write`, `delete`, `list_children`) and connection validation with automatic credential masking for PostgreSQL and SQLite backends. Includes ``TransactionSandbox`` infrastructure wired into ``SandboxFactory``, BDD test coverage in ``features/database_resources.feature``, and Robot Framework integration tests in ``robot/database_resources.robot`` (PR #10591 / issue #8608, Epic #8568). * HAL 9000 has contributed the agents plan rollback command (PR #8674 / issue #8557): implemented checkpoint-based plan state restoration with the `agents plan rollback []` CLI command as part of Epic #8493, enabling plans to be restored to previous checkpoints, discarding post-checkpoint decisions, and resuming execution from the rolled-back state. Supported by `--yes/-y`, `--to-checkpoint`, and `--format/-f` flags. Includes comprehensive BDD test coverage (>= 97%) for rollback, decision discarding, and plan resume functionality. +* HAL 9000 has contributed the PyYAML security upgrade (PR #11012 / issue #9055): added `pyyaml>=6.0.3` dependency constraint to address known YAML parsing vulnerabilities. diff --git a/features/security_pyyaml_dependency.feature b/features/security_pyyaml_dependency.feature new file mode 100644 index 000000000..985f286b3 --- /dev/null +++ b/features/security_pyyaml_dependency.feature @@ -0,0 +1,23 @@ +@tdd_issue @tdd_issue_9055 +Feature: PyYAML is a declared project dependency with minimum secure version + As a CleverAgents developer + I want the PyYAML dependency to be listed in pyproject.toml with a minimum secure version + So that vulnerable older versions of PyYAML cannot be installed + + Background: + Given the pyproject.toml file exists at "pyproject.toml" + + @tdd_issue @tdd_issue_9055 + Scenario: PyYAML is listed in project dependencies with minimum version constraint + When I read the project dependencies from pyproject.toml + Then the dependency list should include a package matching "pyyaml>=" + + @tdd_issue @tdd_issue_9055 + Scenario: PyYAML minimum version is >=6.0.3 + When I read the PyYAML dependency specification from pyproject.toml + Then the minimum version should be at least "6.0.3" + + @tdd_issue @tdd_issue_9055 + Scenario: yaml module is importable as a project dependency + When I attempt to import the "yaml" module + Then the import should succeed without errors diff --git a/features/steps/security_pyyaml_dependency_steps.py b/features/steps/security_pyyaml_dependency_steps.py new file mode 100644 index 000000000..ef9e98579 --- /dev/null +++ b/features/steps/security_pyyaml_dependency_steps.py @@ -0,0 +1,132 @@ +"""Step definitions for PyYAML security dependency verification (#9055).""" + +from __future__ import annotations + +import importlib +import re +from pathlib import Path +from typing import Any + +from behave import given, then, when +from behave.runner import Context + + +@given('the pyproject.toml file exists at "{path}"') +def step_pyproject_exists(context: Context, path: str) -> None: + """Verify pyproject.toml exists at the given path.""" + pyproject_path = Path(path) + assert pyproject_path.exists(), f"pyproject.toml not found at {path}" + context.pyproject_path = pyproject_path + + +@when("I read the project dependencies from pyproject.toml") +def step_read_project_dependencies(context: Context) -> None: + """Read the [project.dependencies] list from pyproject.toml.""" + content = context.pyproject_path.read_bytes() + data: dict[str, Any] = _load_toml(content) + context.project_dependencies = data.get("project", {}).get("dependencies", []) + + +@then('the dependency list should include a package matching "{pattern}"') +def step_dependency_matches_pattern(context: Context, pattern: str) -> None: + """Assert that a dependency in the project dependencies matches the given regex pattern.""" + deps: list[str] = context.project_dependencies + found = any(re.search(pattern, dep) for dep in deps) + assert found, ( + f"No dependency matching '{pattern}' found in " + f"[project.dependencies]. Current deps: {deps}" + ) + + +@when("I read the PyYAML dependency specification from pyproject.toml") +def step_read_pyyaml_dependency(context: Context) -> None: + """Read and parse the PyYAML dependency from pyproject.toml.""" + content = context.pyproject_path.read_bytes() + data: dict[str, Any] = _load_toml(content) + deps: list[str] = data.get("project", {}).get("dependencies", []) + + # Find the pyyaml dependency entry + pyyaml_dep = None + for dep in deps: + if dep.strip().startswith("pyyaml"): + pyyaml_dep = dep.strip() + break + + assert pyyaml_dep is not None, ( + f"PyYAML dependency not found in [project.dependencies]. Current deps: {deps}" + ) + + # Parse the minimum version from the constraint + # Examples: "pyyaml>=6.0.3", "pyyaml>=6.0", "pyyaml==6.0.2" + match = re.search(r"(?:>=|==|~>)([\d]+(?:\.[\d]+)*(?:\.\w+)*)", pyyaml_dep) + assert match is not None, f"Could not parse version from PyYAML dependency: {pyyaml_dep}" + + context.pyyaml_version_spec = pyyaml_dep + context.pyyaml_min_version = match.group(1) + + +@then("the minimum version should be at least \"{expected}\"") +def step_pyyaml_minimum_version(context: Context, expected: str) -> None: + """Assert that the PyYAML minimum version is >= expected version.""" + min_version = context.pyyaml_min_version + assert min_version == expected, ( + f"PyYAML minimum version {min_version} does not match expected {expected}. " + f"Dependency spec: {context.pyyaml_version_spec}" + ) + + +@when('I attempt to import the "{module_name}" module') +def step_attempt_import(context: Context, module_name: str) -> None: + """Attempt to import the given module and record the result.""" + try: + context.imported_module = importlib.import_module(module_name) + context.import_error = None + except ImportError as exc: + context.imported_module = None + context.import_error = exc + + +@then("the import should succeed without errors") +def step_import_succeeded(context: Context) -> None: + """Assert that the previous import attempt succeeded.""" + assert context.import_error is None, f"Import failed with: {context.import_error}" + assert context.imported_module is not None, "Module was not imported" + + +@then('the version constraint in uv.lock should include specifier "{specifier}"') +def step_uv_lock_specifier(context: Context, specifier: str) -> None: + """Assert that pyyaml appears in both package.dependencies and requires-dist in uv.lock.""" + lock_path = context.pyproject_path.parent / "uv.lock" + content = lock_path.read_bytes().decode("utf-8") + + # Check that pyyaml is in the cleveragents package dependencies section + assert ( + '[[package]]\nname = "cleveragents"' in content + ), "cleveragents package section not found in uv.lock" + + # Verify pyyaml entry exists in file after cleveragents section + cleveragents_match = re.search( + r'\[\[package\]\]\s*name = "cleveragents"(.*?)^\[\[package\]\]', + content, + re.DOTALL | re.MULTILINE, + ) + assert cleveragents_match, "Could not find cleveragents package section" + + cleveragents_block = cleveragents_match.group(1) + + assert '{ name = "pyyaml" }' in cleveragents_block, ( + f"pyyaml not found in cleveragents package dependencies in uv.lock" + ) + + +def _load_toml(content: bytes) -> dict[str, Any]: + """Load a TOML file from bytes. Uses tomllib if available, else fallback.""" + try: + import tomllib + + return tomllib.loads(content.decode("utf-8")) + except ImportError: + # Fallback for older Python versions without tomllib + import tomlkit + + return tomlkit.parse(content.decode("utf-8")).unwrap() # type: ignore[no-any-return] diff --git a/pyproject.toml b/pyproject.toml index c1fdf4a71..f711440c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ dependencies = [ "tomlkit>=0.13.0", # TOML writing with comment preservation for config CLI "tenacity>=8.2.0", # Retry framework for service layer resilience "aiohttp>=3.13.4", # CVE-2026-34515 mitigation: open redirect vulnerability + "pyyaml>=6.0.3", # Security: address known YAML parsing vulnerabilities "a2a-sdk>=0.3.0,<1.0.0", # A2A Python SDK — required transport for local (stdio) and server (HTTP) modes (ADR-047); pinned <1.0.0 (removed legacy A2AClient) ] diff --git a/uv.lock b/uv.lock index 31d88e524..c0ed72e3f 100644 --- a/uv.lock +++ b/uv.lock @@ -446,6 +446,7 @@ dependencies = [ { name = "pydantic" }, { name = "pydantic-settings" }, { name = "python-ulid" }, + { name = "pyyaml" }, { name = "restrictedpython" }, { name = "rx" }, { name = "structlog" }, @@ -528,6 +529,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, + { name = "pyyaml", specifier = ">=6.0.3" }, { name = "python-ulid", specifier = ">=2.7.0" }, { name = "radon", marker = "extra == 'dev'", specifier = ">=6.0.1" }, { name = "restrictedpython", specifier = ">=7.0" }, -- 2.52.0 From 63746b4d30d654f3e04ac07a4d4646ec1b8bac04 Mon Sep 17 00:00:00 2001 From: CleverThis Date: Fri, 8 May 2026 14:00:57 +0000 Subject: [PATCH 2/4] fix(pyyaml): address CI review feedback from PR #11012 - Fix ruff lint F541 error: remove unnecessary f-string prefix - Remove dead step_uv_lock_specifier step (never called by BDD scenarios) - Update CONTRIBUTORS.md to reference correct PR number (#11012 not #9244) ISSUES CLOSED: #9055 --- .../steps/security_pyyaml_dependency_steps.py | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/features/steps/security_pyyaml_dependency_steps.py b/features/steps/security_pyyaml_dependency_steps.py index ef9e98579..33b2672f8 100644 --- a/features/steps/security_pyyaml_dependency_steps.py +++ b/features/steps/security_pyyaml_dependency_steps.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import Any from behave import given, then, when +from packaging.version import parse as parse_version from behave.runner import Context @@ -69,8 +70,8 @@ def step_read_pyyaml_dependency(context: Context) -> None: def step_pyyaml_minimum_version(context: Context, expected: str) -> None: """Assert that the PyYAML minimum version is >= expected version.""" min_version = context.pyyaml_min_version - assert min_version == expected, ( - f"PyYAML minimum version {min_version} does not match expected {expected}. " + assert parse_version(min_version) >= parse_version(expected), ( + f"PyYAML minimum version {min_version} is less than required {expected}. " f"Dependency spec: {context.pyyaml_version_spec}" ) @@ -93,40 +94,31 @@ def step_import_succeeded(context: Context) -> None: assert context.imported_module is not None, "Module was not imported" -@then('the version constraint in uv.lock should include specifier "{specifier}"') -def step_uv_lock_specifier(context: Context, specifier: str) -> None: - """Assert that pyyaml appears in both package.dependencies and requires-dist in uv.lock.""" - lock_path = context.pyproject_path.parent / "uv.lock" - content = lock_path.read_bytes().decode("utf-8") - - # Check that pyyaml is in the cleveragents package dependencies section - assert ( - '[[package]]\nname = "cleveragents"' in content - ), "cleveragents package section not found in uv.lock" - - # Verify pyyaml entry exists in file after cleveragents section - cleveragents_match = re.search( - r'\[\[package\]\]\s*name = "cleveragents"(.*?)^\[\[package\]\]', - content, - re.DOTALL | re.MULTILINE, - ) - assert cleveragents_match, "Could not find cleveragents package section" - - cleveragents_block = cleveragents_match.group(1) - - assert '{ name = "pyyaml" }' in cleveragents_block, ( - f"pyyaml not found in cleveragents package dependencies in uv.lock" - ) - - def _load_toml(content: bytes) -> dict[str, Any]: """Load a TOML file from bytes. Uses tomllib if available, else fallback.""" try: import tomllib return tomllib.loads(content.decode("utf-8")) - except ImportError: + except ImportError: # pragma: no cover — Python <3.11 # Fallback for older Python versions without tomllib import tomlkit - return tomlkit.parse(content.decode("utf-8")).unwrap() # type: ignore[no-any-return] + parsed_doc = tomlkit.parse(content.decode("utf-8")) + return _toml_to_python(parsed_doc) + + +def _toml_to_python(data: object) -> dict[str, Any]: + """Convert a TOML document from tomlkit (or tomllib) to plain Python dicts.""" + if isinstance(data, dict): + def _flatten(d: dict[Any, Any]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in d.items(): + if isinstance(value, dict): + result[key] = _flatten(value) + else: + result[key] = value + return result + return _flatten(data) + raise TypeError(f"Expected dict-like TOML table, got {type(data).__name__}") + -- 2.52.0 From 2f01e7d6250fce84a826fda2dad26e7a6b34079a Mon Sep 17 00:00:00 2001 From: CleverThis Date: Mon, 11 May 2026 05:07:02 +0000 Subject: [PATCH 3/4] fix(tests): resolve lint format failure and harden pyyaml BDD step path resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move _flatten_toml_dict to module level: eliminates nested function definition that ruff format reformats unexpectedly; makes the helper independently testable and clearly documented. - Rename helper to _flatten_toml_dict (was _flatten) for unambiguous module-level identity and improved readability. - Add _PROJECT_ROOT constant computed from __file__ so pyproject.toml is always resolved via an absolute path regardless of the process working directory — fixes brittle relative-path lookup that fails when behave-parallel forks workers whose cwd differs from the repo root. - Fix ruff format violations: wrap long assert message, normalise decorator quote style, ensure trailing newline, fix import ordering (behave imports before packaging). - All quality gates pass: lint, format --check, security_scan, dead_code. ISSUES CLOSED: #9055 --- .../steps/security_pyyaml_dependency_steps.py | 66 ++++++++++++------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/features/steps/security_pyyaml_dependency_steps.py b/features/steps/security_pyyaml_dependency_steps.py index 33b2672f8..a758cdfdb 100644 --- a/features/steps/security_pyyaml_dependency_steps.py +++ b/features/steps/security_pyyaml_dependency_steps.py @@ -8,16 +8,23 @@ from pathlib import Path from typing import Any from behave import given, then, when -from packaging.version import parse as parse_version from behave.runner import Context +from packaging.version import parse as parse_version + +# Absolute path to the project root (two levels above features/steps/). +# Using an absolute path makes these steps robust against cwd changes that +# can occur when behave-parallel forks worker processes. +_PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent @given('the pyproject.toml file exists at "{path}"') def step_pyproject_exists(context: Context, path: str) -> None: """Verify pyproject.toml exists at the given path.""" - pyproject_path = Path(path) - assert pyproject_path.exists(), f"pyproject.toml not found at {path}" - context.pyproject_path = pyproject_path + # Resolve relative paths against the project root so the step works + # regardless of the process working directory (e.g. inside parallel workers). + resolved = Path(path) if Path(path).is_absolute() else _PROJECT_ROOT / path + assert resolved.exists(), f"pyproject.toml not found at {resolved}" + context.pyproject_path = resolved @when("I read the project dependencies from pyproject.toml") @@ -60,13 +67,15 @@ def step_read_pyyaml_dependency(context: Context) -> None: # Parse the minimum version from the constraint # Examples: "pyyaml>=6.0.3", "pyyaml>=6.0", "pyyaml==6.0.2" match = re.search(r"(?:>=|==|~>)([\d]+(?:\.[\d]+)*(?:\.\w+)*)", pyyaml_dep) - assert match is not None, f"Could not parse version from PyYAML dependency: {pyyaml_dep}" + assert match is not None, ( + f"Could not parse version from PyYAML dependency: {pyyaml_dep}" + ) context.pyyaml_version_spec = pyyaml_dep context.pyyaml_min_version = match.group(1) -@then("the minimum version should be at least \"{expected}\"") +@then('the minimum version should be at least "{expected}"') def step_pyyaml_minimum_version(context: Context, expected: str) -> None: """Assert that the PyYAML minimum version is >= expected version.""" min_version = context.pyyaml_min_version @@ -94,6 +103,35 @@ def step_import_succeeded(context: Context) -> None: assert context.imported_module is not None, "Module was not imported" +def _flatten_toml_dict(d: dict[Any, Any]) -> dict[str, Any]: + """Recursively convert a tomlkit dict to a plain Python dict. + + tomlkit wraps every value in a proxy object that behaves like the + underlying Python type but is not identical to it. This helper + strips those wrappers so the result is a plain ``dict[str, Any]`` + compatible with ``tomllib`` output. + """ + result: dict[str, Any] = {} + for key, value in d.items(): + if isinstance(value, dict): + result[key] = _flatten_toml_dict(value) + else: + result[key] = value + return result + + +def _toml_to_python(data: object) -> dict[str, Any]: + """Convert a tomlkit document to a plain Python dict. + + Delegates recursive flattening to the module-level + ``_flatten_toml_dict`` helper so the logic stays testable in + isolation and ruff does not flag nested function definitions. + """ + if isinstance(data, dict): + return _flatten_toml_dict(data) + raise TypeError(f"Expected dict-like TOML table, got {type(data).__name__}") + + def _load_toml(content: bytes) -> dict[str, Any]: """Load a TOML file from bytes. Uses tomllib if available, else fallback.""" try: @@ -106,19 +144,3 @@ def _load_toml(content: bytes) -> dict[str, Any]: parsed_doc = tomlkit.parse(content.decode("utf-8")) return _toml_to_python(parsed_doc) - - -def _toml_to_python(data: object) -> dict[str, Any]: - """Convert a TOML document from tomlkit (or tomllib) to plain Python dicts.""" - if isinstance(data, dict): - def _flatten(d: dict[Any, Any]) -> dict[str, Any]: - result: dict[str, Any] = {} - for key, value in d.items(): - if isinstance(value, dict): - result[key] = _flatten(value) - else: - result[key] = value - return result - return _flatten(data) - raise TypeError(f"Expected dict-like TOML table, got {type(data).__name__}") - -- 2.52.0 From b692894c88782eda55000b0116bb5eb6dad0612d Mon Sep 17 00:00:00 2001 From: CleverThis Date: Mon, 11 May 2026 07:24:31 +0000 Subject: [PATCH 4/4] fix(tests): remove AmbiguousStep conflict in security_pyyaml_dependency_steps Duplicate step definitions for 'the pyproject.toml file exists at', 'I read the project dependencies from pyproject.toml', 'I attempt to import the module', and 'the import should succeed without errors' were present in both security_pyyaml_dependency_steps.py and tdd_a2a_sdk_dependency_steps.py, causing a behave.AmbiguousStep error that failed the unit_tests CI gate. Removed the 4 duplicate step definitions from security_pyyaml_dependency_steps.py, keeping only the 3 PyYAML-specific step definitions. The shared steps are now exclusively provided by tdd_a2a_sdk_dependency_steps.py. Also resolved merge conflicts from master in CONTRIBUTORS.md: preserved all master additions (database resource types entry) alongside the existing PyYAML upgrade entry. ISSUES CLOSED: #9055 --- .../steps/security_pyyaml_dependency_steps.py | 52 +++---------------- 1 file changed, 7 insertions(+), 45 deletions(-) diff --git a/features/steps/security_pyyaml_dependency_steps.py b/features/steps/security_pyyaml_dependency_steps.py index a758cdfdb..9150433a4 100644 --- a/features/steps/security_pyyaml_dependency_steps.py +++ b/features/steps/security_pyyaml_dependency_steps.py @@ -1,39 +1,19 @@ -"""Step definitions for PyYAML security dependency verification (#9055).""" +"""Step definitions for PyYAML security dependency verification (#9055). + +Note: the shared steps for reading pyproject.toml and verifying importability +are defined in ``tdd_a2a_sdk_dependency_steps.py`` to avoid AmbiguousStep +conflicts. This file contains only the PyYAML-specific step definitions. +""" from __future__ import annotations -import importlib import re -from pathlib import Path from typing import Any -from behave import given, then, when +from behave import then, when from behave.runner import Context from packaging.version import parse as parse_version -# Absolute path to the project root (two levels above features/steps/). -# Using an absolute path makes these steps robust against cwd changes that -# can occur when behave-parallel forks worker processes. -_PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent - - -@given('the pyproject.toml file exists at "{path}"') -def step_pyproject_exists(context: Context, path: str) -> None: - """Verify pyproject.toml exists at the given path.""" - # Resolve relative paths against the project root so the step works - # regardless of the process working directory (e.g. inside parallel workers). - resolved = Path(path) if Path(path).is_absolute() else _PROJECT_ROOT / path - assert resolved.exists(), f"pyproject.toml not found at {resolved}" - context.pyproject_path = resolved - - -@when("I read the project dependencies from pyproject.toml") -def step_read_project_dependencies(context: Context) -> None: - """Read the [project.dependencies] list from pyproject.toml.""" - content = context.pyproject_path.read_bytes() - data: dict[str, Any] = _load_toml(content) - context.project_dependencies = data.get("project", {}).get("dependencies", []) - @then('the dependency list should include a package matching "{pattern}"') def step_dependency_matches_pattern(context: Context, pattern: str) -> None: @@ -85,24 +65,6 @@ def step_pyyaml_minimum_version(context: Context, expected: str) -> None: ) -@when('I attempt to import the "{module_name}" module') -def step_attempt_import(context: Context, module_name: str) -> None: - """Attempt to import the given module and record the result.""" - try: - context.imported_module = importlib.import_module(module_name) - context.import_error = None - except ImportError as exc: - context.imported_module = None - context.import_error = exc - - -@then("the import should succeed without errors") -def step_import_succeeded(context: Context) -> None: - """Assert that the previous import attempt succeeded.""" - assert context.import_error is None, f"Import failed with: {context.import_error}" - assert context.imported_module is not None, "Module was not imported" - - def _flatten_toml_dict(d: dict[Any, Any]) -> dict[str, Any]: """Recursively convert a tomlkit dict to a plain Python dict. -- 2.52.0