## Tests for Binary Serialization and Cache Key Calculation ## ## This test suite verifies: ## - Deterministic MessagePack serialization ## - Correct deserialization (round-trip) ## - Cache key determinism ## - Cache invalidation on metadata changes import unittest import tables import ../src/nip/resolver/serialization import ../src/nip/resolver/types suite "MessagePack Serialization": test "Empty graph serialization": let graph = DependencyGraph( rootPackage: PackageId(name: "test", version: "1.0", variant: "default"), nodes: @[], timestamp: 1234567890 ) let binary = toMessagePack(graph) let reconstructed = fromMessagePack(binary) check reconstructed.rootPackage.name == "test" check reconstructed.rootPackage.version == "1.0" check reconstructed.rootPackage.variant == "default" check reconstructed.nodes.len == 0 check reconstructed.timestamp == 1234567890 test "Single node graph serialization": let graph = DependencyGraph( rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "ssl"), nodes: @[ DependencyNode( packageId: PackageId(name: "nginx", version: "1.24.0", variant: "ssl"), dependencies: @[], buildHash: "xxh3-abc123", metadata: {"source": "official"}.toTable ) ], timestamp: 1700000000 ) let binary = toMessagePack(graph) let reconstructed = fromMessagePack(binary) check reconstructed.nodes.len == 1 check reconstructed.nodes[0].packageId.name == "nginx" check reconstructed.nodes[0].buildHash == "xxh3-abc123" check reconstructed.nodes[0].metadata["source"] == "official" test "Complex graph with dependencies": let graph = DependencyGraph( rootPackage: PackageId(name: "app", version: "1.0", variant: "default"), nodes: @[ DependencyNode( packageId: PackageId(name: "app", version: "1.0", variant: "default"), dependencies: @[ PackageId(name: "libssl", version: "3.0", variant: "default"), PackageId(name: "zlib", version: "1.2.13", variant: "default") ], buildHash: "xxh3-app123", metadata: {"type": "application"}.toTable ), DependencyNode( packageId: PackageId(name: "libssl", version: "3.0", variant: "default"), dependencies: @[], buildHash: "xxh3-ssl456", metadata: {"type": "library"}.toTable ), DependencyNode( packageId: PackageId(name: "zlib", version: "1.2.13", variant: "default"), dependencies: @[], buildHash: "xxh3-zlib789", metadata: {"type": "library"}.toTable ) ], timestamp: 1700000000 ) let binary = toMessagePack(graph) let reconstructed = fromMessagePack(binary) check reconstructed.nodes.len == 3 check reconstructed.nodes[0].dependencies.len == 2 # Verify dependencies are preserved let appNode = reconstructed.nodes[0] check appNode.dependencies[0].name in ["libssl", "zlib"] check appNode.dependencies[1].name in ["libssl", "zlib"] suite "Serialization Determinism": test "Same graph produces identical binary": let graph1 = DependencyGraph( rootPackage: PackageId(name: "test", version: "1.0", variant: "default"), nodes: @[ DependencyNode( packageId: PackageId(name: "dep1", version: "2.0", variant: "default"), dependencies: @[], buildHash: "hash1", metadata: {"key": "value"}.toTable ) ], timestamp: 1234567890 ) let graph2 = graph1 # Identical graph let binary1 = toMessagePack(graph1) let binary2 = toMessagePack(graph2) check binary1 == binary2 test "Node order doesn't affect binary (sorted)": let graph1 = DependencyGraph( rootPackage: PackageId(name: "test", version: "1.0", variant: "default"), nodes: @[ DependencyNode( packageId: PackageId(name: "aaa", version: "1.0", variant: "default"), dependencies: @[], buildHash: "hash1", metadata: initTable[string, string]() ), DependencyNode( packageId: PackageId(name: "zzz", version: "1.0", variant: "default"), dependencies: @[], buildHash: "hash2", metadata: initTable[string, string]() ) ], timestamp: 1234567890 ) let graph2 = DependencyGraph( rootPackage: PackageId(name: "test", version: "1.0", variant: "default"), nodes: @[ DependencyNode( packageId: PackageId(name: "zzz", version: "1.0", variant: "default"), dependencies: @[], buildHash: "hash2", metadata: initTable[string, string]() ), DependencyNode( packageId: PackageId(name: "aaa", version: "1.0", variant: "default"), dependencies: @[], buildHash: "hash1", metadata: initTable[string, string]() ) ], timestamp: 1234567890 ) let binary1 = toMessagePack(graph1) let binary2 = toMessagePack(graph2) # Should be identical because nodes are sorted by packageId check binary1 == binary2 test "Dependency order doesn't affect binary (sorted)": let graph1 = DependencyGraph( rootPackage: PackageId(name: "test", version: "1.0", variant: "default"), nodes: @[ DependencyNode( packageId: PackageId(name: "app", version: "1.0", variant: "default"), dependencies: @[ PackageId(name: "aaa", version: "1.0", variant: "default"), PackageId(name: "zzz", version: "1.0", variant: "default") ], buildHash: "hash1", metadata: initTable[string, string]() ) ], timestamp: 1234567890 ) let graph2 = DependencyGraph( rootPackage: PackageId(name: "test", version: "1.0", variant: "default"), nodes: @[ DependencyNode( packageId: PackageId(name: "app", version: "1.0", variant: "default"), dependencies: @[ PackageId(name: "zzz", version: "1.0", variant: "default"), PackageId(name: "aaa", version: "1.0", variant: "default") ], buildHash: "hash1", metadata: initTable[string, string]() ) ], timestamp: 1234567890 ) let binary1 = toMessagePack(graph1) let binary2 = toMessagePack(graph2) # Should be identical because dependencies are sorted check binary1 == binary2 suite "Cache Key Calculation": test "Cache key is deterministic": let key1 = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: "repo-hash-123", variantDemand: VariantDemand( useFlags: @["ssl", "http2"], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @["-O2", "-march=native"] ) ) let key2 = key1 # Identical key let hash1 = calculateCacheKey(key1) let hash2 = calculateCacheKey(key2) check hash1 == hash2 check hash1.len == 32 # xxh3_128 produces 32-character hex string test "Different packages produce different keys": let key1 = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: "repo-hash-123", variantDemand: VariantDemand( useFlags: @[], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let key2 = CacheKey( rootPackage: "apache", # Different package rootConstraint: ">=2.4.0", repoStateHash: "repo-hash-123", variantDemand: VariantDemand( useFlags: @[], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let hash1 = calculateCacheKey(key1) let hash2 = calculateCacheKey(key2) check hash1 != hash2 test "Different USE flags produce different keys": let key1 = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: "repo-hash-123", variantDemand: VariantDemand( useFlags: @["ssl"], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let key2 = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: "repo-hash-123", variantDemand: VariantDemand( useFlags: @["ssl", "http2"], # Different USE flags libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let hash1 = calculateCacheKey(key1) let hash2 = calculateCacheKey(key2) check hash1 != hash2 test "USE flag order doesn't affect key (sorted)": let key1 = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: "repo-hash-123", variantDemand: VariantDemand( useFlags: @["ssl", "http2", "brotli"], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let key2 = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: "repo-hash-123", variantDemand: VariantDemand( useFlags: @["brotli", "http2", "ssl"], # Different order libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let hash1 = calculateCacheKey(key1) let hash2 = calculateCacheKey(key2) # Should be identical because USE flags are sorted check hash1 == hash2 test "Different repo state produces different keys": let key1 = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: "repo-hash-123", variantDemand: VariantDemand( useFlags: @[], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let key2 = CacheKey( rootPackage: "nginx", rootConstraint: ">=1.24.0", repoStateHash: "repo-hash-456", # Different repo state variantDemand: VariantDemand( useFlags: @[], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) ) let hash1 = calculateCacheKey(key1) let hash2 = calculateCacheKey(key2) check hash1 != hash2 suite "Global Repo State Hash": test "Empty repositories produce deterministic hash": let repos: seq[Repository] = @[] let hash1 = calculateGlobalRepoStateHash(repos) let hash2 = calculateGlobalRepoStateHash(repos) 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"}.toTable ) ] ) ] let repos2 = repos1 # Identical let hash1 = calculateGlobalRepoStateHash(repos1) let hash2 = calculateGlobalRepoStateHash(repos2) check hash1 == hash2 test "Different metadata 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 "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 suite "Variant Demand Canonicalization": test "Canonical form is deterministic": let demand1 = VariantDemand( useFlags: @["ssl", "http2"], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @["-O2", "-march=native"] ) let demand2 = demand1 # Identical let canon1 = canonicalizeVariantDemand(demand1) let canon2 = canonicalizeVariantDemand(demand2) check canon1 == canon2 test "USE flag order doesn't affect canonical form": let demand1 = VariantDemand( useFlags: @["ssl", "http2", "brotli"], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) let demand2 = VariantDemand( useFlags: @["brotli", "http2", "ssl"], # Different order libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @[] ) let canon1 = canonicalizeVariantDemand(demand1) let canon2 = canonicalizeVariantDemand(demand2) check canon1 == canon2 test "Build flag order doesn't affect canonical form": let demand1 = VariantDemand( useFlags: @[], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @["-O2", "-march=native", "-flto"] ) let demand2 = VariantDemand( useFlags: @[], libc: "musl", allocator: "jemalloc", targetArch: "x86_64", buildFlags: @["-flto", "-march=native", "-O2"] # Different order ) let canon1 = canonicalizeVariantDemand(demand1) let canon2 = canonicalizeVariantDemand(demand2) check canon1 == canon2