__tests__/Test-AgentProgressLive.ps1

# Explicit live checks; invokes real providers and tools without mocks.
# Run: pwsh -NoProfile -File ./__tests__/Test-AgentProgressLive.ps1
[CmdletBinding()]
param(
    [string[]]$Models = @('openai:gpt-6-luna', 'anthropic:claude-sonnet-5-5', 'vercel:openai/gpt-6-luna', 'deepseek:deepseek-flash'),
    [ValidateSet('Success', 'ToolFailure', 'Limit', 'ApiFailure')]
    [string[]]$Cases = @('Success', 'ToolFailure', 'Limit', 'ApiFailure')
)

$ErrorActionPreference = 'Stop'
if ($env:PSAISUITE_DEFAULT_MODEL) {
    throw 'Clear PSAISUITE_DEFAULT_MODEL before testing individual providers.'
}
$manifestPath = (Resolve-Path "$PSScriptRoot/../PSAISuite.psd1").Path
$runCase = {
    param($ManifestPath, $Model, $Mode, $Nonce)
    $ErrorActionPreference = 'Stop'
    $ProgressPreference = 'Continue'
    Import-Module $ManifestPath -Force
    $script:probeValue = $Nonce
    $script:probeMode = $Mode
    $script:probeCalls = 0
    function Get-AgentProgressValue {
        <#
        .SYNOPSIS
        Returns the test value. Call this tool to discover the value.
        #>

        [CmdletBinding()]
        param()
        $script:probeCalls++
        if ($script:probeMode -eq 'ToolFailure') {
            Write-Error 'Intentional live tool failure' -ErrorAction Continue
            return
        }
        $script:probeValue
    }

    $options = @{
        Model = $Model
        Tools = @('Get-AgentProgressValue')
        SystemPrompt = 'Call Get-AgentProgressValue exactly once before answering. Return only its exact value. If it fails, say the tool failed and do not retry.'
        Prompt = 'Call the tool and return the test value.'
        MaxIterations = 3
        Verbose = $true
    }
    if ($Mode -eq 'Limit') { $options.MaxIterations = 1 }
    if ($Mode -eq 'ApiFailure') {
        $provider = ($Model -split ':', 2)[0]
        $options.Model = if ($provider -eq 'vercel') { 'vercel:openai/psaisuite-missing-model-live-test' } else { "$provider`:psaisuite-missing-model-live-test" }
    }
    $errorCaught = $false
    $response = @()
    try { $response = @(New-AgentHarness @options) }
    catch { $errorCaught = $true }
    [pscustomobject]@{ Response = $response; ToolCalls = $script:probeCalls; ErrorCaught = $errorCaught }
}

foreach ($model in $Models) {
    foreach ($case in $Cases) {
        $nonce = [guid]::NewGuid().ToString()
        $pipeline = [powershell]::Create()
        try {
            $null = $pipeline.AddScript($runCase.ToString()).AddArgument($manifestPath).AddArgument($model).AddArgument($case).AddArgument($nonce)
            $pending = $pipeline.BeginInvoke()
            $timer = [System.Diagnostics.Stopwatch]::StartNew()
            $sawLiveProgress = $false
            while (-not $pending.IsCompleted) {
                if ($pipeline.Streams.Progress.Count -gt 0) { $sawLiveProgress = $true }
                if ($timer.Elapsed.TotalSeconds -gt 180) {
                    $pipeline.Stop()
                    throw "$model $case timed out."
                }
                Start-Sleep -Milliseconds 100
            }
            $results = @($pipeline.EndInvoke($pending))
            if ($results.Count -ne 1) { throw "$model $case returned unexpected success-stream output." }
            $result = $results[0]
            $progress = @($pipeline.Streams.Progress)
            $logs = @($pipeline.Streams.Verbose | ForEach-Object Message)
            $statuses = @($progress | ForEach-Object StatusDescription)
            if ($progress.Count -eq 0 -or $progress[-1].RecordType -ne 'Completed') {
                throw "$model $case left progress unfinished."
            }
            if (@($progress | Where-Object PercentComplete -ne -1).Count) {
                throw "$model $case reported a misleading completion percentage."
            }
            if (-not ($statuses -match 'Round 1/\d+ \| Waiting for response \| [\d.]+s elapsed')) {
                throw "$model $case is missing the waiting status."
            }
            if (-not ($logs -match 'Waiting for response')) {
                throw "$model $case did not forward -Verbose through the harness."
            }
            switch ($case) {
                'Success' {
                    if (-not $sawLiveProgress) { throw "$model progress was not visible while the request ran." }
                    if ($result.Response.Count -ne 1 -or $result.Response[0].Trim() -ne $nonce -or $result.ToolCalls -ne 1) {
                        throw "$model failed real tool execution or polluted the response stream."
                    }
                    foreach ($expected in @('Tool started: Get-AgentProgressValue', 'Tool completed: Get-AgentProgressValue', 'Round 2/', 'Response completed')) {
                        if (-not ($statuses -match [regex]::Escape($expected))) { throw "$model missing activity: $expected" }
                    }
                }
                'ToolFailure' {
                    if ($result.ErrorCaught -or $result.ToolCalls -ne 1 -or -not ($statuses -match 'Tool failed: Get-AgentProgressValue')) {
                        throw "$model did not report a real tool failure and continue the conversation."
                    }
                }
                'Limit' {
                    if ($result.ToolCalls -ne 1 -or -not ($statuses[-1] -match 'Stopped: maximum iterations reached')) {
                        throw "$model did not report the iteration limit."
                    }
                }
                'ApiFailure' {
                    if (-not $result.ErrorCaught -or -not ($statuses[-1] -match 'Request failed')) {
                        throw "$model did not report the API failure (caught=$($result.ErrorCaught); statuses=$($statuses -join '; '))."
                    }
                }
            }
            Write-Output "PASS: $model / $case ($($progress.Count) progress events; output kept separate)."
        }
        finally { $pipeline.Dispose() }
    }
}