## Cache Invalidation Strategy Tests ## ## This test suite verifies that the GlobalRepoStateHash correctly triggers ## cache invalidation when repository metadata changes. This is the keystone ## of the caching system's correctness. ## ## **Test Strategy:** ## - Verify hash changes on metadata modifications ## - Verify cache invalidation on hash changes ## - Verify cache remains valid when hash unchanged ## - Test various metadata change scenarios import unittest import tables import ../src/nip/resolver/serialization import ../src/nip/resolver/resolution_cache import ../src/nip/resolver/types import ../src/nip/cas/storage suite "Global Repo State Hash Calculation": test "Empty repositories produce deterministic hash": let repos1: seq[Repository] = @[] let repos2: seq[Repository] = @[] let hash1 = calculateGlobalRepoStateHash(repos1) let hash2 = calculateGlobalRepoStateHash(repos2) check hash1 == hash2 check hash1.len == 32 # xxh3_128 produces 32-character hex string test "Same repositories produce identical hash": let repos1 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official", "arch": "x86_64"}.toTable ), PackageMetadata( name: "zlib", version: "1.2.13", metadata: {"source": "official"}.toTable ) ] ) ] let repos2 = repos1 # Identical let hash1 = calculateGlobalRepoStateHash(repos1) let hash2 = calculateGlobalRepoStateHash(repos2) check hash1 == hash2 test "Different package version produces different hash": let repos1 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official"}.toTable ) ] ) ] let repos2 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.1", # Different version metadata: {"source": "official"}.toTable ) ] ) ] let hash1 = calculateGlobalRepoStateHash(repos1) let hash2 = calculateGlobalRepoStateHash(repos2) check hash1 != hash2 test "Different package metadata produces different hash": let repos1 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official", "arch": "x86_64"}.toTable ) ] ) ] let repos2 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official", "arch": "aarch64"}.toTable # Different arch ) ] ) ] let hash1 = calculateGlobalRepoStateHash(repos1) let hash2 = calculateGlobalRepoStateHash(repos2) check hash1 != hash2 test "Adding package produces different hash": let repos1 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official"}.toTable ) ] ) ] let repos2 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official"}.toTable ), PackageMetadata( name: "apache", version: "2.4.0", metadata: {"source": "official"}.toTable ) ] ) ] let hash1 = calculateGlobalRepoStateHash(repos1) let hash2 = calculateGlobalRepoStateHash(repos2) check hash1 != hash2 test "Removing package produces different hash": let repos1 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official"}.toTable ), PackageMetadata( name: "apache", version: "2.4.0", metadata: {"source": "official"}.toTable ) ] ) ] let repos2 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official"}.toTable ) ] ) ] let hash1 = calculateGlobalRepoStateHash(repos1) let hash2 = calculateGlobalRepoStateHash(repos2) check hash1 != hash2 test "Package order doesn't affect hash (sorted)": let repos1 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "aaa", version: "1.0", metadata: initTable[string, string]() ), PackageMetadata( name: "zzz", version: "1.0", metadata: initTable[string, string]() ) ] ) ] let repos2 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "zzz", version: "1.0", metadata: initTable[string, string]() ), PackageMetadata( name: "aaa", version: "1.0", metadata: initTable[string, string]() ) ] ) ] let hash1 = calculateGlobalRepoStateHash(repos1) let hash2 = calculateGlobalRepoStateHash(repos2) # Should be identical because metadata hashes are sorted check hash1 == hash2 test "Multiple repositories combined correctly": let repos1 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official"}.toTable ) ] ), Repository( name: "testing", packages: @[ PackageMetadata( name: "apache", version: "2.4.0", metadata: {"source": "testing"}.toTable ) ] ) ] let repos2 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official"}.toTable ) ] ) ] let hash1 = calculateGlobalRepoStateHash(repos1) let hash2 = calculateGlobalRepoStateHash(repos2) check hash1 != hash2 suite "Cache Invalidation on Metadata Changes": test "Cache invalidated when package version changes": let cas = newCASStorage("/tmp/test-cas-inv-1") let cache = newResolutionCache(cas) # Initial repository state let repos1 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official"}.toTable ) ] ) ] let repoHash1 = calculateGlobalRepoStateHash(repos1) cache.updateRepoHash(repoHash1) # Cache a resolution result let key = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: repoHash1, variantDemand: VariantDemand( useFlags: @[], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let graph = DependencyGraph( rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"), nodes: @[], timestamp: 1700000000 ) cache.put(key, graph) check cache.get(key).value.isSome # Update repository (new package version) let repos2 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.1", # Version changed metadata: {"source": "official"}.toTable ) ] ) ] let repoHash2 = calculateGlobalRepoStateHash(repos2) check repoHash1 != repoHash2 # Hash should change cache.updateRepoHash(repoHash2) # Cache should be invalidated check cache.get(key).value.isNone test "Cache invalidated when package added": let cas = newCASStorage("/tmp/test-cas-inv-2") let cache = newResolutionCache(cas) # Initial repository state let repos1 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official"}.toTable ) ] ) ] let repoHash1 = calculateGlobalRepoStateHash(repos1) cache.updateRepoHash(repoHash1) # Cache a resolution result let key = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: repoHash1, variantDemand: VariantDemand( useFlags: @[], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let graph = DependencyGraph( rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"), nodes: @[], timestamp: 1700000000 ) cache.put(key, graph) check cache.get(key).value.isSome # Add new package to repository let repos2 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official"}.toTable ), PackageMetadata( name: "apache", version: "2.4.0", metadata: {"source": "official"}.toTable ) ] ) ] let repoHash2 = calculateGlobalRepoStateHash(repos2) check repoHash1 != repoHash2 # Hash should change cache.updateRepoHash(repoHash2) # Cache should be invalidated check cache.get(key).value.isNone test "Cache invalidated when package metadata changes": let cas = newCASStorage("/tmp/test-cas-inv-3") let cache = newResolutionCache(cas) # Initial repository state let repos1 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official", "arch": "x86_64"}.toTable ) ] ) ] let repoHash1 = calculateGlobalRepoStateHash(repos1) cache.updateRepoHash(repoHash1) # Cache a resolution result let key = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: repoHash1, variantDemand: VariantDemand( useFlags: @[], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let graph = DependencyGraph( rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"), nodes: @[], timestamp: 1700000000 ) cache.put(key, graph) check cache.get(key).value.isSome # Update package metadata let repos2 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official", "arch": "aarch64"}.toTable # Arch changed ) ] ) ] let repoHash2 = calculateGlobalRepoStateHash(repos2) check repoHash1 != repoHash2 # Hash should change cache.updateRepoHash(repoHash2) # Cache should be invalidated check cache.get(key).value.isNone test "Cache remains valid when repo state unchanged": let cas = newCASStorage("/tmp/test-cas-inv-4") let cache = newResolutionCache(cas) # Initial repository state let repos = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official"}.toTable ) ] ) ] let repoHash = calculateGlobalRepoStateHash(repos) cache.updateRepoHash(repoHash) # Cache a resolution result let key = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: repoHash, variantDemand: VariantDemand( useFlags: @[], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let graph = DependencyGraph( rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"), nodes: @[], timestamp: 1700000000 ) cache.put(key, graph) check cache.get(key).value.isSome # Update with same hash (no metadata change) cache.updateRepoHash(repoHash) # Cache should still be valid check cache.get(key).value.isSome check cache.get(key).source == L1Hit suite "Cache Invalidation Edge Cases": test "Multiple cached entries all invalidated": let cas = newCASStorage("/tmp/test-cas-inv-5") let cache = newResolutionCache(cas) # Initial repository state let repos1 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official"}.toTable ), PackageMetadata( name: "apache", version: "2.4.0", metadata: {"source": "official"}.toTable ) ] ) ] let repoHash1 = calculateGlobalRepoStateHash(repos1) cache.updateRepoHash(repoHash1) # Cache multiple resolution results let key1 = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: repoHash1, variantDemand: VariantDemand( useFlags: @[], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let key2 = CacheKey( rootPackage: "apache", rootConstraint: ">=2.4.0", repoStateHash: repoHash1, variantDemand: VariantDemand( useFlags: @[], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let graph1 = DependencyGraph( rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"), nodes: @[], timestamp: 1700000000 ) let graph2 = DependencyGraph( rootPackage: PackageId(name: "apache", version: "2.4.0", variant: "default"), nodes: @[], timestamp: 1700000000 ) cache.put(key1, graph1) cache.put(key2, graph2) check cache.get(key1).value.isSome check cache.get(key2).value.isSome # Update repository (change metadata) let repos2 = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.1", # Version changed metadata: {"source": "official"}.toTable ), PackageMetadata( name: "apache", version: "2.4.0", metadata: {"source": "official"}.toTable ) ] ) ] let repoHash2 = calculateGlobalRepoStateHash(repos2) cache.updateRepoHash(repoHash2) # All cached entries should be invalidated check cache.get(key1).value.isNone check cache.get(key2).value.isNone test "Cache survives multiple updates with same hash": let cas = newCASStorage("/tmp/test-cas-inv-6") let cache = newResolutionCache(cas) let repos = @[ Repository( name: "main", packages: @[ PackageMetadata( name: "nginx", version: "1.24.0", metadata: {"source": "official"}.toTable ) ] ) ] let repoHash = calculateGlobalRepoStateHash(repos) cache.updateRepoHash(repoHash) let key = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: repoHash, variantDemand: VariantDemand( useFlags: @[], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let graph = DependencyGraph( rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"), nodes: @[], timestamp: 1700000000 ) cache.put(key, graph) # Multiple updates with same hash for i in 0..<10: cache.updateRepoHash(repoHash) check cache.get(key).value.isSome test "Empty repository hash is deterministic": let repos1: seq[Repository] = @[] let repos2: seq[Repository] = @[] let hash1 = calculateGlobalRepoStateHash(repos1) let hash2 = calculateGlobalRepoStateHash(repos2) check hash1 == hash2 let cas = newCASStorage("/tmp/test-cas-inv-7") let cache = newResolutionCache(cas) cache.updateRepoHash(hash1) cache.updateRepoHash(hash2) # Should not trigger invalidation (same hash) let metrics = cache.getMetrics() check metrics.l1Size == 0 # No entries cached yet