Files
CoreRasurae 8f986c1e31 test(coverage): add coverage scenarios for pre-existing code paths
Additional BDD scenarios covering registry resolver errors, cache
TTL expiry, runtime dispatch normalization, template base edge cases,
validation actor coverage gaps, and YAML Jinja loader deferred rendering.
2026-06-23 10:49:16 +01:00

396 lines
14 KiB
Python

"""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}"
)