## Unit Tests for NipCell Fallback ## ## This module tests the NipCell isolation fallback mechanism for the ## NIP dependency resolver. ## ## **Requirements Tested:** ## - 10.1: Detect unresolvable conflicts and suggest NipCell isolation ## - 10.2: Create separate NipCells for conflicting packages ## - 10.3: Maintain separate dependency graphs per cell ## - 10.4: Support cell switching ## - 10.5: Clean up cell-specific packages when removing cells import std/[unittest, options, sets, tables, strutils, strformat, json] import ../src/nip/resolver/nipcell_fallback import ../src/nip/resolver/conflict_detection import ../src/nip/resolver/solver_types # ============================================================================= # Test Helpers # ============================================================================= proc createVersionConflict(pkg: string): ConflictReport = ## Create a test version conflict ConflictReport( kind: VersionConflict, packages: @[pkg], details: fmt"Package '{pkg}' has conflicting version requirements", suggestions: @["Try relaxing version constraints"], conflictingTerms: @[], cyclePath: none(seq[string]) ) proc createVariantConflict(pkg: string, domain: string = "init"): ConflictReport = ## Create a test variant conflict ConflictReport( kind: VariantConflict, packages: @[pkg], details: fmt"Package '{pkg}' has conflicting exclusive variant flags in domain '{domain}'", suggestions: @["Consider using NipCell isolation"], conflictingTerms: @[], cyclePath: none(seq[string]) ) proc createCircularDependency(packages: seq[string]): ConflictReport = ## Create a test circular dependency conflict let cycleStr = packages.join(" -> ") ConflictReport( kind: CircularDependency, packages: packages, details: "Circular dependency detected: " & cycleStr, suggestions: @["Break the cycle"], conflictingTerms: @[], cyclePath: some(packages) ) # ============================================================================= # Conflict Severity Analysis Tests # ============================================================================= suite "Conflict Severity Analysis": test "Version conflict has low severity": let conflict = createVersionConflict("openssl") let severity = analyzeConflictSeverity(conflict) check severity == Low test "Variant conflict with exclusive domain has high severity": let conflict = createVariantConflict("systemd", "init") let severity = analyzeConflictSeverity(conflict) check severity == High test "Circular dependency has critical severity": let conflict = createCircularDependency(@["a", "b", "c", "a"]) let severity = analyzeConflictSeverity(conflict) check severity == Critical test "Low severity does not suggest isolation": check shouldSuggestIsolation(Low) == false test "Medium severity suggests isolation": check shouldSuggestIsolation(Medium) == true test "High severity suggests isolation": check shouldSuggestIsolation(High) == true test "Critical severity suggests isolation": check shouldSuggestIsolation(Critical) == true # ============================================================================= # Isolation Candidate Detection Tests # ============================================================================= suite "Isolation Candidate Detection": test "Detect candidates from variant conflict": let conflicts = @[createVariantConflict("openssl", "crypto")] let candidates = detectIsolationCandidates(conflicts) check candidates.len >= 1 check candidates[0].packageName == "openssl" check candidates[0].suggestedCellName == "openssl-cell" test "No candidates for low severity conflicts": let conflicts = @[createVersionConflict("zlib")] let candidates = detectIsolationCandidates(conflicts) # Version conflicts are low severity, should not suggest isolation check candidates.len == 0 test "Detect candidates from circular dependency": let conflicts = @[createCircularDependency(@["a", "b", "c", "a"])] let candidates = detectIsolationCandidates(conflicts) # Circular dependencies are critical, should suggest isolation check candidates.len >= 1 test "Multiple conflicts generate multiple candidates": let conflicts = @[ createVariantConflict("openssl", "crypto"), createVariantConflict("nginx", "http") ] let candidates = detectIsolationCandidates(conflicts) check candidates.len >= 2 # ============================================================================= # Isolation Suggestion Generation Tests # ============================================================================= suite "Isolation Suggestion Generation": test "Generate suggestion with commands": let conflict = createVariantConflict("openssl", "crypto") let candidates = @[ IsolationCandidate( packageName: "openssl", conflictingWith: @["nginx"], severity: High, suggestedCellName: "openssl-cell", reason: "Exclusive domain conflict" ) ] let suggestion = generateIsolationSuggestion(conflict, candidates) check suggestion.candidates.len == 1 check suggestion.suggestedCells.len == 1 check suggestion.commands.len >= 1 check suggestion.explanation.len > 0 test "Suggestion includes cell creation command": let conflict = createVariantConflict("openssl", "crypto") let candidates = @[ IsolationCandidate( packageName: "openssl", conflictingWith: @[], severity: High, suggestedCellName: "openssl-cell", reason: "Conflict" ) ] let suggestion = generateIsolationSuggestion(conflict, candidates) var hasCreateCommand = false for cmd in suggestion.commands: if cmd.contains("cell create"): hasCreateCommand = true break check hasCreateCommand test "Format suggestion produces readable output": let conflict = createVariantConflict("openssl", "crypto") let candidates = @[ IsolationCandidate( packageName: "openssl", conflictingWith: @["nginx"], severity: High, suggestedCellName: "openssl-cell", reason: "Conflict" ) ] let suggestion = generateIsolationSuggestion(conflict, candidates) let formatted = formatIsolationSuggestion(suggestion) check formatted.contains("IsolationSuggested") check formatted.contains("openssl") check formatted.contains("Suggested commands") # ============================================================================= # NipCell Graph Manager Tests # ============================================================================= suite "NipCell Graph Manager - Cell Creation": test "Create new cell": let manager = newNipCellGraphManager("/tmp/test-cells") let result = manager.createCell("test-cell", "Test cell description") check result.success == true check result.cellName == "test-cell" check result.cellId.len > 0 check result.error == "" test "Cannot create duplicate cell": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("test-cell") let result = manager.createCell("test-cell") check result.success == false check result.error.contains("already exists") test "List cells returns created cells": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("cell-a") discard manager.createCell("cell-b") discard manager.createCell("cell-c") let cells = manager.listCells() check cells.len == 3 check "cell-a" in cells check "cell-b" in cells check "cell-c" in cells # ============================================================================= # NipCell Graph Manager Tests - Cell Switching # ============================================================================= suite "NipCell Graph Manager - Cell Switching": test "Switch to existing cell": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("test-cell") let result = manager.switchCell("test-cell") check result.success == true check result.newCell == "test-cell" check result.error == "" test "Cannot switch to non-existent cell": let manager = newNipCellGraphManager("/tmp/test-cells") let result = manager.switchCell("non-existent") check result.success == false check result.error.contains("not found") test "Get active cell after switch": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("test-cell") discard manager.switchCell("test-cell") let activeCell = manager.getActiveCell() check activeCell.isSome check activeCell.get() == "test-cell" test "No active cell initially": let manager = newNipCellGraphManager("/tmp/test-cells") let activeCell = manager.getActiveCell() check activeCell.isNone test "Switch tracks previous cell": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("cell-a") discard manager.createCell("cell-b") discard manager.switchCell("cell-a") let result = manager.switchCell("cell-b") check result.success == true check result.previousCell.isSome check result.previousCell.get() == "cell-a" check result.newCell == "cell-b" # ============================================================================= # NipCell Graph Manager Tests - Separate Graphs # ============================================================================= suite "NipCell Graph Manager - Separate Graphs": test "Each cell has its own graph": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("cell-a") discard manager.createCell("cell-b") let graphA = manager.getCellGraph("cell-a") let graphB = manager.getCellGraph("cell-b") check graphA.isSome check graphB.isSome check graphA.get().cellName == "cell-a" check graphB.get().cellName == "cell-b" check graphA.get().cellId != graphB.get().cellId test "Get active cell graph": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("test-cell") discard manager.switchCell("test-cell") let graph = manager.getActiveCellGraph() check graph.isSome check graph.get().cellName == "test-cell" test "No active graph when no cell active": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("test-cell") let graph = manager.getActiveCellGraph() check graph.isNone # ============================================================================= # NipCell Graph Manager Tests - Package Management # ============================================================================= suite "NipCell Graph Manager - Package Management": test "Add package to cell": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("test-cell") let result = manager.addPackageToCell("test-cell", "nginx") check result == true check manager.isPackageInCell("test-cell", "nginx") test "Cannot add package to non-existent cell": let manager = newNipCellGraphManager("/tmp/test-cells") let result = manager.addPackageToCell("non-existent", "nginx") check result == false test "Remove package from cell": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("test-cell") discard manager.addPackageToCell("test-cell", "nginx") let result = manager.removePackageFromCell("test-cell", "nginx") check result == true check not manager.isPackageInCell("test-cell", "nginx") test "Get cell packages": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("test-cell") discard manager.addPackageToCell("test-cell", "nginx") discard manager.addPackageToCell("test-cell", "openssl") discard manager.addPackageToCell("test-cell", "zlib") let packages = manager.getCellPackages("test-cell") check packages.len == 3 check "nginx" in packages check "openssl" in packages check "zlib" in packages test "Packages are isolated between cells": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("cell-a") discard manager.createCell("cell-b") discard manager.addPackageToCell("cell-a", "nginx") discard manager.addPackageToCell("cell-b", "apache") check manager.isPackageInCell("cell-a", "nginx") check not manager.isPackageInCell("cell-a", "apache") check manager.isPackageInCell("cell-b", "apache") check not manager.isPackageInCell("cell-b", "nginx") # ============================================================================= # NipCell Graph Manager Tests - Cell Deletion # ============================================================================= suite "NipCell Graph Manager - Cell Deletion": test "Delete existing cell": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("test-cell") let result = manager.deleteCell("test-cell") check result == true check manager.listCells().len == 0 test "Cannot delete non-existent cell": let manager = newNipCellGraphManager("/tmp/test-cells") let result = manager.deleteCell("non-existent") check result == false test "Deleting active cell deactivates it": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("test-cell") discard manager.switchCell("test-cell") discard manager.deleteCell("test-cell") check manager.getActiveCell().isNone test "Packages are cleaned up when cell is deleted": let manager = newNipCellGraphManager("/tmp/test-cells") discard manager.createCell("test-cell") discard manager.addPackageToCell("test-cell", "nginx") discard manager.addPackageToCell("test-cell", "openssl") discard manager.deleteCell("test-cell") # Cell no longer exists, so packages are gone check manager.getCellPackages("test-cell").len == 0 # ============================================================================= # Conflict-Triggered Fallback Tests # ============================================================================= suite "Conflict-Triggered Fallback": test "No suggestion for empty conflicts": let suggestion = checkForIsolationFallback(@[]) check suggestion.isNone test "No suggestion for low severity conflicts": let conflicts = @[createVersionConflict("zlib")] let suggestion = checkForIsolationFallback(conflicts) check suggestion.isNone test "Suggestion for high severity conflicts": let conflicts = @[createVariantConflict("openssl", "crypto")] let suggestion = checkForIsolationFallback(conflicts) check suggestion.isSome check suggestion.get().candidates.len >= 1 test "Handle unresolvable conflict with auto-create": let manager = newNipCellGraphManager("/tmp/test-cells") let conflict = createVariantConflict("openssl", "crypto") let (suggestion, cellsCreated) = manager.handleUnresolvableConflict( conflict, autoCreate = true ) check suggestion.candidates.len >= 1 check cellsCreated.len >= 1 check manager.listCells().len >= 1 test "Handle unresolvable conflict without auto-create": let manager = newNipCellGraphManager("/tmp/test-cells") let conflict = createVariantConflict("openssl", "crypto") let (suggestion, cellsCreated) = manager.handleUnresolvableConflict( conflict, autoCreate = false ) check suggestion.candidates.len >= 1 check cellsCreated.len == 0 check manager.listCells().len == 0 # ============================================================================= # Cell Serialization Tests # ============================================================================= suite "Cell Serialization": test "Serialize cell to JSON": var cell = newNipCellGraph("test-cell", "test-id-123") cell.packages.incl("nginx") cell.packages.incl("openssl") cell.metadata["description"] = "Test cell" let json = cell.toJson() check json["cellName"].getStr() == "test-cell" check json["cellId"].getStr() == "test-id-123" check json["packages"].len == 2 check json["metadata"]["description"].getStr() == "Test cell" test "Deserialize cell from JSON": let json = %*{ "cellName": "test-cell", "cellId": "test-id-123", "packages": ["nginx", "openssl"], "created": "2025-01-01T00:00:00Z", "lastModified": "2025-01-01T00:00:00Z", "metadata": {"description": "Test cell"} } let cell = fromJson(json) check cell.cellName == "test-cell" check cell.cellId == "test-id-123" check "nginx" in cell.packages check "openssl" in cell.packages check cell.metadata["description"] == "Test cell" # ============================================================================= # Run Tests # ============================================================================= when isMainModule: echo "Running NipCell Fallback Tests..."