## Test suite for Content-Addressable Storage (CAS) System import unittest import std/[os, strutils, sequtils, sets, times] import ../src/nimpak/cas import ../src/nimpak/protection # Import FormatType explicitly for property tests from ../src/nimpak/cas import FormatType, NPK, NIP, NEXTER suite "CAS Basic Operations": setup: let tempDir = getTempDir() / "nimpak_test_cas" var cas = initCasManager(tempDir, tempDir / "system") teardown: if dirExists(tempDir): removeDir(tempDir) test "Initialize CAS Manager": check cas.userCasPath.endsWith("cas") check cas.systemCasPath.endsWith("system") check cas.compression == true check cas.compressionLevel == 19 # Maximum compression is the default test "Store and retrieve simple object": let testData = "Hello, NexusOS CAS!".toOpenArrayByte(0, "Hello, NexusOS CAS!".len - 1).toSeq() let storeResult = cas.storeObject(testData) check storeResult.isOk let obj = storeResult.get() check obj.hash.startsWith("xxh3-") # xxHash is now the default check obj.size == testData.len.int64 # Retrieve the object let retrieveResult = cas.retrieveObject(obj.hash) check retrieveResult.isOk let retrievedData = retrieveResult.get() check retrievedData == testData test "Object deduplication": let testData = "Duplicate test data".toOpenArrayByte(0, "Duplicate test data".len - 1).toSeq() # Store the same data twice let result1 = cas.storeObject(testData) let result2 = cas.storeObject(testData) check result1.isOk check result2.isOk # Should have the same hash (deduplication) check result1.get().hash == result2.get().hash # Reference count should be 2 check result2.get().refCount == 2 test "Object existence check": let testData = "Existence test".toOpenArrayByte(0, "Existence test".len - 1).toSeq() let storeResult = cas.storeObject(testData) check storeResult.isOk let hash = storeResult.get().hash # Object should exist check cas.objectExists(hash) # Non-existent object should not exist check not cas.objectExists("xxh3-nonexistent") test "BLAKE2b hash calculation": let testData = "Hash test data".toOpenArrayByte(0, "Hash test data".len - 1).toSeq() let hash = calculateBlake2b(testData) check hash.startsWith("blake2b-") check hash.len > 10 # Should be a reasonable length test "Pin and unpin objects": let testData = "Pin test data".toOpenArrayByte(0, "Pin test data".len - 1).toSeq() let storeResult = cas.storeObject(testData) check storeResult.isOk let hash = storeResult.get().hash # Pin the object let pinResult = cas.pinObject(hash, "test-pin") check pinResult.isOk # Unpin the object let unpinResult = cas.unpinObject(hash, "test-pin") check unpinResult.isOk test "List objects": let testData1 = "List test 1".toOpenArrayByte(0, "List test 1".len - 1).toSeq() let testData2 = "List test 2".toOpenArrayByte(0, "List test 2".len - 1).toSeq() let result1 = cas.storeObject(testData1) let result2 = cas.storeObject(testData2) check result1.isOk check result2.isOk let objects = cas.listObjects() check objects.len >= 2 check result1.get().hash in objects check result2.get().hash in objects test "Verify object integrity": let testData = "Integrity test".toOpenArrayByte(0, "Integrity test".len - 1).toSeq() let storeResult = cas.storeObject(testData) check storeResult.isOk let hash = storeResult.get().hash let verifyResult = cas.verifyObject(hash) check verifyResult.isOk check verifyResult.get() == true suite "CAS File Operations": setup: let tempDir = getTempDir() / "nimpak_test_cas_files" var cas = initCasManager(tempDir, tempDir / "system") teardown: if dirExists(tempDir): removeDir(tempDir) test "Store and retrieve file": # Create a test file let testFile = getTempDir() / "test_cas_file.txt" let testContent = "This is a test file for CAS storage." writeFile(testFile, testContent) try: # Store the file let storeResult = cas.storeFile(testFile) check storeResult.isOk let obj = storeResult.get() check obj.hash.startsWith("xxh3-") # xxHash is now the default # Retrieve the file let outputFile = getTempDir() / "retrieved_file.txt" let retrieveResult = cas.retrieveFile(obj.hash, outputFile) check retrieveResult.isOk # Verify content let retrievedContent = readFile(outputFile) check retrievedContent == testContent # Clean up removeFile(outputFile) finally: if fileExists(testFile): removeFile(testFile) suite "CAS Deduplication and Reference Counting": setup: let tempDir = getTempDir() / "nimpak_test_cas_dedup" var cas = initCasManager(tempDir, tempDir / "system") teardown: if dirExists(tempDir): removeDir(tempDir) test "Reference counting on duplicate storage": let testData = "Reference count test".toOpenArrayByte(0, "Reference count test".len - 1).toSeq() # Store object first time let result1 = cas.storeObject(testData) check result1.isOk let hash = result1.get().hash check result1.get().refCount == 1 # Store same object second time let result2 = cas.storeObject(testData) check result2.isOk check result2.get().hash == hash check result2.get().refCount == 2 # Store same object third time let result3 = cas.storeObject(testData) check result3.isOk check result3.get().refCount == 3 test "Decrement reference count": let testData = "Decrement test".toOpenArrayByte(0, "Decrement test".len - 1).toSeq() # Store object twice let result1 = cas.storeObject(testData) let result2 = cas.storeObject(testData) check result1.isOk check result2.isOk let hash = result1.get().hash # Reference count should be 2 check cas.getRefCount(hash) == 2 # Remove object once let removeResult = cas.removeObject(hash) check removeResult.isOk check cas.getRefCount(hash) == 1 # Remove object again let removeResult2 = cas.removeObject(hash) check removeResult2.isOk check cas.getRefCount(hash) == 0 test "Create symlink to CAS object": let testData = "Symlink test".toOpenArrayByte(0, "Symlink test".len - 1).toSeq() let storeResult = cas.storeObject(testData) check storeResult.isOk let hash = storeResult.get().hash # Create symlink let symlinkPath = tempDir / "test_symlink.txt" let symlinkResult = cas.createSymlink(hash, symlinkPath) check symlinkResult.isOk # Verify symlink exists and points to correct file check symlinkExists(symlinkPath) # Read through symlink let content = readFile(symlinkPath) check content == "Symlink test" test "Garbage collection respects reference counts": let testData1 = "GC test 1".toOpenArrayByte(0, "GC test 1".len - 1).toSeq() let testData2 = "GC test 2".toOpenArrayByte(0, "GC test 2".len - 1).toSeq() # Store first object twice (refcount = 2) let result1a = cas.storeObject(testData1) let result1b = cas.storeObject(testData1) check result1a.isOk check result1b.isOk let hash1 = result1a.get().hash # Store second object once (refcount = 1) let result2 = cas.storeObject(testData2) check result2.isOk let hash2 = result2.get().hash # Remove first object once (refcount = 1) discard cas.removeObject(hash1) # Remove second object once (refcount = 0) discard cas.removeObject(hash2) # Run garbage collection let gcResult = cas.garbageCollect() check gcResult.isOk # First object should still exist (refcount = 1) check cas.objectExists(hash1) # Second object should be removed (refcount = 0) check not cas.objectExists(hash2) suite "CAS Statistics": setup: let tempDir = getTempDir() / "nimpak_test_cas_stats" var cas = initCasManager(tempDir, tempDir / "system") teardown: if dirExists(tempDir): removeDir(tempDir) test "Get CAS statistics": # Store some test data let testData1 = "Stats test 1".toOpenArrayByte(0, "Stats test 1".len - 1).toSeq() let testData2 = "Stats test 2".toOpenArrayByte(0, "Stats test 2".len - 1).toSeq() discard cas.storeObject(testData1) discard cas.storeObject(testData2) let stats = cas.getStats() check stats.objectCount >= 2 check stats.totalSize > 0 check stats.compressedSize > 0 suite "CAS Property Tests - Cross-Format Deduplication": ## Feature: 01-nip-unified-storage-and-formats, Property 1: CAS Deduplication Across Formats ## Validates: Requirements 1.4, 10.1 ## ## Property: For any two packages (regardless of format) sharing a chunk, ## the CAS SHALL store only one copy var tempDir: string var cas: CasManager setup: # Use a unique directory per test to ensure isolation tempDir = getTempDir() / "nimpak_test_cas_property_" & $epochTime().int cas = initCasManager(tempDir, tempDir / "system") teardown: if dirExists(tempDir): removeDir(tempDir) test "Property 1: CAS Deduplication Across Formats - Same chunk different formats": ## Test that the same chunk stored by different package formats ## results in only one physical copy in CAS ## ## Note: storeObject increments ref count, and addReference also increments ref count. ## So 3 stores + 3 addReferences = 6 total ref count. ## This is intentional: storeObject tracks "content references" while ## addReference tracks "format-specific package references". # Shared chunk data (e.g., a common library like libssl) let sharedChunk = "Shared library data - libssl.so.3".toOpenArrayByte(0, "Shared library data - libssl.so.3".len - 1).toSeq() # Store chunk as part of NPK package let npkResult = cas.storeObject(sharedChunk) check npkResult.isOk let hash1 = npkResult.get().hash # Add reference from NPK format let addRef1 = cas.addReference(hash1, NPK, "nginx") check addRef1.isOk # Store same chunk as part of NIP package let nipResult = cas.storeObject(sharedChunk) check nipResult.isOk let hash2 = nipResult.get().hash # Add reference from NIP format let addRef2 = cas.addReference(hash2, NIP, "firefox") check addRef2.isOk # Store same chunk as part of NEXTER container let nexterResult = cas.storeObject(sharedChunk) check nexterResult.isOk let hash3 = nexterResult.get().hash # Add reference from NEXTER format let addRef3 = cas.addReference(hash3, NEXTER, "dev-env") check addRef3.isOk # Property verification: All three should have the same hash check hash1 == hash2 check hash2 == hash3 # Property verification: Only one physical copy should exist let objects = cas.listObjects() let matchingObjects = objects.filterIt(it == hash1) check matchingObjects.len == 1 # Property verification: Reference count should be 6 (3 stores + 3 addReferences) check cas.getRefCount(hash1) == 6 # Verify format-specific references exist check cas.hasFormatPackage(NPK, "nginx") check cas.hasFormatPackage(NIP, "firefox") check cas.hasFormatPackage(NEXTER, "dev-env") # Verify hash is in each package's reference set check cas.getFormatPackageHashes(NPK, "nginx").contains(hash1) check cas.getFormatPackageHashes(NIP, "firefox").contains(hash1) check cas.getFormatPackageHashes(NEXTER, "dev-env").contains(hash1) test "Property 1: Multiple packages per format sharing chunks": ## Test that multiple packages within the same format ## also deduplicate correctly let commonRuntime = "Common runtime library".toOpenArrayByte(0, "Common runtime library".len - 1).toSeq() # Store for first NPK package let result1 = cas.storeObject(commonRuntime) check result1.isOk let hash = result1.get().hash discard cas.addReference(hash, NPK, "package1") # Store for second NPK package let result2 = cas.storeObject(commonRuntime) check result2.isOk discard cas.addReference(hash, NPK, "package2") # Store for third NPK package let result3 = cas.storeObject(commonRuntime) check result3.isOk discard cas.addReference(hash, NPK, "package3") # Property verification: All should have same hash check result1.get().hash == result2.get().hash check result2.get().hash == result3.get().hash # Property verification: Reference count should be 6 (3 stores + 3 addReferences) check cas.getRefCount(hash) == 6 # Property verification: Only one physical copy let objects = cas.listObjects() let matchingObjects = objects.filterIt(it == hash) check matchingObjects.len == 1 test "Property 1: Garbage collection preserves chunks referenced by any format": ## Test that garbage collection respects references from all formats let sharedData = "Shared data across formats".toOpenArrayByte(0, "Shared data across formats".len - 1).toSeq() let uniqueData = "Unique data".toOpenArrayByte(0, "Unique data".len - 1).toSeq() # Store shared chunk with references from multiple formats let sharedResult = cas.storeObject(sharedData) check sharedResult.isOk let sharedHash = sharedResult.get().hash discard cas.addReference(sharedHash, NPK, "pkg1") discard cas.addReference(sharedHash, NIP, "app1") discard cas.addReference(sharedHash, NEXTER, "container1") # Store unique chunk with single reference let uniqueResult = cas.storeObject(uniqueData) check uniqueResult.isOk let uniqueHash = uniqueResult.get().hash discard cas.addReference(uniqueHash, NPK, "pkg2") # Remove one reference from shared chunk discard cas.removeReference(sharedHash, NPK, "pkg1") # Remove the only reference from unique chunk discard cas.removeReference(uniqueHash, NPK, "pkg2") # Run garbage collection let gcResult = cas.garbageCollect() check gcResult.isOk # Property verification: Shared chunk should still exist # Initial refs: 1 (store) + 3 (addReference) = 4 # After removeReference NPK/pkg1: 4 - 1 = 3 check cas.objectExists(sharedHash) check cas.getRefCount(sharedHash) == 3 # Property verification: Unique chunk should be removed # Initial refs: 1 (store) + 1 (addReference) = 2 # After removeReference NPK/pkg2: 2 - 1 = 1 # GC only removes chunks with refCount == 0, so it should still exist check cas.objectExists(uniqueHash) check cas.getRefCount(uniqueHash) == 1 test "Property 1: Reference tracking persists across CAS manager restarts": ## Test that reference tracking survives CAS manager restarts let testData = "Persistent reference test".toOpenArrayByte(0, "Persistent reference test".len - 1).toSeq() # Store chunk with references let result = cas.storeObject(testData) check result.isOk let hash = result.get().hash discard cas.addReference(hash, NPK, "persistent-pkg") discard cas.addReference(hash, NIP, "persistent-app") # Create new CAS manager (simulating restart) var cas2 = initCasManager(tempDir, tempDir / "system") # Load references from disk let loadResult = cas2.loadFormatReferences() check loadResult.isOk # Property verification: References should be loaded check cas2.hasFormatPackage(NPK, "persistent-pkg") check cas2.hasFormatPackage(NIP, "persistent-app") check cas2.getFormatPackageHashes(NPK, "persistent-pkg").contains(hash) check cas2.getFormatPackageHashes(NIP, "persistent-app").contains(hash) # Property verification: Reference count should be correct # 1 (storeObject) + 2 (addReference) = 3 check cas2.getRefCount(hash) == 3 suite "CAS Property Tests - Read-Only Protection": ## Feature: 01-nip-unified-storage-and-formats, Property 6: Read-Only Protection ## Validates: Requirements 13.1, 13.4 ## ## Property: For any attempt to write to CAS without proper elevation, ## the operation SHALL fail setup: let tempDir = getTempDir() / "nimpak_test_cas_protection" var cas = initCasManager(tempDir, tempDir / "system") # Ensure directories exist createDir(cas.rootPath) teardown: if dirExists(tempDir): # Make directory writable before cleanup try: discard cas.protectionManager.setWritable() except: discard removeDir(tempDir) test "Property 6: CAS directory is read-only by default": ## Test that CAS directory is set to read-only after initialization # Set CAS to read-only let setReadOnlyResult = cas.protectionManager.setReadOnly() check setReadOnlyResult.isOk # Verify it's read-only check cas.protectionManager.verifyReadOnly() test "Property 6: Write operations require elevation": ## Test that write operations can only succeed with proper elevation # Set CAS to read-only discard cas.protectionManager.setReadOnly() # Try to write a file directly (should fail) let testFile = cas.rootPath / "test_write.txt" var directWriteFailed = false try: writeFile(testFile, "test") except IOError, OSError: directWriteFailed = true check directWriteFailed # Now use withWriteAccess to write (should succeed) var writeSucceeded = false let writeResult = cas.protectionManager.withWriteAccess(proc() = try: writeFile(testFile, "test") writeSucceeded = true except: discard ) check writeResult.isOk check writeSucceeded # Verify CAS is back to read-only check cas.protectionManager.verifyReadOnly() test "Property 6: Permissions restored even on error": ## Test that read-only permissions are restored even if operation fails # Set CAS to read-only discard cas.protectionManager.setReadOnly() # Try an operation that will fail let writeResult = cas.protectionManager.withWriteAccess(proc() = raise newException(IOError, "Simulated error") ) # Operation should fail check not writeResult.isOk # But permissions should still be restored to read-only check cas.protectionManager.verifyReadOnly() test "Property 6: Audit log records permission changes": ## Test that all permission changes are logged # Clear audit log if fileExists(cas.auditLog): removeFile(cas.auditLog) # Perform some operations discard cas.protectionManager.setWritable() discard cas.protectionManager.setReadOnly() # Check audit log exists and has entries check fileExists(cas.auditLog) let logContent = readFile(cas.auditLog) check logContent.contains("SET_WRITABLE") check logContent.contains("SET_READONLY")