## NimPak Integration Tests ## ## End-to-end integration tests for the NimPak package manager. ## Task 46: Integration testing. ## ## Tests cover: ## - Cross-format deduplication (NPK, NIP, NEXTER sharing CAS) ## - Atomic installations (no partial states) ## - Garbage collection safety (pinned objects preserved) ## - Migration workflows (format conversions) ## - Error recovery (graceful failure handling) import std/[os, strutils, strformat, times, sequtils, sets] import unittest import ../src/nimpak/cas import ../src/nimpak/migration import ../src/nimpak/errors import ../src/nip/types suite "Integration - Cross-Format Deduplication": var testDir: string var casManager: CasManager setup: testDir = getTempDir() / "nip_integration_test_" & $epochTime().int createDir(testDir) casManager = initCasManager(testDir / "cas", testDir / "cas" / "system") teardown: removeDir(testDir) test "Same content across NPK, NIP, NEXTER shares CAS object": # Simulate the same library being used by all three formats let sharedLibData = @[byte(1), byte(2), byte(3), byte(4), byte(5)] # Store for NPK format let npkResult = casManager.storeObject(sharedLibData) check npkResult.isOk let hash1 = npkResult.get().hash discard casManager.addReference(hash1, NPK, "libfoo-npk") # Store same data for NIP format - should deduplicate let nipResult = casManager.storeObject(sharedLibData) check nipResult.isOk let hash2 = nipResult.get().hash discard casManager.addReference(hash2, NIP, "myapp-nip") # Store same data for NEXTER format - should deduplicate let nexterResult = casManager.storeObject(sharedLibData) check nexterResult.isOk let hash3 = nexterResult.get().hash discard casManager.addReference(hash3, NEXTER, "devenv-nexter") # All three should have the same hash (deduplication) check hash1 == hash2 check hash2 == hash3 # Object should exist only once in CAS check casManager.objectExists(hash1) # Reference count should reflect multiple stores let refCount = casManager.getRefCount(hash1) check refCount >= 3 # At least 3 stores test "Deduplication across multiple packages of same format": # Common data shared by multiple packages let commonData = @[byte(10), byte(20), byte(30)] var storedHashes: seq[string] = @[] # Simulate 5 NPK packages sharing the same file for i in 1..5: let result = casManager.storeObject(commonData) check result.isOk storedHashes.add(result.get().hash) discard casManager.addReference(result.get().hash, NPK, fmt"pkg-{i}") # All hashes should be identical (deduplication) check storedHashes.toHashSet.len == 1 # Object should exist check casManager.objectExists(storedHashes[0]) # Reference count should be high let refCount = casManager.getRefCount(storedHashes[0]) check refCount >= 5 test "Different content produces different hashes": let data1 = @[byte(1), byte(2), byte(3)] let data2 = @[byte(4), byte(5), byte(6)] let data3 = @[byte(7), byte(8), byte(9)] let hash1 = casManager.storeObject(data1).get().hash let hash2 = casManager.storeObject(data2).get().hash let hash3 = casManager.storeObject(data3).get().hash check hash1 != hash2 check hash2 != hash3 check hash1 != hash3 suite "Integration - Atomic Installations": var testDir: string var casManager: CasManager setup: testDir = getTempDir() / "nip_atomic_test_" & $epochTime().int createDir(testDir) casManager = initCasManager(testDir / "cas", testDir / "cas" / "system") teardown: removeDir(testDir) test "Package files are stored atomically": # Simulate installing multiple files as a package var packageHashes: seq[string] = @[] for i in 1..5: let data = @[byte(i), byte(i+1), byte(i+2)] let result = casManager.storeObject(data) check result.isOk packageHashes.add(result.get().hash) # Add reference as part of atomic transaction discard casManager.addReference(result.get().hash, NPK, "test-pkg") # Verify all objects exist for hash in packageHashes: check casManager.objectExists(hash) test "Complete installation is fully committed": var packageHashes: seq[string] = @[] # Install a "package" with 3 files for i in 1..3: let data = @[byte(100 + i)] let result = casManager.storeObject(data) check result.isOk packageHashes.add(result.get().hash) discard casManager.addReference(result.get().hash, NIP, "complete-pkg") # Verify all exist for hash in packageHashes: check casManager.objectExists(hash) # Run GC - should not remove anything (still referenced) discard casManager.garbageCollect() # All still exist for hash in packageHashes: check casManager.objectExists(hash) suite "Integration - Garbage Collection Safety": var testDir: string var casManager: CasManager setup: testDir = getTempDir() / "nip_gc_safety_test_" & $epochTime().int createDir(testDir) casManager = initCasManager(testDir / "cas", testDir / "cas" / "system") teardown: removeDir(testDir) test "Pinned objects survive garbage collection": let criticalData = @[byte(255), byte(254), byte(253)] let result = casManager.storeObject(criticalData) check result.isOk let hash = result.get().hash # Pin the object discard casManager.pinObject(hash, "system-critical") # Run GC discard casManager.garbageCollect() # Pinned object should still exist check casManager.objectExists(hash) test "Referenced objects survive GC": let data = @[byte(42)] let result = casManager.storeObject(data) check result.isOk let hash = result.get().hash # Add reference discard casManager.addReference(hash, NPK, "my-package") # Run GC discard casManager.garbageCollect() # Object should still exist check casManager.objectExists(hash) test "Cross-format references all protect object": let sharedData = @[byte(50), byte(51)] let result = casManager.storeObject(sharedData) check result.isOk let hash = result.get().hash # Add references from multiple formats discard casManager.addReference(hash, NPK, "pkg1") discard casManager.addReference(hash, NIP, "app1") discard casManager.addReference(hash, NEXTER, "container1") # Run GC discard casManager.garbageCollect() # Object should still exist (multiple references) check casManager.objectExists(hash) suite "Integration - Migration Workflows": var testDir: string var migrationManager: MigrationManager setup: testDir = getTempDir() / "nip_migration_test_" & $epochTime().int createDir(testDir) migrationManager = initMigrationManager(testDir / "cas", dryRun = false, verbose = false) teardown: removeDir(testDir) test "Legacy NIP to new format migration preserves data": # Create a mock legacy NIP structure let legacyDir = testDir / "legacy-app" createDir(legacyDir) createDir(legacyDir / "bin") createDir(legacyDir / "lib") writeFile(legacyDir / "manifest.kdl", "name \"test-app\"\nversion \"1.0.0\"") writeFile(legacyDir / "bin" / "app", "#!/bin/sh\necho hello") writeFile(legacyDir / "lib" / "libfoo.so", "ELF binary data simulation") # Migrate let result = migrationManager.migrateLegacyNip(legacyDir) check result.source == OldNip check result.packageName == "legacy-app" check result.success == true check result.casHashes.len > 0 test "Format conversion NPK to NIP": let result = migrationManager.convertNpkToNip("/mock/package.npk") check result.success == true # Placeholder succeeds check result.warnings.len > 0 # Has warnings about placeholder test "Format conversion NIP to NEXTER": let result = migrationManager.convertNipToNexter("/mock/app.nip") check result.success == true # Placeholder succeeds check result.warnings.len > 0 test "Migration verification catches missing objects": let mockResult = MigrationResult( success: true, source: OldNip, packageName: "missing-objects", casHashes: @["xxh3-nonexistent123456"], errors: @[] ) # Verification should fail for missing objects check not migrationManager.verifyMigration(mockResult) test "Migration report generation": # Create two mock results let results = @[ MigrationResult(success: true, source: OldNip, packageName: "app1"), MigrationResult(success: true, source: Flatpak, packageName: "app2"), MigrationResult(success: false, source: AppImage, packageName: "app3", errors: @["Test error"]) ] let report = generateMigrationReport(results) check report.len > 0 check report.contains("app1") check report.contains("app2") check report.contains("app3") suite "Integration - Error Recovery": var testDir: string var casManager: CasManager setup: testDir = getTempDir() / "nip_error_test_" & $epochTime().int createDir(testDir) casManager = initCasManager(testDir / "cas", testDir / "cas" / "system") teardown: removeDir(testDir) test "Retrieve nonexistent object returns error": let result = casManager.retrieveObject("xxh3-doesnotexist000000") check result.isErr test "Store and retrieve cycle works correctly": let originalData = @[byte(1), byte(2), byte(3), byte(4), byte(5)] # Store let storeResult = casManager.storeObject(originalData) check storeResult.isOk let hash = storeResult.get().hash # Retrieve let retrieveResult = casManager.retrieveObject(hash) check retrieveResult.isOk let retrievedData = retrieveResult.get() check retrievedData == originalData test "Error factory provides actionable suggestions": let err = checksumMismatchError("pkg.nip", "xxh3-expected", "xxh3-actual") check err.code == ChecksumMismatch check err.suggestions.len > 0 check err.msg.len > 0 test "Recovery strategies are appropriate for error types": # Network errors should suggest retry let networkErr = networkError("timeout", "https://example.com") check suggestRecovery(networkErr) == Retry # Permission errors should suggest manual intervention let permErr = permissionDeniedError("/root/file", "write") check suggestRecovery(permErr) == Manual # Checksum errors should abort let checksumErr = checksumMismatchError("file", "a", "b") check suggestRecovery(checksumErr) == Abort suite "Integration - End-to-End Workflows": var testDir: string var casManager: CasManager setup: testDir = getTempDir() / "nip_e2e_test_" & $epochTime().int createDir(testDir) casManager = initCasManager(testDir / "cas", testDir / "cas" / "system") teardown: removeDir(testDir) test "Multiple package versions coexist": # Install v1.0 let v1Data = @[byte(1), byte(0)] let v1Result = casManager.storeObject(v1Data) check v1Result.isOk let v1Hash = v1Result.get().hash discard casManager.addReference(v1Hash, NPK, "myapp-1.0") # Install v2.0 (different content) let v2Data = @[byte(2), byte(0)] let v2Result = casManager.storeObject(v2Data) check v2Result.isOk let v2Hash = v2Result.get().hash discard casManager.addReference(v2Hash, NPK, "myapp-2.0") # Both versions exist check casManager.objectExists(v1Hash) check casManager.objectExists(v2Hash) check v1Hash != v2Hash test "Concurrent format usage: NPK + NIP + NEXTER": # System library (NPK) let libData = @[byte(100)] let libResult = casManager.storeObject(libData) check libResult.isOk discard casManager.addReference(libResult.get().hash, NPK, "libsystem") # Application using library (NIP) let appData = @[byte(200)] let appResult = casManager.storeObject(appData) check appResult.isOk discard casManager.addReference(appResult.get().hash, NIP, "myapp") # Dev container (NEXTER) let containerData = @[byte(255)] let containerResult = casManager.storeObject(containerData) check containerResult.isOk discard casManager.addReference(containerResult.get().hash, NEXTER, "devenv") # All three exist check casManager.objectExists(libResult.get().hash) check casManager.objectExists(appResult.get().hash) check casManager.objectExists(containerResult.get().hash) test "Hash consistency across store cycles": let data = @[byte(1), byte(2), byte(3)] # Store same data multiple times let hash1 = casManager.storeObject(data).get().hash let hash2 = casManager.storeObject(data).get().hash let hash3 = casManager.storeObject(data).get().hash # All hashes are identical check hash1 == hash2 check hash2 == hash3 when isMainModule: echo "Integration Tests Complete"