Files
cleveractors-core/features/registry_cache.feature
CoreRasurae 054b432197
CI / lint (pull_request) Successful in 56s
CI / typecheck (pull_request) Successful in 56s
CI / security (pull_request) Successful in 1m2s
CI / quality (pull_request) Successful in 41s
CI / integration_tests (pull_request) Successful in 1m11s
CI / build (pull_request) Successful in 45s
CI / unit_tests (pull_request) Successful in 3m29s
CI / coverage (pull_request) Successful in 3m14s
CI / status-check (pull_request) Successful in 4s
CI / lint (push) Successful in 48s
CI / typecheck (push) Successful in 58s
CI / security (push) Successful in 1m1s
CI / quality (push) Successful in 39s
CI / integration_tests (push) Successful in 1m15s
CI / build (push) Successful in 41s
CI / unit_tests (push) Successful in 3m31s
CI / coverage (push) Successful in 3m27s
CI / status-check (push) Successful in 3s
feat(registry): implement RegistryCache with LRU eviction and TTL
- Add RegistryCache with LRU eviction, TTL expiry, SHA-1 content validation
 - Add CacheFactory for centralised cache configuration (Factory Method pattern)
 - Add CacheStats dataclass for hit/miss/eviction observability
 - Implement singleflight coalescing for concurrent-miss protection
 - Use tuple keys for resolve store to prevent separator collision
 - Fix cancellation-safe singleflight via plain Future + asyncio.shield
 - Independent max_size budgets per store (content + resolve)
 - Concurrent test assertions strengthened
 - All test imports moved to module level per CONTRIBUTING.md
 - Unused dead code removed from tests

 ISSUES CLOSED: #28
2026-06-11 19:26:04 +01:00

209 lines
11 KiB
Gherkin

