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
- 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
209 lines
11 KiB
Gherkin
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
|