"""Step definitions for RegistryCache coverage gap tests.""" from __future__ import annotations import asyncio import time from typing import Any from unittest.mock import AsyncMock, MagicMock from behave import given, then, when from cleveractors.registry.cache import CacheFactory, RegistryCache from cleveractors.registry.client import RegistryClient from cleveractors.registry.reference_resolver import PackageContentResolver from cleveractors.registry.types import PackageId, PackageReference async def _async_noop() -> None: pass def _run_async(context: Any, coro: Any) -> Any: return context.loop.run_until_complete(coro) def _create_mock_client(context: Any) -> dict[str, Any]: mock = MagicMock(spec=RegistryClient) mock.get_package = AsyncMock(return_value=dict(context._test_content)) resolve_return = dict(context._test_content) resolve_return["package_id"] = context._pid mock.resolve_package = AsyncMock(return_value=resolve_return) mock.close = AsyncMock() return mock # ── Common test data ─────────────────────────────────────────────────── @given("a clean test environment for cache coverage") def step_clean_coverage_env(context: Any) -> None: context.cache = None context.resolver = None context.error = None context.result = None context._test_content = { "name": "cov-pkg", "type": "actor", "version": "v1.0.0", "config": {"agent_type": "llm", "model": "gpt-4"}, } context._pid = "pkg_act_0123456789abcdef0123456789abcdef01234567" # ── RegistryCache.resolve_package ────────────────────────────────────── @when("I create a RegistryCache wrapping a mock client") def step_create_cache_mock(context: Any) -> None: context.cache = RegistryCache(_create_mock_client(context), validate_content=False) @when( "I call resolve_package with type={pkg_type} ns={ns} name={name} version={version}" ) def step_call_resolve_package( context: Any, pkg_type: str, ns: str, name: str, version: str ) -> None: async def _call() -> None: context.resolve_result = await context.cache.resolve_package( package_type=pkg_type, namespace=ns, name=name, version=version, ) _run_async(context, _call()) @when( "I call resolve_package twice with type={pkg_type} ns={ns} name={name} version={version}" ) def step_call_resolve_package_twice( context: Any, pkg_type: str, ns: str, name: str, version: str ) -> None: async def _call() -> None: await context.cache.resolve_package( package_type=pkg_type, namespace=ns, name=name, version=version, ) context.resolve_result = await context.cache.resolve_package( package_type=pkg_type, namespace=ns, name=name, version=version, ) _run_async(context, _call()) @when("I create a RegistryCache with validation enabled and mock client") def step_create_cache_val_enabled_mock(context: Any) -> None: context.cache = RegistryCache(_create_mock_client(context), validate_content=True) # ── RegistryCache.put ────────────────────────────────────────────────── @when("I put a package entry into the cache") def step_put_into_cache(context: Any) -> None: async def _call() -> None: await context.cache.put(context._pid, dict(context._test_content)) _run_async(context, _call()) @when("I put {count:d} entries into the cache at max_size {max_size:d}") def step_put_entries_exceeding_max(context: Any, count: int, max_size: int) -> None: mock = MagicMock(spec=RegistryClient) mock.get_package = AsyncMock(return_value=dict(context._test_content)) mock.resolve_package = AsyncMock(return_value=dict(context._test_content)) mock.close = AsyncMock() context.cache = RegistryCache(mock, max_size=max_size, validate_content=False) async def _call() -> None: for i in range(count): pid = f"pkg_act_{i:040x}" await context.cache.put( pid, {**context._test_content, "name": f"cov-pkg-{i}"} ) _run_async(context, _call()) # ── CacheFactory ─────────────────────────────────────────────────────── @when("I create a CacheFactory with default settings") def step_create_factory_default(context: Any) -> None: context.factory = CacheFactory() @when("I create a CacheFactory with max_size={max_size:d} ttl={ttl}") def step_create_factory_custom(context: Any, max_size: int, ttl: str) -> None: context.factory = CacheFactory(max_size=max_size, ttl=float(ttl)) @when("I try to create a CacheFactory with max_size={max_size:d}") def step_create_factory_bad_size(context: Any, max_size: int) -> None: try: CacheFactory(max_size=max_size) except ValueError as exc: context.error = exc @when("I try to create a CacheFactory with ttl={ttl}") def step_create_factory_bad_ttl(context: Any, ttl: str) -> None: try: CacheFactory(ttl=float(ttl)) except ValueError as exc: context.error = exc @when("I try to create a CacheFactory with max_size boolean True") def step_create_factory_bool(context: Any) -> None: try: CacheFactory(max_size=True) except ValueError as exc: context.error = exc @when("I call factory.create with a mock client") def step_factory_create(context: Any) -> None: context.cache = context.factory.create(_create_mock_client(context)) # ── PackageContentResolver with CacheFactory ─────────────────────────── @when("I create a PackageContentResolver with a CacheFactory") def step_create_resolver_with_factory(context: Any) -> None: import tempfile from pathlib import Path from cleveractors.registry.local_store import LocalPackageStore context._tmpdir = tempfile.TemporaryDirectory() context._base_dir = Path(context._tmpdir.name) (context._base_dir / "test").mkdir(parents=True, exist_ok=True) (context._base_dir / "test" / "file.yaml").write_text( "name: test-package\ndescription: test\n", encoding="utf-8" ) context._local_store = LocalPackageStore(context._base_dir) context.factory = CacheFactory(max_size=64, ttl=300.0, validate_content=False) context.resolver = PackageContentResolver( cache_factory=context.factory, local_store=context._local_store ) context.resolver.set_server_alias("test.registry", "http://127.0.0.1:1") @when("I inject a mock registry client into the resolver") def step_inject_mock_client(context: Any) -> None: resolve_return = dict(context._test_content) resolve_return["package_id"] = context._pid mock = MagicMock(spec=RegistryClient) mock.resolve_package = AsyncMock(return_value=resolve_return) mock.get_package = AsyncMock(return_value=dict(context._test_content)) mock.close = AsyncMock() context.resolver.clients["http://127.0.0.1:1"] = mock @when("I resolve a registry reference via the resolver") def step_resolve_registry_ref(context: Any) -> None: pr = PackageReference.from_string("test.registry:acme/pkg@v1.0.0") context.resolve_result = context.resolver.resolve(pr, package_type="actor") @when("I resolve the same registry reference twice") def step_resolve_registry_ref_twice(context: Any) -> None: pr = PackageReference.from_string("test.registry:acme/pkg@v1.0.0") context.resolver.resolve(pr, package_type="actor") context.resolve_result = context.resolver.resolve(pr, package_type="actor") @when("I resolve a local reference via the resolver") def step_resolve_local_ref(context: Any) -> None: pr = PackageReference.from_string("local:test/file.yaml") context.resolve_result = context.resolver.resolve(pr) @when("I resolve the same local reference twice") def step_resolve_local_ref_twice(context: Any) -> None: pr = PackageReference.from_string("local:test/file.yaml") context.resolver.resolve(pr) context.resolve_result = context.resolver.resolve(pr) # ── total_stats ──────────────────────────────────────────────────────── @when("I read the resolver total_stats") def step_read_total_stats(context: Any) -> None: context.stats = context.resolver.total_stats # ── clear_cache ──────────────────────────────────────────────────────── @when("I clear the resolver cache") def step_clear_resolver_cache(context: Any) -> None: context.resolver.clear_cache() # ── close_all ────────────────────────────────────────────────────────── @when("I close all resolver resources") def step_close_all_resources(context: Any) -> None: async def _close() -> None: await context.resolver.close_all() _run_async(context, _close()) # ── Assertions ───────────────────────────────────────────────────────── @then("the resolve result should contain package_id") def step_resolve_result_has_package_id(context: Any) -> None: assert context.resolve_result is not None assert "package_id" in context.resolve_result, ( f"Expected package_id in {context.resolve_result}" ) @then("the resolve result should contain the test content") def step_resolve_result_has_content(context: Any) -> None: assert context.resolve_result is not None assert context.resolve_result.get("name") == context._test_content["name"] @then("the package should be present in the cache") def step_pkg_in_cache(context: Any) -> None: assert context._pid in context.cache, f"Expected {context._pid} in cache" @then("the factory should be created with max_size={expected:d}") def step_factory_max_size(context: Any, expected: int) -> None: assert context.factory.max_size == expected @then("the factory should be created with ttl={expected}") def step_factory_ttl(context: Any, expected: str) -> None: assert context.factory.ttl == float(expected) @then("a ValueError should be raised by the cache factory") def step_factory_value_error(context: Any) -> None: assert context.error is not None assert isinstance(context.error, ValueError), ( f"Expected ValueError, got {type(context.error).__name__}" ) @then("the cache should wrap the mock client") def step_cache_has_client(context: Any) -> None: assert context.cache is not None assert context.cache._client is not None @then("the resolve result should not be None") def step_result_not_none(context: Any) -> None: assert context.resolve_result is not None @then("the total_stats should have been accessible without error") def step_stats_accessible(context: Any) -> None: assert context.stats is not None @then("the resolver cache should be empty") def step_resolver_cache_empty(context: Any) -> None: assert len(context.resolver.cache) == 0, ( f"Expected empty cache, got {len(context.resolver.cache)} entries" ) @when("I create a RegistryCache with max_size {max_size:d} and mock client") def step_create_cache_max_size_mock(context: Any, max_size: int) -> None: context.cache = RegistryCache( _create_mock_client(context), max_size=max_size, validate_content=False, ) @then("the resolver clients and content caches should be empty") def step_resolver_clients_empty(context: Any) -> None: assert len(context.resolver.clients) == 0, ( f"Expected 0 clients, got {len(context.resolver.clients)}" ) assert len(context.resolver._content_caches) == 0, ( f"Expected 0 content caches, got {len(context.resolver._content_caches)}" ) @then("the resolver total stats hits should be {expected:d}") def step_total_stats_hits(context: Any, expected: int) -> None: assert context.stats.hits == expected, ( f"Expected total_stats.hits={expected}, got {context.stats.hits}" ) @then("the resolver total stats misses should be {expected:d}") def step_total_stats_misses(context: Any, expected: int) -> None: assert context.stats.misses == expected, ( f"Expected total_stats.misses={expected}, got {context.stats.misses}" ) @when("I create a RegistryCache with ttl {ttl:f} and mock client") def step_create_cache_short_ttl(context: Any, ttl: float) -> None: context.cache = RegistryCache( _create_mock_client(context), ttl=ttl, validate_content=False ) @when("I wait for TTL to expire") def step_wait_ttl_expire(context: Any) -> None: time.sleep(0.1) @when("I trigger SHA-1 validation with a TypeError-raising canonicalizer") def step_cache_val_typeerror(context: Any) -> None: from cleveractors.registry.types import PackageContent, PackageId cache = context.cache cache._validate_content_enabled = True def _bad_compute(content_data, pkg_type): raise TypeError("bogus type") cache._canonicalizer.compute_package_id = _bad_compute pid = PackageId.from_string("pkg_act_0123456789abcdef0123456789abcdef01234567") content = PackageContent(id=pid, content={"key": "val"}) context.cache_val_result = cache._validate_content(content) @then("the validation should return False") def step_cache_val_false(context: Any) -> None: assert context.cache_val_result is False, ( f"Expected False, got {context.cache_val_result}" )