Feature: Registry Client-Side Cache
As a developer using the Package Registry Standard v1.0.0,
I want a client-side LRU cache that stores retrieved packages locally,
validates cached content via SHA-1, and expires entries based on TTL,
so that I can reduce network roundtrips and ensure content integrity.
Background:
Given a clean test environment for registry cache
# ── Cache Initialisation ──────────────────────────────────────────────
Scenario: Create cache with default max size and TTL
When I create a RegistryCache with default settings
Then the cache max_size should be 256
Then the cache ttl should be 300
Scenario: Create cache with custom max size and TTL
When I create a RegistryCache with max_size 64 and ttl 60
Then the cache max_size should be 64
Then the cache ttl should be 60
Scenario: Create cache with invalid client raises TypeError
When I try to create a RegistryCache with a non-client argument
Then a TypeError should be raised by the registry cache
Scenario: Create cache with invalid max_size raises ValueError
When I try to create a RegistryCache with max_size 0
Then a ValueError should be raised by the registry cache
Scenario: Create cache with invalid ttl raises ValueError
When I try to create a RegistryCache with ttl -1
Then a ValueError should be raised by the registry cache
Scenario: Create cache with boolean max_size raises ValueError
When I try to create a RegistryCache with max_size boolean True
Then a ValueError should be raised by the registry cache
# ── Cache Miss — Fresh Fetch ───────────────────────────────────────────
Scenario: First access triggers upstream fetch
When I create a RegistryCache with validation disabled and label "dummy"
When the mock upstream records a standard package
When I call cache get_package for the standard package
Then the cache stats hits should be 0
Then the cache stats misses should be 1
Scenario: First access returns upstream content
When I create a RegistryCache with validation disabled and label "dummy"
When the mock upstream records a standard package
When I call cache get_package for the standard package
Then the cache content should match the standard package
# ── Cache Hit ──────────────────────────────────────────────────────────
Scenario: Second access hits cache and does not call upstream
When I create a RegistryCache with validation disabled and label "dummy"
When the mock upstream records a standard package
When I call cache get_package for the standard package
When I call cache get_package for the standard package
Then the cache stats hits should be 1
# ── Content Validation ─────────────────────────────────────────────────
Scenario: Valid content passes SHA-1 validation and serves from cache
When I create a RegistryCache with validation enabled and label "validated"
When the mock upstream records a standard package
When I call cache get_package for the standard package
When I call cache get_package for the standard package
Then the cache stats hits should be 1
Then the cache stats misses should be 1
Scenario: Tampered cache entry fails validation and re-fetches
When I create a RegistryCache with validation enabled and label "validated"
When the mock upstream records a standard package
When I call cache get_package for the standard package
When I tamper with the cached standard package
When I call cache get_package for the standard package
Then the cache stats misses should be 2
# ── TTL Expiration ──────────────────────────────────────────────────────
Scenario: Expired TTL triggers re-fetch
When I create a RegistryCache with max_size 256 and ttl 0.01 and no validation and label "dummy"
When the mock upstream records a standard package
When I call cache get_package for the standard package
Then I wait 0.05 seconds for TTL to expire
When I call cache get_package for the standard package
Then the cache stats misses should be 2
# ── LRU Eviction ────────────────────────────────────────────────────────
Scenario: Exceeding max_size evicts least-recently-used entry
When I create a RegistryCache with max_size 2 and ttl 300 and no validation and label "dummy"
When the mock upstream records package A
When the mock upstream records package B
When the mock upstream records package C
When I call cache get_package for package A
When I call cache get_package for package B
When I call cache get_package for package C
Then the cache stats evictions should be 1
Scenario: LRU eviction removes the least-recently-used entry not the MRU
When I create a RegistryCache with max_size 2 and ttl 300 and no validation and label "dummy"
When the mock upstream records package A
When the mock upstream records package B
When I call cache get_package for package A
When I call cache get_package for package B
When I call cache get_package for package A
When the mock upstream records package C
When I call cache get_package for package C
Then the cache stats evictions should be 1
Then package B should NOT be in cache
Then package A should be in cache
Then package C should be in cache
# ── Cache Statistics ────────────────────────────────────────────────────
Scenario: Statistics track hits and misses across multiple accesses
When I create a RegistryCache with validation disabled and label "dummy"
When the mock upstream records a standard package
When I call cache get_package for the standard package
When I call cache get_package for the standard package
Then the cache stats hits should be 1
Then the cache stats misses should be 1
# ── Invalidation ────────────────────────────────────────────────────────
Scenario: Invalidating an entry causes a cache miss on next access
When I create a RegistryCache with validation disabled and label "dummy"
When the mock upstream records a standard package
When I call cache get_package for the standard package
When I invalidate the standard package from cache
When the mock upstream records a standard package
When I call cache get_package for the standard package
Then the cache stats misses should be 2
Scenario: Invalidating a missing entry returns False
When I create a RegistryCache with default settings
When I invalidate a missing cache entry
Then the cache should report the entry was NOT invalidated
# ── Clear ───────────────────────────────────────────────────────────────
Scenario: Clearing the cache resets statistics and removes all entries
When I create a RegistryCache with validation disabled and label "dummy"
When the mock upstream records a standard package
When I call cache get_package for the standard package
When I clear the cache
Then the cache stats hits should be 0
Then the cache stats misses should be 0
Then the standard package should NOT be in cache
# ── Wrapping RegistryClient ─────────────────────────────────────────────
Scenario: Cache wraps RegistryClient transparently
When I create a RegistryCache wrapping a real RegistryClient
Then the cache should have the same base URL "https://registry.example.com"
# ── Concurrent Access ───────────────────────────────────────────────────
Scenario: Concurrent access does not corrupt cache state
When I create a RegistryCache with validation disabled and label "dummy"
When the mock upstream records a standard package
When I call cache get_package for the standard package 8 times concurrently
Then the cache stats hits should be 0
Then the cache stats misses should be 1
Then the total stats hits plus misses should be 1
Then the cache content should match the standard package
Then the standard package should be in cache
# ── get_package_content Alias ───────────────────────────────────────────
Scenario: get_package_content alias delegates to get_package
When I create a RegistryCache with validation disabled and label "dummy"
When the mock upstream records a standard package
When I call cache get_package_content for the standard package
Then the cache stats misses should be 1
Then the cache content should match the standard package
# ── Error Propagation ───────────────────────────────────────────────────
Scenario: Invalid PackageId raises InvalidPackageIdError before upstream call
When I create a RegistryCache with validation disabled and label "dummy"
When I try to call cache get_package with an invalid package ID
Then an InvalidPackageIdError should be raised by the registry cache
Scenario: Upstream PackageNotFoundError propagates to caller
When I create a RegistryCache with validation disabled and label "dummy"
When the mock upstream records a standard package
When the mock upstream raises PackageNotFoundError for the standard package
When I call cache get_package for the standard package and catch the error
Then a PackageNotFoundError should be raised by the registry cache
Scenario: Upstream RegistryNetworkError propagates to caller
When I create a RegistryCache with validation disabled and label "dummy"
When the mock upstream records a standard package
When the mock upstream raises RegistryNetworkError for the standard package
When I call cache get_package for the standard package and catch the error
Then a RegistryNetworkError should be raised by the registry cache
# ── Async Context Manager ───────────────────────────────────────────────
Scenario: Async context manager works correctly and closes client
When I create a RegistryCache with validation disabled and label "dummy"
When the mock upstream records a standard package
When I use the RegistryCache as an async context manager
Then the underlying client should be closed after context exit