nip/tests/test_nipcell_fallback.nim

503 lines
17 KiB
Nim

## 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..."