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 env var when set' { $origKey = $env:OPENAI_API_KEY $env:OPENAI_API_KEY = 'test-key-123' try { InModuleScope PowerCraft.AI { $result = Resolve-PCApiKey -Provider 'openai' $result | Should -Be 'test-key-123' } } finally { if ($origKey) { $env:OPENAI_API_KEY = $origKey } else { Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue } } } It 'Returns null when no key available' { $origKeys = @{ OPENAI_API_KEY = $env:OPENAI_API_KEY } Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue try { InModuleScope PowerCraft.AI { Mock Get-PCSecret { return $null } $result = Resolve-PCApiKey -Provider 'openai' $result | Should -BeNullOrEmpty } } finally { if ($origKeys.OPENAI_API_KEY) { $env:OPENAI_API_KEY = $origKeys.OPENAI_API_KEY } } } It 'Supports GEMINI_API_KEY env var' { $orig = $env:GEMINI_API_KEY $env:GEMINI_API_KEY = 'gemini-test-key' try { InModuleScope PowerCraft.AI { $result = Resolve-PCApiKey -Provider 'gemini' $result | Should -Be 'gemini-test-key' } } finally { if ($orig) { $env:GEMINI_API_KEY = $orig } else { Remove-Item env:GEMINI_API_KEY -ErrorAction SilentlyContinue } } } It 'Supports ANTHROPIC_API_KEY env var' { $orig = $env:ANTHROPIC_API_KEY $env:ANTHROPIC_API_KEY = 'claude-test-key' try { InModuleScope PowerCraft.AI { $result = Resolve-PCApiKey -Provider 'anthropic' $result | Should -Be 'claude-test-key' } } finally { if ($orig) { $env:ANTHROPIC_API_KEY = $orig } else { Remove-Item env:ANTHROPIC_API_KEY -ErrorAction SilentlyContinue } } } } Describe 'Get-PCProviders' { It 'Returns providers with configured keys' { $orig = $env:OPENAI_API_KEY $env:OPENAI_API_KEY = 'test-key' try { $result = Get-PCProviders $result | Where-Object Provider -eq 'openai' | Should -Not -BeNullOrEmpty } finally { if ($orig) { $env:OPENAI_API_KEY = $orig } else { Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue } } } It 'Shows DefaultModel for each provider' { $orig = $env:GEMINI_API_KEY $env:GEMINI_API_KEY = 'test-key' try { $result = Get-PCProviders $gemini = $result | Where-Object Provider -eq 'gemini' $gemini.DefaultModel | Should -Not -BeNullOrEmpty } finally { if ($orig) { $env:GEMINI_API_KEY = $orig } else { Remove-Item env:GEMINI_API_KEY -ErrorAction SilentlyContinue } } } } Describe 'Invoke-PCCompletion parameter validation' { It 'Throws when no provider can be resolved' { # Clear all env keys $origO = $env:OPENAI_API_KEY; $origG = $env:GEMINI_API_KEY; $origA = $env:ANTHROPIC_API_KEY Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue Remove-Item env:GEMINI_API_KEY -ErrorAction SilentlyContinue Remove-Item env:ANTHROPIC_API_KEY -ErrorAction SilentlyContinue try { { Invoke-PCCompletion -UserPrompt "test" } | Should -Throw '*No AI provider*' } finally { if ($origO) { $env:OPENAI_API_KEY = $origO } if ($origG) { $env:GEMINI_API_KEY = $origG } if ($origA) { $env:ANTHROPIC_API_KEY = $origA } } } It 'Throws when API key missing for specified provider' { $orig = $env:OPENAI_API_KEY Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue try { { Invoke-PCCompletion -Provider openai -UserPrompt "test" } | Should -Throw '*No API key*' } finally { if ($orig) { $env:OPENAI_API_KEY = $orig } } } It 'Auto-detects provider from model name' { $orig = $env:GEMINI_API_KEY $env:GEMINI_API_KEY = 'fake-key' try { # This will throw on API call but should correctly pick gemini provider { Invoke-PCCompletion -Model 'gemini-2.5-flash' -UserPrompt "test" } | Should -Throw } finally { if ($orig) { $env:GEMINI_API_KEY = $orig } else { Remove-Item env:GEMINI_API_KEY -ErrorAction SilentlyContinue } } } } 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 $origKey = $env:OPENAI_API_KEY Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue try { { Invoke-PCCompletionCached -UserPrompt "x" -CachePath $cacheDir -CacheKey 'ttl-expired' -CacheTTLHours 1 -Provider openai } | Should -Throw '*No API key*' } finally { if ($origKey) { $env:OPENAI_API_KEY = $origKey } } } } Describe 'Test-PCProvider' { It 'Returns false when no key configured' { $orig = $env:OPENAI_API_KEY Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue try { $result = Test-PCProvider -Name openai -WarningAction SilentlyContinue $result | Should -BeFalse } finally { if ($orig) { $env:OPENAI_API_KEY = $orig } } } } |