## Property-Based Test: NIP Manifest Roundtrip ## ## **Feature:** 01-nip-unified-storage-and-formats ## **Property 3:** Manifest Roundtrip ## **Validates:** Requirements 6.4 ## ## **Property Statement:** ## For any NIP manifest, parsing and regenerating SHALL produce semantically equivalent KDL ## ## **Test Strategy:** ## 1. Generate random NIP manifests with valid data ## 2. Convert manifest to KDL string ## 3. Parse KDL string back to manifest ## 4. Verify semantic equivalence (all fields match) ## 5. Verify determinism (same manifest = same KDL) import std/[unittest, times, options, random, strutils] import nip/nip_manifest import nip/manifest_parser # ============================================================================ # Test Generators # ============================================================================ proc genSemanticVersion(): SemanticVersion = ## Generate random semantic version SemanticVersion( major: rand(0..10), minor: rand(0..20), patch: rand(0..50) ) proc genAppInfo(): AppInfo = ## Generate random application metadata AppInfo( description: "Test application " & $rand(1000), homepage: some("https://example.com/" & $rand(1000)), license: ["MIT", "GPL-3.0", "Apache-2.0", "BSD-3-Clause"][rand(3)], author: some("Test Author " & $rand(100)), maintainer: some("Test Maintainer " & $rand(100)), tags: @["test", "example", "app" & $rand(10)], category: some(["Graphics", "Network", "Development", "Utility"][rand(3)]) ) proc genProvenanceInfo(): ProvenanceInfo = ## Generate random provenance information ProvenanceInfo( source: "https://github.com/test/repo" & $rand(1000), sourceHash: "xxh3-" & $rand(high(int)), upstream: some("https://upstream.example.com/" & $rand(1000)), buildTimestamp: now(), builder: some("builder-" & $rand(100)) ) proc genBuildConfiguration(): BuildConfiguration = ## Generate random build configuration BuildConfiguration( configureFlags: @["--enable-feature" & $rand(10), "--with-lib" & $rand(5)], compilerFlags: @["-O2", "-march=native", "-flto"], compilerVersion: "gcc-" & $rand(10..13) & ".0.0", targetArchitecture: ["x86_64", "aarch64", "riscv64"][rand(2)], libc: ["musl", "glibc"][rand(1)], allocator: ["jemalloc", "tcmalloc", "default"][rand(2)], buildSystem: ["cmake", "meson", "autotools"][rand(2)] ) proc genChunkReference(): ChunkReference = ## Generate random CAS chunk reference ChunkReference( hash: "xxh3-" & $rand(high(int)), size: rand(1024..1048576).int64, chunkType: [Binary, Library, Runtime, Config, Data][rand(4)], path: "bin/app" & $rand(10) ) proc genDesktopFileSpec(): DesktopFileSpec = ## Generate random desktop file specification DesktopFileSpec( name: "Test App " & $rand(100), genericName: some("Generic App " & $rand(100)), comment: some("A test application"), exec: "/usr/bin/testapp" & $rand(10), icon: "testapp" & $rand(10), terminal: rand(1) == 0, categories: @["Graphics", "Utility"], keywords: @["test", "example", "app"] ) proc genIconSpec(): IconSpec = ## Generate random icon specification IconSpec( size: [16, 32, 48, 64, 128, 256][rand(5)], path: "icons/icon" & $rand(10) & ".png", format: ["png", "svg"][rand(1)] ) proc genDesktopMetadata(): DesktopMetadata = ## Generate random desktop metadata DesktopMetadata( desktopFile: genDesktopFileSpec(), icons: @[genIconSpec(), genIconSpec()], mimeTypes: @["text/plain", "application/json"], appId: "org.example.testapp" & $rand(100) ) proc genFilesystemAccess(): FilesystemAccess = ## Generate random filesystem access permission FilesystemAccess( path: ["/home", "/tmp", "/var/cache"][rand(2)], mode: [ReadOnly, ReadWrite, Create][rand(2)] ) proc genDBusAccess(): DBusAccess = ## Generate random D-Bus access permissions DBusAccess( session: @["org.freedesktop.Notifications", "org.kde.StatusNotifierWatcher"], system: @["org.freedesktop.NetworkManager"], own: @["org.example.TestApp" & $rand(100)] ) proc genMount(): Mount = ## Generate random mount specification Mount( source: "/host/path" & $rand(10), target: "/app/path" & $rand(10), mountType: [Bind, Tmpfs, Devtmpfs][rand(2)], readOnly: rand(1) == 0 ) proc genNamespaceConfig(): NamespaceConfig = ## Generate random namespace configuration NamespaceConfig( namespaceType: ["user", "strict", "none"][rand(2)], permissions: Permissions( network: rand(1) == 0, gpu: rand(1) == 0, audio: rand(1) == 0, camera: rand(1) == 0, microphone: rand(1) == 0, filesystem: @[genFilesystemAccess(), genFilesystemAccess()], dbus: genDBusAccess() ), mounts: @[genMount(), genMount()] ) proc genSignatureInfo(): SignatureInfo = ## Generate random signature information SignatureInfo( algorithm: "ed25519", keyId: "key-" & $rand(1000), signature: "sig-" & $rand(high(int)) ) proc genNIPManifest(): NIPManifest = ## Generate random NIP manifest NIPManifest( name: "testapp" & $rand(1000), version: genSemanticVersion(), buildDate: now(), metadata: genAppInfo(), provenance: genProvenanceInfo(), buildConfig: genBuildConfiguration(), casChunks: @[genChunkReference(), genChunkReference(), genChunkReference()], desktop: genDesktopMetadata(), namespace: genNamespaceConfig(), buildHash: "xxh3-" & $rand(high(int)), signature: genSignatureInfo() ) # ============================================================================ # Semantic Equivalence Checks # ============================================================================ proc checkTimestampsClose(a, b: DateTime, toleranceSeconds: int = 2): bool = ## Check if two timestamps are within tolerance (for parsing precision) let diff = abs((a - b).inSeconds) return diff <= toleranceSeconds proc checkAppInfoEqual(a, b: AppInfo): bool = ## Check if two AppInfo objects are semantically equivalent result = a.description == b.description and a.homepage == b.homepage and a.license == b.license and a.author == b.author and a.maintainer == b.maintainer and a.tags == b.tags and a.category == b.category proc checkProvenanceEqual(a, b: ProvenanceInfo): bool = ## Check if two ProvenanceInfo objects are semantically equivalent result = a.source == b.source and a.sourceHash == b.sourceHash and a.upstream == b.upstream and a.builder == b.builder and checkTimestampsClose(a.buildTimestamp, b.buildTimestamp) proc checkBuildConfigEqual(a, b: BuildConfiguration): bool = ## Check if two BuildConfiguration objects are semantically equivalent result = a.configureFlags == b.configureFlags and a.compilerFlags == b.compilerFlags and a.compilerVersion == b.compilerVersion and a.targetArchitecture == b.targetArchitecture and a.libc == b.libc and a.allocator == b.allocator and a.buildSystem == b.buildSystem proc checkChunkRefEqual(a, b: ChunkReference): bool = ## Check if two ChunkReference objects are semantically equivalent result = a.hash == b.hash and a.size == b.size and a.chunkType == b.chunkType and a.path == b.path proc checkDesktopFileEqual(a, b: DesktopFileSpec): bool = ## Check if two DesktopFileSpec objects are semantically equivalent result = a.name == b.name and a.genericName == b.genericName and a.comment == b.comment and a.exec == b.exec and a.icon == b.icon and a.terminal == b.terminal and a.categories == b.categories and a.keywords == b.keywords proc checkIconEqual(a, b: IconSpec): bool = ## Check if two IconSpec objects are semantically equivalent result = a.size == b.size and a.path == b.path and a.format == b.format proc checkDesktopMetadataEqual(a, b: DesktopMetadata, verbose: bool = false): bool = ## Check if two DesktopMetadata objects are semantically equivalent if not checkDesktopFileEqual(a.desktopFile, b.desktopFile): if verbose: echo " Desktop file mismatch" return false if a.icons.len != b.icons.len: if verbose: echo " Icons length mismatch: ", a.icons.len, " vs ", b.icons.len return false for i in 0.. 0: echo "\nFirst 5 errors:" for i in 0.. 0 check "name cannot be empty" in issues1[0].toLowerAscii() # Test invalid hash format manifest = genNIPManifest() manifest.buildHash = "invalid-hash" let issues2 = validateNIPManifest(manifest) check issues2.len > 0 check "xxh3" in issues2[0].toLowerAscii() # Test invalid namespace type manifest = genNIPManifest() manifest.namespace.namespaceType = "invalid" let issues3 = validateNIPManifest(manifest) check issues3.len > 0 check "namespace type" in issues3[0].toLowerAscii() # Test empty app_id manifest = genNIPManifest() manifest.desktop.appId = "" let issues4 = validateNIPManifest(manifest) check issues4.len > 0 check "app_id" in issues4[0].toLowerAscii() when isMainModule: # Run tests randomize() echo "Running NIP Manifest Roundtrip Property Tests..." echo "Testing Property 3: Manifest Roundtrip" echo "Validates: Requirements 6.4" echo ""