Tests/Infra-LivingDoc.Tests.ps1
|
BeforeAll { $ModuleRoot = Split-Path -Parent $PSScriptRoot $ModuleName = 'Infra-LivingDoc' $ManifestPath = Join-Path $ModuleRoot "$ModuleName.psd1" # Remove module if already loaded Get-Module $ModuleName -ErrorAction SilentlyContinue | Remove-Module -Force # Import the module Import-Module $ManifestPath -Force -ErrorAction Stop } AfterAll { Get-Module 'Infra-LivingDoc' -ErrorAction SilentlyContinue | Remove-Module -Force } Describe 'Module Loading' { It 'Should import the module without errors' { $module = Get-Module 'Infra-LivingDoc' $module | Should -Not -BeNullOrEmpty } It 'Should export exactly 5 public functions' { $module = Get-Module 'Infra-LivingDoc' $module.ExportedFunctions.Count | Should -Be 5 } It 'Should export Import-Documentation' { Get-Command 'Import-Documentation' -Module 'Infra-LivingDoc' | Should -Not -BeNullOrEmpty } It 'Should export Extract-EnvironmentFacts' { Get-Command 'Extract-EnvironmentFacts' -Module 'Infra-LivingDoc' | Should -Not -BeNullOrEmpty } It 'Should export Test-EnvironmentFacts' { Get-Command 'Test-EnvironmentFacts' -Module 'Infra-LivingDoc' | Should -Not -BeNullOrEmpty } It 'Should export Get-DocumentationDrift' { Get-Command 'Get-DocumentationDrift' -Module 'Infra-LivingDoc' | Should -Not -BeNullOrEmpty } It 'Should export Update-Documentation' { Get-Command 'Update-Documentation' -Module 'Infra-LivingDoc' | Should -Not -BeNullOrEmpty } It 'Should NOT export private functions' { $module = Get-Module 'Infra-LivingDoc' $module.ExportedFunctions.Keys | Should -Not -Contain 'Invoke-AICompletion' $module.ExportedFunctions.Keys | Should -Not -Contain 'Read-WordDocument' $module.ExportedFunctions.Keys | Should -Not -Contain 'Read-ExcelDocument' $module.ExportedFunctions.Keys | Should -Not -Contain 'Test-SingleFact' $module.ExportedFunctions.Keys | Should -Not -Contain 'New-HtmlDashboard' } It 'Should have a Templates directory' { $templatesPath = Join-Path (Split-Path -Parent $PSScriptRoot) 'Templates' Test-Path $templatesPath | Should -BeTrue } It 'Should have extraction-prompt.txt template' { $templatePath = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Templates') 'extraction-prompt.txt' Test-Path $templatePath | Should -BeTrue } It 'Should have update-prompt.txt template' { $templatePath = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Templates') 'update-prompt.txt' Test-Path $templatePath | Should -BeTrue } } Describe 'Module Manifest Validation' { BeforeAll { $manifestPath = Join-Path (Split-Path -Parent $PSScriptRoot) 'Infra-LivingDoc.psd1' $manifest = Test-ModuleManifest -Path $manifestPath -ErrorAction Stop } It 'Should have the correct GUID' { $manifest.GUID | Should -Be 'b8c9d0e1-5f46-4bc2-a3d4-9f0a1b2c3d45' } It 'Should require PowerShell 5.1' { $manifest.PowerShellVersion | Should -Be '5.1' } It 'Should have the correct author' { $manifest.Author | Should -BeLike '*Larry Roberts*' } It 'Should have required tags' { $tags = $manifest.PrivateData.PSData.Tags $tags | Should -Contain 'Documentation' $tags | Should -Contain 'LivingDoc' $tags | Should -Contain 'AI' $tags | Should -Contain 'Drift' $tags | Should -Contain 'Infrastructure' } It 'Should have a ProjectUri' { $manifest.PrivateData.PSData.ProjectUri | Should -Not -BeNullOrEmpty } It 'Should have a LicenseUri' { $manifest.PrivateData.PSData.LicenseUri | Should -Not -BeNullOrEmpty } } Describe 'Parameter Validation' { Context 'Import-Documentation' { It 'Should have mandatory Path parameter' { $cmd = Get-Command 'Import-Documentation' $param = $cmd.Parameters['Path'] $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.Mandatory } | Should -Not -BeNullOrEmpty } It 'Should have ValidateSet on FileType parameter' { $cmd = Get-Command 'Import-Documentation' $param = $cmd.Parameters['FileType'] $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } $validateSet | Should -Not -BeNullOrEmpty $validateSet.ValidValues | Should -Contain 'All' $validateSet.ValidValues | Should -Contain 'Word' $validateSet.ValidValues | Should -Contain 'Excel' $validateSet.ValidValues | Should -Contain 'Markdown' $validateSet.ValidValues | Should -Contain 'Text' $validateSet.ValidValues | Should -Contain 'CSV' } } Context 'Extract-EnvironmentFacts' { It 'Should have mandatory DocumentText parameter' { $cmd = Get-Command 'Extract-EnvironmentFacts' $param = $cmd.Parameters['DocumentText'] $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.Mandatory } | Should -Not -BeNullOrEmpty } It 'Should have ValidateSet on Provider parameter' { $cmd = Get-Command 'Extract-EnvironmentFacts' $param = $cmd.Parameters['Provider'] $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } $validateSet | Should -Not -BeNullOrEmpty $validateSet.ValidValues | Should -Contain 'Anthropic' $validateSet.ValidValues | Should -Contain 'OpenAI' $validateSet.ValidValues | Should -Contain 'Ollama' $validateSet.ValidValues | Should -Contain 'Custom' } } Context 'Update-Documentation' { It 'Should have ValidateSet on Format parameter' { $cmd = Get-Command 'Update-Documentation' $param = $cmd.Parameters['Format'] $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } $validateSet | Should -Not -BeNullOrEmpty $validateSet.ValidValues | Should -Contain 'Markdown' $validateSet.ValidValues | Should -Contain 'HTML' } } Context 'Get-DocumentationDrift' { It 'Should have mandatory DocumentPath parameter' { $cmd = Get-Command 'Get-DocumentationDrift' $param = $cmd.Parameters['DocumentPath'] $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.Mandatory } | Should -Not -BeNullOrEmpty } It 'Should have ValidateSet on Provider parameter' { $cmd = Get-Command 'Get-DocumentationDrift' $param = $cmd.Parameters['Provider'] $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } $validateSet | Should -Not -BeNullOrEmpty } } } Describe 'Import-Documentation' { BeforeAll { $testDir = Join-Path $TestDrive 'docs' New-Item -ItemType Directory -Path $testDir -Force | Out-Null } Context 'Markdown file extraction' { BeforeAll { $mdContent = @" # Test Document ## Servers - SRV01 (10.0.0.1) - Web Server - SRV02 (10.0.0.2) - Database Server "@ $mdPath = Join-Path $testDir 'test.md' $mdContent | Out-File -FilePath $mdPath -Encoding UTF8 } It 'Should extract text from .md files' { $result = Import-Documentation -Path $mdPath $result | Should -Not -BeNullOrEmpty $result.Content | Should -BeLike '*Test Document*' $result.Content | Should -BeLike '*SRV01*' } It 'Should return correct metadata' { $result = Import-Documentation -Path $mdPath $result.FileName | Should -Be 'test.md' $result.FileType | Should -Be 'Markdown' $result.CharacterCount | Should -BeGreaterThan 0 $result.ExtractedDate | Should -Not -BeNullOrEmpty } } Context 'Text file extraction' { BeforeAll { $txtContent = "Server: APP01`nIP: 192.168.1.100`nRole: Application Server" $txtPath = Join-Path $testDir 'test.txt' $txtContent | Out-File -FilePath $txtPath -Encoding UTF8 } It 'Should extract text from .txt files' { $result = Import-Documentation -Path $txtPath $result | Should -Not -BeNullOrEmpty $result.Content | Should -BeLike '*APP01*' $result.FileType | Should -Be 'Text' } } Context 'Directory scanning' { BeforeAll { 'File A content' | Out-File -FilePath (Join-Path $testDir 'a.md') -Encoding UTF8 'File B content' | Out-File -FilePath (Join-Path $testDir 'b.txt') -Encoding UTF8 } It 'Should import multiple files from a directory' { $results = Import-Documentation -Path $testDir $results.Count | Should -BeGreaterOrEqual 2 } It 'Should filter by FileType' { $results = Import-Documentation -Path $testDir -FileType Markdown $results | ForEach-Object { $_.FileType | Should -Be 'Markdown' } } } Context 'Missing file handling' { It 'Should warn on nonexistent path' { $result = Import-Documentation -Path (Join-Path $testDir 'nonexistent.md') -WarningAction SilentlyContinue -WarningVariable warnings $warnings.Count | Should -BeGreaterThan 0 } } } Describe 'Extract-EnvironmentFacts' { BeforeAll { $testFactsPath = Join-Path $TestDrive 'test-facts.json' } Context 'AI response parsing' { It 'Should parse AI response into fact objects' { # Mock the AI completion to return a known JSON response Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Invoke-AICompletion' -MockWith { return @' { "facts": [ { "source_text": "SRV01 (10.0.0.1) - Web Server", "category": "server", "claims": [ { "claim_type": "server_exists", "subject": "SRV01", "expected_value": "exists", "verification_method": "ad_computer" }, { "claim_type": "server_ip", "subject": "SRV01", "expected_value": "10.0.0.1", "verification_method": "dns_resolve" } ], "confidence": 0.95 } ] } '@ } $result = Extract-EnvironmentFacts -DocumentText 'SRV01 (10.0.0.1) - Web Server' -SourceDocument 'test.md' -Provider Anthropic -ApiKey 'fake-key' -FactsPath $testFactsPath $result | Should -Not -BeNullOrEmpty $result[0].category | Should -Be 'server' $result[0].claims.Count | Should -Be 2 $result[0].claims[0].claim_type | Should -Be 'server_exists' $result[0].claims[0].subject | Should -Be 'SRV01' $result[0].claims[1].expected_value | Should -Be '10.0.0.1' } It 'Should save facts to JSON file' { Test-Path $testFactsPath | Should -BeTrue $savedData = Get-Content -Path $testFactsPath -Raw | ConvertFrom-Json $savedData.facts | Should -Not -BeNullOrEmpty $savedData.metadata | Should -Not -BeNullOrEmpty } } Context 'Fact deduplication' { It 'Should not create duplicate facts when re-extracting same document' { Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Invoke-AICompletion' -MockWith { return @' { "facts": [ { "source_text": "SRV01 (10.0.0.1) - Web Server", "category": "server", "claims": [ { "claim_type": "server_exists", "subject": "SRV01", "expected_value": "exists", "verification_method": "ad_computer" } ], "confidence": 0.95 } ] } '@ } $dedupPath = Join-Path $TestDrive 'dedup-facts.json' # First extraction Extract-EnvironmentFacts -DocumentText 'SRV01 (10.0.0.1)' -SourceDocument 'test.md' -Provider Anthropic -ApiKey 'fake-key' -FactsPath $dedupPath | Out-Null $firstCount = (Get-Content $dedupPath -Raw | ConvertFrom-Json).facts.Count # Second extraction with same data Extract-EnvironmentFacts -DocumentText 'SRV01 (10.0.0.1)' -SourceDocument 'test.md' -Provider Anthropic -ApiKey 'fake-key' -FactsPath $dedupPath | Out-Null $secondCount = (Get-Content $dedupPath -Raw | ConvertFrom-Json).facts.Count $secondCount | Should -Be $firstCount } } Context 'AI response with code blocks' { It 'Should handle JSON wrapped in markdown code blocks' { Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Invoke-AICompletion' -MockWith { return @' Here are the extracted facts: ```json { "facts": [ { "source_text": "DB01 runs PostgreSQL", "category": "software", "claims": [ { "claim_type": "software_version", "subject": "DB01", "expected_value": "PostgreSQL", "verification_method": "cim_service" } ], "confidence": 0.85 } ] } ``` '@ } $codeBlockPath = Join-Path $TestDrive 'codeblock-facts.json' $result = Extract-EnvironmentFacts -DocumentText 'DB01 runs PostgreSQL' -SourceDocument 'test.md' -Provider Anthropic -ApiKey 'fake-key' -FactsPath $codeBlockPath $result | Should -Not -BeNullOrEmpty $result[0].claims[0].subject | Should -Be 'DB01' } } } Describe 'Test-EnvironmentFacts' { Context 'Claim verification with mocked cmdlets' { BeforeAll { $verifyFactsPath = Join-Path $TestDrive 'verify-facts.json' $factsDb = @{ metadata = @{ created = (Get-Date).ToString('o') last_verified = $null source_documents = @('test.md') total_facts = 2 verified = 0 drift_detected = 0 unverifiable = 0 } facts = @( @{ id = 'fact-test-001' source_document = 'test.md' source_text = 'SRV01 resolves to 10.0.0.1' category = 'server' claims = @( @{ claim_type = 'server_ip' subject = 'SRV01' expected_value = '10.0.0.1' verification_method = 'dns_resolve' actual_value = $null status = 'pending' last_checked = $null } ) confidence = 0.95 last_verified = $null overall_status = 'pending' }, @{ id = 'fact-test-002' source_document = 'test.md' source_text = 'SRV02 resolves to 10.0.0.2' category = 'server' claims = @( @{ claim_type = 'server_ip' subject = 'SRV02' expected_value = '10.0.0.2' verification_method = 'dns_resolve' actual_value = $null status = 'pending' last_checked = $null } ) confidence = 0.90 last_verified = $null overall_status = 'pending' } ) } $factsDb | ConvertTo-Json -Depth 10 | Out-File -FilePath $verifyFactsPath -Encoding UTF8 } It 'Should mark matching DNS as verified' { Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Resolve-DnsName' -MockWith { return @([PSCustomObject]@{ QueryType = 'A'; IPAddress = '10.0.0.1'; Name = 'SRV01' }) } -ParameterFilter { $Name -eq 'SRV01' } Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Resolve-DnsName' -MockWith { return @([PSCustomObject]@{ QueryType = 'A'; IPAddress = '10.0.0.99'; Name = 'SRV02' }) } -ParameterFilter { $Name -eq 'SRV02' } $result = Test-EnvironmentFacts -FactsPath $verifyFactsPath $result.Verified | Should -Be 1 $result.Drift | Should -Be 1 } It 'Should update the facts.json file in place' { $updated = Get-Content -Path $verifyFactsPath -Raw | ConvertFrom-Json $updated.metadata.last_verified | Should -Not -BeNullOrEmpty } } } Describe 'Test-SingleFact' { BeforeAll { # Access private function via module scope $testSingleFact = & (Get-Module 'Infra-LivingDoc') { Get-Command 'Test-SingleFact' } } Context 'dns_resolve verification' { It 'Should return verified when IP matches' { Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Resolve-DnsName' -MockWith { return @([PSCustomObject]@{ QueryType = 'A'; IPAddress = '10.0.0.1'; Name = 'SRV01' }) } $claim = [PSCustomObject]@{ claim_type = 'server_ip' subject = 'SRV01' expected_value = '10.0.0.1' verification_method = 'dns_resolve' actual_value = $null status = 'pending' last_checked = $null } $result = & (Get-Module 'Infra-LivingDoc') { param($c) Test-SingleFact -Claim $c } $claim $result.status | Should -Be 'verified' $result.actual_value | Should -BeLike '*10.0.0.1*' } It 'Should return drift when IP does not match' { Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Resolve-DnsName' -MockWith { return @([PSCustomObject]@{ QueryType = 'A'; IPAddress = '10.0.0.99'; Name = 'SRV01' }) } $claim = [PSCustomObject]@{ claim_type = 'server_ip' subject = 'SRV01' expected_value = '10.0.0.1' verification_method = 'dns_resolve' actual_value = $null status = 'pending' last_checked = $null } $result = & (Get-Module 'Infra-LivingDoc') { param($c) Test-SingleFact -Claim $c } $claim $result.status | Should -Be 'drift' } } Context 'unverifiable method' { It 'Should return unverifiable status' { $claim = [PSCustomObject]@{ claim_type = 'other' subject = 'something' expected_value = 'some value' verification_method = 'unverifiable' actual_value = $null status = 'pending' last_checked = $null } $result = & (Get-Module 'Infra-LivingDoc') { param($c) Test-SingleFact -Claim $c } $claim $result.status | Should -Be 'unverifiable' } } Context 'network_test verification' { It 'Should return verified when ping succeeds' { Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Test-NetConnection' -MockWith { return [PSCustomObject]@{ PingSucceeded = $true RemoteAddress = '10.0.0.1' TcpTestSucceeded = $null } } $claim = [PSCustomObject]@{ claim_type = 'other' subject = '10.0.0.1' expected_value = 'reachable' verification_method = 'network_test' actual_value = $null status = 'pending' last_checked = $null } $result = & (Get-Module 'Infra-LivingDoc') { param($c) Test-SingleFact -Claim $c } $claim $result.status | Should -Be 'verified' } } Context 'Unknown verification method' { It 'Should return unverifiable for unknown methods' { $claim = [PSCustomObject]@{ claim_type = 'other' subject = 'something' expected_value = 'some value' verification_method = 'totally_unknown_method' actual_value = $null status = 'pending' last_checked = $null } $result = & (Get-Module 'Infra-LivingDoc') { param($c) Test-SingleFact -Claim $c } $claim $result.status | Should -Be 'unverifiable' } } } Describe 'Sample Files Validation' { It 'Should have valid sample-facts.json' { $sampleFactsPath = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples') 'sample-facts.json' Test-Path $sampleFactsPath | Should -BeTrue $facts = Get-Content -Path $sampleFactsPath -Raw | ConvertFrom-Json $facts.metadata | Should -Not -BeNullOrEmpty $facts.facts | Should -Not -BeNullOrEmpty $facts.facts.Count | Should -BeGreaterThan 0 } It 'Should have sample-input.md' { $sampleInputPath = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples') 'sample-input.md' Test-Path $sampleInputPath | Should -BeTrue } It 'Should have sample-drift-report.html' { $sampleReportPath = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples') 'sample-drift-report.html' Test-Path $sampleReportPath | Should -BeTrue } It 'Should have sample-updated-doc.md' { $sampleUpdatedPath = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples') 'sample-updated-doc.md' Test-Path $sampleUpdatedPath | Should -BeTrue } } |