__tests__/Test-AgentActivityHost.ps1

# Offline integration checks using real PowerShell hosts and Write-Progress.
# Run explicitly: pwsh -NoProfile -File ./__tests__/Test-AgentActivityHost.ps1
[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'
if (-not ('AgentActivityHost' -as [type])) {
    Add-Type -TypeDefinition @'
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Management.Automation;
using System.Management.Automation.Host;
using System.Security;
 
public sealed class AgentActivityHost : PSHost
{
    private readonly Guid id = Guid.NewGuid();
    public readonly AgentActivityUI Display;
    public AgentActivityHost(string mode) { Display = new AgentActivityUI(mode); }
    public override Guid InstanceId { get { return id; } }
    public override string Name { get { return "Agent activity integration host"; } }
    public override Version Version { get { return new Version(1, 0); } }
    public override CultureInfo CurrentCulture { get { return CultureInfo.InvariantCulture; } }
    public override CultureInfo CurrentUICulture { get { return CultureInfo.InvariantCulture; } }
    public override PSHostUserInterface UI { get { return Display; } }
    public override void SetShouldExit(int exitCode) { }
    public override void EnterNestedPrompt() { throw new NotSupportedException(); }
    public override void ExitNestedPrompt() { throw new NotSupportedException(); }
    public override void NotifyBeginApplication() { }
    public override void NotifyEndApplication() { }
}
 
public sealed class AgentActivityUI : PSHostUserInterface
{
    private readonly string mode;
    public readonly ConcurrentQueue<ProgressRecord> Progress = new ConcurrentQueue<ProgressRecord>();
    public readonly ConcurrentQueue<string> Verbose = new ConcurrentQueue<string>();
    public AgentActivityUI(string mode) { this.mode = mode; }
    public override PSHostRawUserInterface RawUI { get { return null; } }
    public override void WriteProgress(long sourceId, ProgressRecord record)
    {
        if (mode == "Unsupported") throw new NotSupportedException("This host cannot display progress.");
        if (mode == "Cancelled") throw new PipelineStoppedException();
        Progress.Enqueue(record);
    }
    public override void WriteVerboseLine(string message) { Verbose.Enqueue(message); }
    public override void Write(string value) { }
    public override void Write(ConsoleColor foreground, ConsoleColor background, string value) { }
    public override void WriteLine(string value) { }
    public override void WriteErrorLine(string value) { }
    public override void WriteDebugLine(string value) { }
    public override void WriteWarningLine(string value) { }
    public override string ReadLine() { throw new NotSupportedException(); }
    public override SecureString ReadLineAsSecureString() { throw new NotSupportedException(); }
    public override Dictionary<string, PSObject> Prompt(string caption, string message, Collection<FieldDescription> descriptions) { throw new NotSupportedException(); }
    public override int PromptForChoice(string caption, string message, Collection<ChoiceDescription> choices, int defaultChoice) { throw new NotSupportedException(); }
    public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) { throw new NotSupportedException(); }
    public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes types, PSCredentialUIOptions options) { throw new NotSupportedException(); }
}
'@

}

$activitySource = (Resolve-Path "$PSScriptRoot/../Public/AgentActivity.ps1").Path
$scenario = {
    param($Source, $Wait)
    . $Source
    $ErrorActionPreference = 'Stop'
    $VerbosePreference = 'Continue'
    $ProgressPreference = 'Continue'
    $activity = New-AgentActivity -Provider 'Integration' -Model 'offline' -MaxIterations 3
    try {
        Write-AgentActivity -Activity $activity -Message 'Waiting for response' -Round 1
        if ($Wait) { while ($true) { Start-Sleep -Milliseconds 20 } }
        'answer'
    }
    finally {
        Write-AgentActivity -Activity $activity -Message 'Request stopped' -Completed
    }
}

foreach ($mode in @('Normal', 'Unsupported', 'Cancelled', 'StopPipeline')) {
    $displayMode = if ($mode -eq 'StopPipeline') { 'Normal' } else { $mode }
    $testHost = [AgentActivityHost]::new($displayMode)
    $runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($testHost)
    $pipeline = [powershell]::Create()
    try {
        $runspace.Open()
        $pipeline.Runspace = $runspace
        $null = $pipeline.AddScript($scenario.ToString()).AddArgument($activitySource).AddArgument($mode -eq 'StopPipeline')
        $results = @()
        if ($mode -eq 'StopPipeline') {
            $pending = $pipeline.BeginInvoke()
            $deadline = [System.Diagnostics.Stopwatch]::StartNew()
            while ($testHost.Display.Progress.Count -eq 0 -and -not $pending.IsCompleted) {
                if ($deadline.Elapsed.TotalSeconds -gt 5) { throw 'Pipeline did not start reporting progress.' }
                Start-Sleep -Milliseconds 20
            }
            $pipeline.Stop()
            try { $null = $pipeline.EndInvoke($pending) }
            catch {
                if ($_.Exception -isnot [System.Management.Automation.PipelineStoppedException] -and
                    $_.Exception.InnerException -isnot [System.Management.Automation.PipelineStoppedException]) { throw }
            }
        }
        else {
            try { $results = @($pipeline.Invoke()) }
            catch {
                if ($mode -ne 'Cancelled') { throw }
            }
        }

        $events = $testHost.Display.Progress.ToArray()
        $logs = $testHost.Display.Verbose.ToArray()
        switch ($mode) {
            'Normal' {
                if ($results.Count -ne 1 -or [string]$results[0] -ne 'answer' -or
                    $events.Count -ne 2 -or $events[-1].RecordType -ne 'Completed') {
                    throw 'Normal progress altered the answer or failed to clear the display.'
                }
            }
            'Unsupported' {
                if ($results.Count -ne 1 -or [string]$results[0] -ne 'answer' -or
                    -not ($logs -match 'Progress display unavailable')) {
                    throw 'An unsupported progress display prevented the answer or hid the diagnostic.'
                }
            }
            'Cancelled' {
                if ($results.Count -gt 0 -or $pipeline.InvocationStateInfo.State -eq 'Completed' -or
                    ($logs -match 'Progress display unavailable')) {
                    throw 'A progress cancellation was swallowed as a display failure.'
                }
            }
            'StopPipeline' {
                if ($pipeline.InvocationStateInfo.State -ne 'Stopped' -or $events.Count -lt 2 -or
                    $events[-1].RecordType -ne 'Completed') {
                    throw 'Stopping the running pipeline did not cancel execution and clear progress.'
                }
            }
        }
        Write-Output "PASS: $mode"
    }
    finally {
        $pipeline.Dispose()
        $runspace.Dispose()
    }
}