## Unit Tests for Conflict Detection ## ## Tests for detecting and reporting various types of conflicts ## in the NIP dependency resolver. ## ## Requirements tested: ## - 7.1: Detect and report version conflicts ## - 7.2: Detect and report variant conflicts ## - 7.3: Detect and report circular dependencies ## - 7.4: Detect and report missing packages ## - 7.5: Provide actionable suggestions for resolution import std/[unittest, tables, sets, options, sequtils, strutils] import ../src/nip/resolver/conflict_detection import ../src/nip/resolver/solver_types import ../src/nip/resolver/variant_types import ../src/nip/manifest_parser suite "Conflict Detection Tests": # --- Version Conflict Tests --- test "Detect version conflict - exact versions": ## Test detecting conflicting exact version requirements ## Requirements: 7.1 let constraints = @[ VersionConstraint(operator: OpExact, version: SemanticVersion(major: 1, minor: 0, patch: 0)), VersionConstraint(operator: OpExact, version: SemanticVersion(major: 2, minor: 0, patch: 0)) ] let conflict = detectVersionConflict("nginx", constraints) check conflict.isSome check conflict.get().kind == VersionConflict check conflict.get().packages.contains("nginx") check conflict.get().details.contains("1.0.0") check conflict.get().details.contains("2.0.0") check conflict.get().suggestions.len > 0 test "No version conflict - compatible versions": ## Test that compatible versions don't trigger conflict ## Requirements: 7.1 let constraints = @[ VersionConstraint(operator: OpGreaterEq, version: SemanticVersion(major: 1, minor: 0, patch: 0)), VersionConstraint(operator: OpGreaterEq, version: SemanticVersion(major: 1, minor: 5, patch: 0)) ] let conflict = detectVersionConflict("nginx", constraints) check conflict.isNone test "No version conflict - single constraint": ## Test that single constraint doesn't trigger conflict ## Requirements: 7.1 let constraints = @[ VersionConstraint(operator: OpExact, version: SemanticVersion(major: 1, minor: 0, patch: 0)) ] let conflict = detectVersionConflict("nginx", constraints) check conflict.isNone test "Detect version conflict - exact vs greater-equal": ## Test detecting conflict between exact and >= constraint ## Requirements: 7.1 let constraints = @[ VersionConstraint(operator: OpExact, version: SemanticVersion(major: 1, minor: 0, patch: 0)), VersionConstraint(operator: OpGreaterEq, version: SemanticVersion(major: 2, minor: 0, patch: 0)) ] let conflict = detectVersionConflict("zlib", constraints) check conflict.isSome check conflict.get().kind == VersionConflict check conflict.get().suggestions.len > 0 # --- Variant Conflict Tests --- test "Detect variant conflict - exclusive domains": ## Test detecting conflicting exclusive variant flags ## Requirements: 7.2 var profile1 = newVariantProfile() var domain1 = newVariantDomain("init", Exclusive) domain1.flags.incl("systemd") profile1.addDomain(domain1) var profile2 = newVariantProfile() var domain2 = newVariantDomain("init", Exclusive) domain2.flags.incl("dinit") profile2.addDomain(domain2) let demands = @[ VariantDemand(packageName: "nginx", variantProfile: profile1, optional: false), VariantDemand(packageName: "nginx", variantProfile: profile2, optional: false) ] let conflict = detectVariantConflict("nginx", demands) check conflict.isSome check conflict.get().kind == conflict_detection.VariantConflict check conflict.get().details.contains("init") check conflict.get().suggestions.len > 0 test "No variant conflict - non-exclusive domains": ## Test that non-exclusive domains don't trigger conflict ## Requirements: 7.2 var profile1 = newVariantProfile() var domain1 = newVariantDomain("features", NonExclusive) domain1.flags.incl("wayland") profile1.addDomain(domain1) var profile2 = newVariantProfile() var domain2 = newVariantDomain("features", NonExclusive) domain2.flags.incl("x11") profile2.addDomain(domain2) let demands = @[ VariantDemand(packageName: "nginx", variantProfile: profile1, optional: false), VariantDemand(packageName: "nginx", variantProfile: profile2, optional: false) ] let conflict = detectVariantConflict("nginx", demands) check conflict.isNone test "No variant conflict - single demand": ## Test that single demand doesn't trigger conflict ## Requirements: 7.2 var profile = newVariantProfile() var domain = newVariantDomain("init", Exclusive) domain.flags.incl("systemd") profile.addDomain(domain) let demands = @[ VariantDemand(packageName: "nginx", variantProfile: profile, optional: false) ] let conflict = detectVariantConflict("nginx", demands) check conflict.isNone test "Detect variant conflict - multiple exclusive domains": ## Test detecting conflicts in multiple exclusive domains ## Requirements: 7.2 var profile1 = newVariantProfile() var domain1a = newVariantDomain("init", Exclusive) domain1a.flags.incl("systemd") profile1.addDomain(domain1a) var domain1b = newVariantDomain("libc", Exclusive) domain1b.flags.incl("glibc") profile1.addDomain(domain1b) var profile2 = newVariantProfile() var domain2a = newVariantDomain("init", Exclusive) domain2a.flags.incl("dinit") profile2.addDomain(domain2a) var domain2b = newVariantDomain("libc", Exclusive) domain2b.flags.incl("musl") profile2.addDomain(domain2b) let demands = @[ VariantDemand(packageName: "nginx", variantProfile: profile1, optional: false), VariantDemand(packageName: "nginx", variantProfile: profile2, optional: false) ] let conflict = detectVariantConflict("nginx", demands) check conflict.isSome check conflict.get().kind == conflict_detection.VariantConflict # --- Circular Dependency Tests --- test "Detect circular dependency - simple cycle": ## Test detecting a simple circular dependency ## Requirements: 7.3 var graph: Table[string, seq[string]] = initTable[string, seq[string]]() graph["nginx"] = @["zlib"] graph["zlib"] = @["nginx"] let conflict = detectCircularDependency(graph, "nginx") check conflict.isSome check conflict.get().kind == CircularDependency check conflict.get().cyclePath.isSome check conflict.get().cyclePath.get().len >= 2 check conflict.get().suggestions.len > 0 test "Detect circular dependency - three-way cycle": ## Test detecting a three-way circular dependency ## Requirements: 7.3 var graph: Table[string, seq[string]] = initTable[string, seq[string]]() graph["nginx"] = @["zlib"] graph["zlib"] = @["pcre"] graph["pcre"] = @["nginx"] let conflict = detectCircularDependency(graph, "nginx") check conflict.isSome check conflict.get().kind == CircularDependency check conflict.get().cyclePath.isSome let cycle = conflict.get().cyclePath.get() check cycle.len >= 3 check cycle.contains("nginx") check cycle.contains("zlib") check cycle.contains("pcre") test "No circular dependency - linear chain": ## Test that linear dependency chains don't trigger circular conflict ## Requirements: 7.3 var graph: Table[string, seq[string]] = initTable[string, seq[string]]() graph["nginx"] = @["zlib"] graph["zlib"] = @["pcre"] graph["pcre"] = @[] let conflict = detectCircularDependency(graph, "nginx") check conflict.isNone test "No circular dependency - diamond dependency": ## Test that diamond dependencies don't trigger circular conflict ## Requirements: 7.3 var graph: Table[string, seq[string]] = initTable[string, seq[string]]() graph["nginx"] = @["zlib", "pcre"] graph["zlib"] = @["openssl"] graph["pcre"] = @["openssl"] graph["openssl"] = @[] let conflict = detectCircularDependency(graph, "nginx") check conflict.isNone test "Detect circular dependency - self-loop": ## Test detecting a package that depends on itself ## Requirements: 7.3 var graph: Table[string, seq[string]] = initTable[string, seq[string]]() graph["nginx"] = @["nginx"] let conflict = detectCircularDependency(graph, "nginx") check conflict.isSome check conflict.get().kind == CircularDependency # --- Missing Package Tests --- test "Detect missing package": ## Test detecting a missing package ## Requirements: 7.4 let available = toHashSet(["nginx", "zlib", "pcre"]) let conflict = detectMissingPackage("openssl", available) check conflict.isSome check conflict.get().kind == MissingPackage check conflict.get().packages.contains("openssl") check conflict.get().details.contains("openssl") check conflict.get().suggestions.len > 0 test "No missing package - package exists": ## Test that existing packages don't trigger missing conflict ## Requirements: 7.4 let available = toHashSet(["nginx", "zlib", "pcre", "openssl"]) let conflict = detectMissingPackage("openssl", available) check conflict.isNone test "Detect missing package - suggest similar names": ## Test that similar package names are suggested ## Requirements: 7.4 let available = toHashSet(["nginx", "nginx-ssl", "nginx-http2"]) let conflict = detectMissingPackage("nginx-http3", available) check conflict.isSome check conflict.get().kind == MissingPackage # Should suggest similar names let suggestions = conflict.get().suggestions.join(" ") check suggestions.contains("nginx") or suggestions.contains("Did you mean") test "Detect missing package - empty repository": ## Test detecting missing package in empty repository ## Requirements: 7.4 let available: HashSet[string] = initHashSet[string]() let conflict = detectMissingPackage("nginx", available) check conflict.isSome check conflict.get().kind == MissingPackage # --- Build Hash Mismatch Tests --- test "Detect build hash mismatch": ## Test detecting build hash mismatch ## Requirements: 7.5 let conflict = detectBuildHashMismatch( "nginx", "blake3-abc123def456", "blake3-xyz789abc123" ) check conflict.isSome check conflict.get().kind == BuildHashMismatch check conflict.get().packages.contains("nginx") check conflict.get().details.contains("abc123def456") check conflict.get().details.contains("xyz789abc123") check conflict.get().suggestions.len > 0 test "No build hash mismatch - hashes match": ## Test that matching hashes don't trigger mismatch conflict ## Requirements: 7.5 let conflict = detectBuildHashMismatch( "nginx", "blake3-abc123def456", "blake3-abc123def456" ) check conflict.isNone # --- Conflict Formatting Tests --- test "Format version conflict": ## Test formatting a version conflict report ## Requirements: 7.1, 7.5 let report = ConflictReport( kind: VersionConflict, packages: @["nginx"], details: "nginx requires version 1.0.0 and 2.0.0", suggestions: @["Update to compatible version"], conflictingTerms: @[], cyclePath: none(seq[string]) ) let formatted = formatConflict(report) check formatted.contains("VersionConflict") check formatted.contains("nginx") check formatted.contains("Update to compatible version") test "Format variant conflict": ## Test formatting a variant conflict report ## Requirements: 7.2, 7.5 let report = ConflictReport( kind: VariantConflict, packages: @["nginx"], details: "nginx requires +systemd and +dinit", suggestions: @["Choose one variant"], conflictingTerms: @[], cyclePath: none(seq[string]) ) let formatted = formatConflict(report) check formatted.contains("VariantConflict") check formatted.contains("nginx") check formatted.contains("Choose one variant") test "Format circular dependency": ## Test formatting a circular dependency report ## Requirements: 7.3, 7.5 let report = ConflictReport( kind: CircularDependency, packages: @["nginx", "zlib", "nginx"], details: "nginx -> zlib -> nginx", suggestions: @["Break the cycle"], conflictingTerms: @[], cyclePath: some(@["nginx", "zlib", "nginx"]) ) let formatted = formatConflict(report) check formatted.contains("CircularDependency") check formatted.contains("nginx") check formatted.contains("Break the cycle") test "Format missing package": ## Test formatting a missing package report ## Requirements: 7.4, 7.5 let report = ConflictReport( kind: MissingPackage, packages: @["openssl"], details: "openssl not found", suggestions: @["Check package name", "Update repositories"], conflictingTerms: @[], cyclePath: none(seq[string]) ) let formatted = formatConflict(report) check formatted.contains("MissingPackage") check formatted.contains("openssl") check formatted.contains("Check package name") test "Format build hash mismatch": ## Test formatting a build hash mismatch report ## Requirements: 7.5 let report = ConflictReport( kind: BuildHashMismatch, packages: @["nginx"], details: "Hash mismatch for nginx", suggestions: @["Reinstall package"], conflictingTerms: @[], cyclePath: none(seq[string]) ) let formatted = formatConflict(report) check formatted.contains("BuildHashMismatch") check formatted.contains("nginx") check formatted.contains("Reinstall package") # --- Conflict Analysis Tests --- test "Analyze conflict origins - version conflict": ## Test analyzing origins of a version conflict ## Requirements: 7.5 var manifests: Table[string, seq[VariantDemand]] = initTable[string, seq[VariantDemand]]() manifests["nginx"] = @[ VariantDemand(packageName: "nginx", variantProfile: newVariantProfile(), optional: false), VariantDemand(packageName: "nginx", variantProfile: newVariantProfile(), optional: false) ] let report = ConflictReport( kind: VersionConflict, packages: @["nginx"], details: "Version conflict", suggestions: @[], conflictingTerms: @[], cyclePath: none(seq[string]) ) let analysis = analyzeConflictOrigins(report, manifests) check analysis.len > 0 check analysis[0].contains("nginx") check analysis[0].contains("2") test "Analyze conflict origins - circular dependency": ## Test analyzing origins of a circular dependency ## Requirements: 7.5 let report = ConflictReport( kind: CircularDependency, packages: @["nginx", "zlib", "nginx"], details: "Circular dependency", suggestions: @[], conflictingTerms: @[], cyclePath: some(@["nginx", "zlib", "nginx"]) ) let analysis = analyzeConflictOrigins(report, initTable[string, seq[VariantDemand]]()) check analysis.len > 0 check analysis[0].contains("3") # --- Minimal Conflict Extraction Tests --- test "Extract minimal conflict": ## Test extracting minimal conflicting incompatibilities ## Requirements: 7.5 let incomp1 = Incompatibility( terms: @[], cause: Root, externalContext: "Incomp 1", fromPackage: none(string), fromVersion: none(SemanticVersion) ) let incomp2 = Incompatibility( terms: @[], cause: Dependency, externalContext: "Incomp 2", fromPackage: none(string), fromVersion: none(SemanticVersion) ) let minimal = extractMinimalConflict(@[incomp1, incomp2]) check minimal.isSome check minimal.get().len == 2 test "Extract minimal conflict - empty list": ## Test extracting minimal conflict from empty list ## Requirements: 7.5 let minimal = extractMinimalConflict(@[]) check minimal.isNone test "Extract minimal conflict - single incompatibility": ## Test extracting minimal conflict from single incompatibility ## Requirements: 7.5 let incomp = Incompatibility( terms: @[], cause: Root, externalContext: "Single incompatibility", fromPackage: none(string), fromVersion: none(SemanticVersion) ) let minimal = extractMinimalConflict(@[incomp]) check minimal.isSome check minimal.get().len == 1 test "Extract minimal conflict - multiple incompatibilities": ## Test extracting minimal conflict from multiple incompatibilities ## Requirements: 7.5 let incomp1 = Incompatibility( terms: @[], cause: Root, externalContext: "Root requirement", fromPackage: some("nginx"), fromVersion: none(SemanticVersion) ) let incomp2 = Incompatibility( terms: @[], cause: Dependency, externalContext: "Dependency conflict", fromPackage: some("nginx"), fromVersion: none(SemanticVersion) ) let incomp3 = Incompatibility( terms: @[], cause: VariantConflict, externalContext: "Variant conflict", fromPackage: some("openssl"), fromVersion: none(SemanticVersion) ) let minimal = extractMinimalConflict(@[incomp1, incomp2, incomp3]) check minimal.isSome # The minimal set should have at least 1 incompatibility check minimal.get().len >= 1 # The minimal set should have at most all incompatibilities check minimal.get().len <= 3 test "Extract minimal conflict - preserves root incompatibilities": ## Test that root incompatibilities are preserved in minimal set ## Requirements: 7.5 let rootIncompatibility = Incompatibility( terms: @[], cause: Root, externalContext: "User requirement", fromPackage: some("nginx"), fromVersion: none(SemanticVersion) ) let dependencyIncompatibility = Incompatibility( terms: @[], cause: Dependency, externalContext: "Dependency", fromPackage: some("zlib"), fromVersion: none(SemanticVersion) ) let minimal = extractMinimalConflict(@[rootIncompatibility, dependencyIncompatibility]) check minimal.isSome # Root incompatibilities should be preserved let hasRoot = minimal.get().anyIt(it.cause == Root) check hasRoot test "Extract minimal conflict - handles variant conflicts": ## Test that variant conflicts are handled correctly ## Requirements: 7.5 let variantConflict1 = Incompatibility( terms: @[], cause: IncompatibilityCause.VariantConflict, externalContext: "Variant conflict 1", fromPackage: some("nginx"), fromVersion: none(SemanticVersion) ) let variantConflict2 = Incompatibility( terms: @[], cause: IncompatibilityCause.VariantConflict, externalContext: "Variant conflict 2", fromPackage: some("nginx"), fromVersion: none(SemanticVersion) ) let minimal = extractMinimalConflict(@[variantConflict1, variantConflict2]) check minimal.isSome # Should have at least one variant conflict let hasVariantConflict = minimal.get().anyIt(it.cause == IncompatibilityCause.VariantConflict) check hasVariantConflict test "Extract minimal conflict - deterministic results": ## Test that minimal conflict extraction is deterministic ## Requirements: 7.5 let incomp1 = Incompatibility( terms: @[], cause: Root, externalContext: "Incomp 1", fromPackage: some("pkg1"), fromVersion: none(SemanticVersion) ) let incomp2 = Incompatibility( terms: @[], cause: Dependency, externalContext: "Incomp 2", fromPackage: some("pkg2"), fromVersion: none(SemanticVersion) ) let incomp3 = Incompatibility( terms: @[], cause: VariantConflict, externalContext: "Incomp 3", fromPackage: some("pkg3"), fromVersion: none(SemanticVersion) ) let incompatibilities = @[incomp1, incomp2, incomp3] # Extract minimal conflict multiple times let minimal1 = extractMinimalConflict(incompatibilities) let minimal2 = extractMinimalConflict(incompatibilities) # Results should be the same check minimal1.isSome check minimal2.isSome check minimal1.get().len == minimal2.get().len