## Property-Based Test: Manifest Hash Determinism ## ## **Feature:** 01-nip-unified-storage-and-formats ## **Property 9:** Manifest Hash Determinism ## **Validates:** Requirements 6.4, 7.5 ## ## **Property Statement:** ## For any manifest, calculating the hash twice SHALL produce identical results ## ## **Test Strategy:** ## 1. Generate random manifests with valid data (all three formats: NPK, NIP, NEXTER) ## 2. Calculate hash twice for each manifest ## 3. Verify hashes are identical (determinism) ## 4. Verify hash format is valid (xxh3-) ## 5. Verify different manifests produce different hashes (collision resistance) ## 6. Verify field order doesn't affect hash (sorted internally) import std/[unittest, times, options, random, strutils, sets] import nip/manifest_parser # ============================================================================ # Test Generators # ============================================================================ proc genSemanticVersion(): SemanticVersion = ## Generate random semantic version SemanticVersion( major: rand(0..10), minor: rand(0..20), patch: rand(0..50), prerelease: if rand(1) == 0: "alpha" & $rand(10) else: "", build: if rand(1) == 0: "build" & $rand(100) else: "" ) proc genDependencySpec(): DependencySpec = ## Generate random dependency specification DependencySpec( name: "dep-" & $rand(1000), versionConstraint: VersionConstraint( operator: [OpExact, OpGreater, OpGreaterEq, OpTilde, OpCaret][rand(4)], version: genSemanticVersion() ), optional: rand(1) == 0, features: if rand(1) == 0: @["feature" & $rand(5)] else: @[] ) proc genPackageManifest(format: FormatType): PackageManifest = ## Generate random package manifest var manifest = PackageManifest( format: format, name: "package-" & $rand(1000), version: genSemanticVersion(), license: ["MIT", "GPL-3.0", "Apache-2.0", "BSD-3-Clause"][rand(3)] ) # Optional fields (randomly include) if rand(1) == 0: manifest.description = some("Description " & $rand(100)) if rand(1) == 0: manifest.homepage = some("https://example.com/" & $rand(100)) if rand(1) == 0: manifest.author = some("Author " & $rand(50)) if rand(1) == 0: manifest.timestamp = some($now()) # Dependencies (random count) let depCount = rand(0..5) for i in 0.. 0: echo "\nFirst 5 errors:" for i in 0.. 5: passCount.inc() else: failCount.inc() echo $format, " iteration ", i, ": Invalid hash format: ", hash except CatchableError as e: failCount.inc() echo $format, " iteration ", i, ": ", e.msg echo "\nHash Format Test Results:" echo " Passed: ", passCount, "/99" echo " Failed: ", failCount, "/99" check passCount == 99 test "Property 9: Collision Resistance - Different manifests produce different hashes": ## Verify that different manifests produce different hashes ## (collision resistance property) var hashes: HashSet[string] var collisionCount = 0 var totalCount = 0 for format in [NPK, NIP, NEXTER]: for i in 0..<33: let manifest = genPackageManifest(format) let hash = calculateManifestHash(manifest) if hash in hashes: collisionCount.inc() echo "Collision detected: ", hash else: hashes.incl(hash) totalCount.inc() echo "\nCollision Resistance Test Results:" echo " Total manifests: ", totalCount echo " Unique hashes: ", hashes.len echo " Collisions: ", collisionCount # With 99 random manifests, we should have 99 unique hashes # (collision probability with xxh3-128 is < 2^-100) check collisionCount == 0 check hashes.len == totalCount test "Property 9: Field Order Independence - Sorted fields produce consistent hash": ## Verify that field order doesn't affect hash ## (internal sorting ensures determinism) var passCount = 0 var failCount = 0 for i in 0..<100: try: # Create manifest with dependencies in random order var manifest1 = genPackageManifest(NPK) manifest1.dependencies = @[ DependencySpec(name: "dep-c", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]), DependencySpec(name: "dep-a", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]), DependencySpec(name: "dep-b", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]) ] # Create same manifest with dependencies in different order var manifest2 = manifest1 manifest2.dependencies = @[ DependencySpec(name: "dep-a", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]), DependencySpec(name: "dep-b", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]), DependencySpec(name: "dep-c", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]) ] # Hashes should be identical (sorted internally) let hash1 = calculateManifestHash(manifest1) let hash2 = calculateManifestHash(manifest2) if hash1 == hash2: passCount.inc() else: failCount.inc() echo "Iteration ", i, ": Field order affected hash" except CatchableError as e: failCount.inc() echo "Iteration ", i, ": ", e.msg echo "\nField Order Independence Test Results:" echo " Passed: ", passCount, "/100" echo " Failed: ", failCount, "/100" check passCount == 100 test "Property 9: Hash Verification - verifyManifestHash works correctly": ## Test the hash verification function var passCount = 0 var failCount = 0 for format in [NPK, NIP, NEXTER]: for i in 0..<33: try: let manifest = genPackageManifest(format) let hash = calculateManifestHash(manifest) # Verify with correct hash if verifyManifestHash(manifest, hash): passCount.inc() else: failCount.inc() echo $format, " iteration ", i, ": Verification failed for correct hash" # Verify with incorrect hash (should fail) let wrongHash = "xxh3-wrong-hash" if not verifyManifestHash(manifest, wrongHash): passCount.inc() else: failCount.inc() echo $format, " iteration ", i, ": Verification passed for wrong hash" except CatchableError as e: failCount.inc() echo $format, " iteration ", i, ": ", e.msg echo "\nHash Verification Test Results:" echo " Passed: ", passCount, "/198" # 99 correct + 99 incorrect echo " Failed: ", failCount, "/198" check passCount == 198 test "Property 9: Minimal Manifest - Hash works with minimal required fields": ## Test that hash calculation works with only required fields var passCount = 0 var failCount = 0 for format in [NPK, NIP, NEXTER]: for i in 0..<33: try: # Create minimal manifest (only required fields) let manifest = PackageManifest( format: format, name: "minimal-" & $i, version: SemanticVersion(major: 1, minor: 0, patch: 0), license: "MIT", buildHash: "xxh3-build", sourceHash: "xxh3-source", artifactHash: "xxh3-artifact" ) # Calculate hash twice let hash1 = calculateManifestHash(manifest) let hash2 = calculateManifestHash(manifest) if hash1 == hash2 and hash1.startsWith("xxh3-"): passCount.inc() else: failCount.inc() echo $format, " iteration ", i, ": Minimal manifest hash failed" except CatchableError as e: failCount.inc() echo $format, " iteration ", i, ": ", e.msg echo "\nMinimal Manifest Test Results:" echo " Passed: ", passCount, "/99" echo " Failed: ", failCount, "/99" check passCount == 99 when isMainModule: # Run tests randomize() echo "Running Manifest Hash Determinism Property Tests..." echo "Testing Property 9: Manifest Hash Determinism" echo "Validates: Requirements 6.4, 7.5" echo ""