## Property-Based Tests for Conflict Minimality ## ## This module tests the property that minimal conflict extraction ## produces the smallest possible set of conflicting incompatibilities. ## ## Property: Conflict Minimality ## For any set of incompatibilities that cause a conflict, ## the extracted minimal set should be a subset of the original set, ## and removing any incompatibility from the minimal set should ## result in a non-conflicting set (or at least a smaller conflict). ## ## Requirements: ## - 7.5: Provide minimal conflicting requirements import std/[unittest, random, sequtils, options, algorithm] import ../src/nip/resolver/conflict_detection import ../src/nip/resolver/solver_types import ../src/nip/manifest_parser suite "Conflict Minimality Properties": test "Property: Minimal conflict is subset of original": ## For any set of incompatibilities, the minimal conflict ## should be a subset of the original set. ## ## **Feature: nip-dependency-resolution, Property 6: Conflict Minimality** ## **Validates: Requirements 7.5** # Generate random incompatibilities var incompatibilities: seq[Incompatibility] = @[] for i in 0 ..< 10: incompatibilities.add(Incompatibility( terms: @[], cause: if i mod 3 == 0: Root elif i mod 3 == 1: Dependency else: IncompatibilityCause.VariantConflict, externalContext: "Incomp " & $i, fromPackage: some("pkg" & $i), fromVersion: none(SemanticVersion) )) let minimal = extractMinimalConflict(incompatibilities) check minimal.isSome # The minimal set should be a subset of the original let minimalSet = minimal.get() check minimalSet.len <= incompatibilities.len # Every incompatibility in minimal should be in original for minIncomp in minimalSet: let found = incompatibilities.anyIt( it.externalContext == minIncomp.externalContext and it.cause == minIncomp.cause ) check found test "Property: Minimal conflict preserves root incompatibilities": ## Root incompatibilities (user requirements) should always ## be preserved in the minimal conflict set. ## ## **Feature: nip-dependency-resolution, Property 6: Conflict Minimality** ## **Validates: Requirements 7.5** # Create a mix of incompatibilities with at least one root let rootIncomp = Incompatibility( terms: @[], cause: Root, externalContext: "User requirement", fromPackage: some("nginx"), fromVersion: none(SemanticVersion) ) var incompatibilities = @[rootIncomp] for i in 0 ..< 5: incompatibilities.add(Incompatibility( terms: @[], cause: Dependency, externalContext: "Dependency " & $i, fromPackage: some("pkg" & $i), fromVersion: none(SemanticVersion) )) let minimal = extractMinimalConflict(incompatibilities) check minimal.isSome # Root incompatibilities should be preserved let hasRoot = minimal.get().anyIt(it.cause == Root) check hasRoot test "Property: Minimal conflict is deterministic": ## Extracting minimal conflict from the same set should ## always produce the same result. ## ## **Feature: nip-dependency-resolution, Property 6: Conflict Minimality** ## **Validates: Requirements 7.5** let incompatibilities = @[ Incompatibility( terms: @[], cause: Root, externalContext: "Root", fromPackage: some("pkg1"), fromVersion: none(SemanticVersion) ), Incompatibility( terms: @[], cause: Dependency, externalContext: "Dep1", fromPackage: some("pkg2"), fromVersion: none(SemanticVersion) ), Incompatibility( terms: @[], cause: Dependency, externalContext: "Dep2", fromPackage: some("pkg3"), fromVersion: none(SemanticVersion) ) ] # Extract minimal conflict multiple times let minimal1 = extractMinimalConflict(incompatibilities) let minimal2 = extractMinimalConflict(incompatibilities) let minimal3 = extractMinimalConflict(incompatibilities) # All results should be the same check minimal1.isSome check minimal2.isSome check minimal3.isSome check minimal1.get().len == minimal2.get().len check minimal2.get().len == minimal3.get().len # The contexts should match let contexts1 = minimal1.get().mapIt(it.externalContext).sorted() let contexts2 = minimal2.get().mapIt(it.externalContext).sorted() let contexts3 = minimal3.get().mapIt(it.externalContext).sorted() check contexts1 == contexts2 check contexts2 == contexts3 test "Property: Minimal conflict handles empty input": ## Extracting minimal conflict from empty set should return None. ## ## **Feature: nip-dependency-resolution, Property 6: Conflict Minimality** ## **Validates: Requirements 7.5** let minimal = extractMinimalConflict(@[]) check minimal.isNone test "Property: Minimal conflict handles single incompatibility": ## Extracting minimal conflict from single incompatibility ## should return that incompatibility. ## ## **Feature: nip-dependency-resolution, Property 6: Conflict Minimality** ## **Validates: Requirements 7.5** let incomp = Incompatibility( terms: @[], cause: Root, externalContext: "Single", fromPackage: some("pkg"), fromVersion: none(SemanticVersion) ) let minimal = extractMinimalConflict(@[incomp]) check minimal.isSome check minimal.get().len == 1 check minimal.get()[0].externalContext == "Single" test "Property: Minimal conflict reduces redundancy": ## The minimal conflict should have fewer or equal incompatibilities ## than the original set (it should not add incompatibilities). ## ## **Feature: nip-dependency-resolution, Property 6: Conflict Minimality** ## **Validates: Requirements 7.5** # Create a large set of incompatibilities var incompatibilities: seq[Incompatibility] = @[] for i in 0 ..< 20: incompatibilities.add(Incompatibility( terms: @[], cause: if i == 0: Root else: Dependency, externalContext: "Incomp " & $i, fromPackage: some("pkg" & $(i mod 5)), fromVersion: none(SemanticVersion) )) let minimal = extractMinimalConflict(incompatibilities) check minimal.isSome # Minimal should not have more incompatibilities than original check minimal.get().len <= incompatibilities.len # Minimal should have at least 1 incompatibility (if original had any) check minimal.get().len >= 1 test "Property: Minimal conflict preserves conflict causes": ## The minimal conflict should preserve the causes of conflicts ## (Root, Dependency, VariantConflict, etc.) ## ## **Feature: nip-dependency-resolution, Property 6: Conflict Minimality** ## **Validates: Requirements 7.5** let incompatibilities = @[ Incompatibility( terms: @[], cause: Root, externalContext: "Root", fromPackage: some("pkg1"), fromVersion: none(SemanticVersion) ), Incompatibility( terms: @[], cause: Dependency, externalContext: "Dep", fromPackage: some("pkg2"), fromVersion: none(SemanticVersion) ), Incompatibility( terms: @[], cause: IncompatibilityCause.VariantConflict, externalContext: "Variant", fromPackage: some("pkg3"), fromVersion: none(SemanticVersion) ) ] let minimal = extractMinimalConflict(incompatibilities) check minimal.isSome # The minimal set should contain at least one of each cause type # (or at least preserve the causes that are present) let causes = minimal.get().mapIt(it.cause) # Should have at least one incompatibility check causes.len >= 1 # All causes in minimal should be in original for cause in causes: let found = incompatibilities.anyIt(it.cause == cause) check found