tests/PowerCraft.AI.Tests.ps1
|
BeforeAll { # Mock PowerCraft.Secrets dependency if (-not (Get-Module PowerCraft.Secrets -ErrorAction SilentlyContinue)) { # Create a minimal mock module so the manifest dependency is satisfied $mockModule = New-Module -Name PowerCraft.Secrets -ScriptBlock { function Get-PCSecret { param([string]$Name) return $null } function Get-PCSecretNames { return @() } Export-ModuleMember -Function * } Import-Module $mockModule -Force } $modulePath = Split-Path $PSScriptRoot -Parent Import-Module $modulePath -Force } Describe 'Module loads correctly' { It 'Exports expected functions' { $expected = @( 'Invoke-PCCompletion' 'Invoke-PCCompletionCached' 'Get-PCProviders' 'Test-PCProvider' ) $exported = (Get-Module PowerCraft.AI).ExportedFunctions.Keys foreach ($fn in $expected) { $exported | Should -Contain $fn } } } Describe 'Provider configuration' { It 'Has openai provider defined' { InModuleScope PowerCraft.AI { $script:Providers.ContainsKey('openai') | Should -BeTrue $script:Providers.openai.DefaultModel | Should -Not -BeNullOrEmpty } } It 'Has gemini provider defined' { InModuleScope PowerCraft.AI { $script:Providers.ContainsKey('gemini') | Should -BeTrue $script:Providers.gemini.DefaultModel | Should -Not -BeNullOrEmpty } } It 'Has anthropic provider defined' { InModuleScope PowerCraft.AI { $script:Providers.ContainsKey('anthropic') | Should -BeTrue $script:Providers.anthropic.DefaultModel | Should -Not -BeNullOrEmpty } } } Describe 'Resolve-PCApiKey' { It 'Returns key from secrets store' { InModuleScope PowerCraft.AI { Mock Get-PCSecret { return 'secret-key-123' } -ParameterFilter { $Name -eq 'openai' } $result = Resolve-PCApiKey -Provider 'openai' $result | Should -Be 'secret-key-123' } } It 'Returns null when no key in secrets store' { InModuleScope PowerCraft.AI { Mock Get-PCSecret { return $null } $result = Resolve-PCApiKey -Provider 'openai' $result | Should -BeNullOrEmpty } } It 'Uses correct secret name per provider' { InModuleScope PowerCraft.AI { Mock Get-PCSecret { return 'gemini-key' } -ParameterFilter { $Name -eq 'gemini' } $result = Resolve-PCApiKey -Provider 'gemini' $result | Should -Be 'gemini-key' } } It 'Resolves anthropic provider' { InModuleScope PowerCraft.AI { Mock Get-PCSecret { return 'anthropic-key' } -ParameterFilter { $Name -eq 'anthropic' } $result = Resolve-PCApiKey -Provider 'anthropic' $result | Should -Be 'anthropic-key' } } } Describe 'Get-PCProviders' { It 'Returns providers with configured keys' { InModuleScope PowerCraft.AI { Mock Get-PCSecret { return 'test-key' } -ParameterFilter { $Name -eq 'openai' } $result = Get-PCProviders $result | Where-Object Provider -eq 'openai' | Should -Not -BeNullOrEmpty } } It 'Shows DefaultModel for each provider' { InModuleScope PowerCraft.AI { Mock Get-PCSecret { return 'test-key' } -ParameterFilter { $Name -eq 'gemini' } $result = Get-PCProviders $gemini = $result | Where-Object Provider -eq 'gemini' $gemini.DefaultModel | Should -Not -BeNullOrEmpty } } } Describe 'Invoke-PCCompletion parameter validation' { It 'Throws when no provider can be resolved' { InModuleScope PowerCraft.AI { Mock Get-PCSecret { return $null } { Invoke-PCCompletion -UserPrompt "test" } | Should -Throw '*No AI provider*' } } It 'Throws when API key missing for specified provider' { InModuleScope PowerCraft.AI { Mock Get-PCSecret { return $null } { Invoke-PCCompletion -Provider openai -UserPrompt "test" } | Should -Throw '*No API key*' } } It 'Auto-detects provider from model name' { InModuleScope PowerCraft.AI { Mock Get-PCSecret { return 'fake-key' } -ParameterFilter { $Name -eq 'gemini' } # This will throw on API call but should correctly pick gemini provider { Invoke-PCCompletion -Model 'gemini-2.5-flash' -UserPrompt "test" } | Should -Throw } } } Describe 'Invoke-PCCompletionCached' { BeforeAll { $cacheDir = Join-Path $TestDrive 'ai-cache' } It 'Creates cache file on miss' { # Pre-populate a cache file to simulate a hit (we cannot call real APIs) $cacheFile = Join-Path $cacheDir 'test-item.json' New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null @{ schema = 'ai-response-cache' version = '1.0' cache_key = 'test-item' metadata = @{ model = 'gpt-4o' provider = 'openai' timestamp = (Get-Date).ToString('o') duration_ms = 1000 max_tokens = 4096 response_length = 5 } raw_response = 'Hello' } | ConvertTo-Json -Depth 5 | Set-Content $cacheFile $result = Invoke-PCCompletionCached -UserPrompt "test" -CachePath $cacheDir -CacheKey 'test-item' -Provider openai $result | Should -Be 'Hello' } It 'Returns cached response on hit' { $cacheFile = Join-Path $cacheDir 'cached-hit.json' @{ schema = 'ai-response-cache' version = '1.0' cache_key = 'cached-hit' metadata = @{ model = 'gpt-4o' provider = 'openai' timestamp = (Get-Date).ToString('o') duration_ms = 500 max_tokens = 4096 response_length = 11 } raw_response = 'cached reply' } | ConvertTo-Json -Depth 5 | Set-Content $cacheFile $result = Invoke-PCCompletionCached -UserPrompt "anything" -CachePath $cacheDir -CacheKey 'cached-hit' -Provider openai $result | Should -Be 'cached reply' } It 'Respects cache TTL — returns cached if not expired' { $cacheFile = Join-Path $cacheDir 'ttl-fresh.json' @{ schema = 'ai-response-cache' version = '1.0' cache_key = 'ttl-fresh' metadata = @{ model = 'gpt-4o' provider = 'openai' timestamp = (Get-Date).ToString('o') duration_ms = 100 max_tokens = 4096 response_length = 5 } raw_response = 'fresh' } | ConvertTo-Json -Depth 5 | Set-Content $cacheFile $result = Invoke-PCCompletionCached -UserPrompt "x" -CachePath $cacheDir -CacheKey 'ttl-fresh' -CacheTTLHours 24 $result | Should -Be 'fresh' } It 'Expires cache when TTL exceeded' { $cacheFile = Join-Path $cacheDir 'ttl-expired.json' @{ schema = 'ai-response-cache' version = '1.0' cache_key = 'ttl-expired' metadata = @{ model = 'gpt-4o' provider = 'openai' timestamp = (Get-Date).AddHours(-48).ToString('o') duration_ms = 100 max_tokens = 4096 response_length = 7 } raw_response = 'expired' } | ConvertTo-Json -Depth 5 | Set-Content $cacheFile # Will try to call API and fail (no real key), which proves cache was skipped InModuleScope PowerCraft.AI -Parameters @{ cacheDir = $cacheDir } { Mock Get-PCSecret { return $null } { Invoke-PCCompletionCached -UserPrompt "x" -CachePath $cacheDir -CacheKey 'ttl-expired' -CacheTTLHours 1 -Provider openai } | Should -Throw '*No API key*' } } } Describe 'Test-PCProvider' { It 'Returns false when no key configured' { InModuleScope PowerCraft.AI { Mock Get-PCSecret { return $null } $result = Test-PCProvider -Name openai -WarningAction SilentlyContinue $result | Should -BeFalse } } } |