|
| 1 | +#Requires -Modules @{ ModuleName='Pester'; ModuleVersion='5.0.0' } |
| 2 | +# PR #481: a profile can pin its agent pane to a supported ACP agent installed |
| 3 | +# inside that profile's WSL distro. The routing assertion is deterministic once |
| 4 | +# a native WSL agent is present; the chat assertion additionally requires auth. |
| 5 | + |
| 6 | +BeforeDiscovery { |
| 7 | + $devPkg = Get-AppxPackage | Where-Object { $_.PackageFamilyName -like 'IntelligentTerminal_*' } |
| 8 | + $script:Ready = [bool]( |
| 9 | + $devPkg -and |
| 10 | + (Get-Command winapp -ErrorAction SilentlyContinue) -and |
| 11 | + (Get-Command wsl.exe -ErrorAction SilentlyContinue) |
| 12 | + ) |
| 13 | +} |
| 14 | + |
| 15 | +Describe 'Feature profile-scoped WSL agent backend' -Tag 'Feature' -Skip:(-not $script:Ready) { |
| 16 | + BeforeAll { |
| 17 | + Import-Module (Join-Path $PSScriptRoot '..\ItE2E\ItE2E.psd1') -Force |
| 18 | + |
| 19 | + $script:app = $null |
| 20 | + $script:skipReason = $null |
| 21 | + $script:oldAgentPaneId = $null |
| 22 | + $script:profileGuid = '{e2e48100-4810-4810-9810-000000000001}' |
| 23 | + $script:profileName = 'ITE2E PR481 WSL Agent' |
| 24 | + |
| 25 | + $distroProbe = Invoke-Native -FilePath 'wsl.exe' -Arguments @( |
| 26 | + '-e', 'sh', '-lc', 'printf "%s" "${WSL_DISTRO_NAME:-}"' |
| 27 | + ) -TimeoutSec 45 |
| 28 | + $script:distro = $distroProbe.StdOut.Trim() |
| 29 | + if ($distroProbe.ExitCode -ne 0 -or -not $script:distro) { |
| 30 | + $script:skipReason = 'no runnable default WSL distro is available' |
| 31 | + return |
| 32 | + } |
| 33 | + if ($script:distro -match '["\r\n]') { |
| 34 | + $script:skipReason = 'the default WSL distro name cannot be represented safely in a test profile' |
| 35 | + return |
| 36 | + } |
| 37 | + |
| 38 | + # Match the product's source probe: reject Windows executables leaked |
| 39 | + # through WSL interop, and require npx for adapter-backed agents. |
| 40 | + $agentProbeScript = @' |
| 41 | +for id in copilot gemini opencode claude codex; do |
| 42 | + path="$(command -v "$id" 2>/dev/null || true)" |
| 43 | + [ -n "$path" ] || continue |
| 44 | + lower="$(printf '%s' "$path" | tr '[:upper:]' '[:lower:]')" |
| 45 | + case "$lower" in |
| 46 | + /mnt/*|*.exe|*.cmd|*.bat) continue ;; |
| 47 | + esac |
| 48 | + case "$id" in |
| 49 | + claude|codex) |
| 50 | + npx_path="$(command -v npx 2>/dev/null || true)" |
| 51 | + npx_lower="$(printf '%s' "$npx_path" | tr '[:upper:]' '[:lower:]')" |
| 52 | + case "$npx_lower" in |
| 53 | + ''|/mnt/*|*.exe|*.cmd|*.bat) continue ;; |
| 54 | + esac |
| 55 | + ;; |
| 56 | + esac |
| 57 | + printf '%s\n' "$id" |
| 58 | + break |
| 59 | +done |
| 60 | +'@ |
| 61 | + $agentProbe = Invoke-Native -FilePath 'wsl.exe' -Arguments @( |
| 62 | + '-d', $script:distro, '--', 'bash', '-lc', $agentProbeScript |
| 63 | + ) -TimeoutSec 45 |
| 64 | + $knownAgents = @('copilot', 'gemini', 'opencode', 'claude', 'codex') |
| 65 | + $script:agent = @( |
| 66 | + $agentProbe.StdOut -split '\r?\n' | |
| 67 | + ForEach-Object { $_.Trim() } | |
| 68 | + Where-Object { $_ -in $knownAgents } |
| 69 | + ) | Select-Object -First 1 |
| 70 | + if ($agentProbe.ExitCode -ne 0 -or -not $script:agent) { |
| 71 | + $script:skipReason = "$($script:distro) has no supported native Linux ACP agent" |
| 72 | + return |
| 73 | + } |
| 74 | + |
| 75 | + $unconfiguredProfile = [pscustomobject][ordered]@{ |
| 76 | + guid = $script:profileGuid |
| 77 | + name = $script:profileName |
| 78 | + commandline = "wsl.exe -d `"$($script:distro)`"" |
| 79 | + } |
| 80 | + $unconfiguredProfiles = [pscustomobject][ordered]@{ |
| 81 | + defaults = [pscustomobject]@{} |
| 82 | + list = @($unconfiguredProfile) |
| 83 | + } |
| 84 | + $script:backend = "wsl:$($script:distro):$($script:agent)" |
| 85 | + $script:app = Start-Terminal -Package Dev -PassFre $true -Settings @{ |
| 86 | + acpAgent = $script:agent |
| 87 | + defaultProfile = $script:profileGuid |
| 88 | + profiles = $unconfiguredProfiles |
| 89 | + } |
| 90 | + $oldAgentPane = Get-AgentPaneSession -App $script:app |
| 91 | + if ($oldAgentPane) { |
| 92 | + $script:oldAgentPaneId = $oldAgentPane.PaneSessionId |
| 93 | + } |
| 94 | + |
| 95 | + # Apply the profile backend only after Start-Terminal has captured log |
| 96 | + # offsets. This exercises settings hot reload and keeps source-routing |
| 97 | + # assertions scoped to this run. |
| 98 | + $configuredProfile = [pscustomobject][ordered]@{ |
| 99 | + guid = $script:profileGuid |
| 100 | + name = $script:profileName |
| 101 | + commandline = "wsl.exe -d `"$($script:distro)`"" |
| 102 | + agentPaneBackend = $script:backend |
| 103 | + } |
| 104 | + $configuredProfiles = [pscustomobject][ordered]@{ |
| 105 | + defaults = [pscustomobject]@{} |
| 106 | + list = @($configuredProfile) |
| 107 | + } |
| 108 | + Set-WtSetting -App $script:app -Key 'profiles' -Value $configuredProfiles | Out-Null |
| 109 | + Open-AgentPane -App $script:app | Out-Null |
| 110 | + } |
| 111 | + AfterAll { |
| 112 | + if ($script:app) { |
| 113 | + Stop-Terminal -App $script:app |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + It 'Hot reload routes the profile agent through its WSL distro without host fallback' { |
| 118 | + if ($script:skipReason) { |
| 119 | + Set-ItResult -Skipped -Because $script:skipReason |
| 120 | + return |
| 121 | + } |
| 122 | + |
| 123 | + $settings = Get-WtSettingsObject -App $script:app |
| 124 | + $profile = @($settings.profiles.list) | |
| 125 | + Where-Object { $_.guid -eq $script:profileGuid } | |
| 126 | + Select-Object -First 1 |
| 127 | + $profile.agentPaneBackend | Should -Be $script:backend |
| 128 | + |
| 129 | + if ($script:oldAgentPaneId) { |
| 130 | + (Test-Until -TimeoutSec 30 -IntervalSec 0.5 -Condition { |
| 131 | + -not (Get-AgentPaneSession -App $script:app -PaneSessionId $script:oldAgentPaneId) |
| 132 | + }) | Should -BeTrue -Because 'changing this profile backend must replace its old host helper' |
| 133 | + } |
| 134 | + |
| 135 | + $agentPattern = [regex]::Escape($script:agent) |
| 136 | + $sourcePattern = [regex]::Escape("wsl:$($script:distro)") |
| 137 | + Assert-Log -App $script:app -Name 'wta-main_master.log' -Pattern ( |
| 138 | + 'resolving agent CLI for helper.*requested_agent_id=Some\("' + $agentPattern + |
| 139 | + '"\).*resolved_agent_source=' + $sourcePattern |
| 140 | + ) -TimeoutSec 60 |
| 141 | + Assert-Log -App $script:app -Name 'wta-main_master.log' -Pattern ( |
| 142 | + 'agent CLI spawned.*agent_source=' + $sourcePattern |
| 143 | + ) -TimeoutSec 60 |
| 144 | + } |
| 145 | + |
| 146 | + It 'The profile-selected WSL agent connects and answers a chat round trip' { |
| 147 | + if ($script:skipReason) { |
| 148 | + Set-ItResult -Skipped -Because $script:skipReason |
| 149 | + return |
| 150 | + } |
| 151 | + if (-not (Wait-AgentReady -App $script:app -TimeoutSec 150)) { |
| 152 | + Set-ItResult -Skipped -Because "$($script:agent) is installed in $($script:distro) but is not authenticated or its ACP server did not connect" |
| 153 | + return |
| 154 | + } |
| 155 | + |
| 156 | + $oldAgentPaneIds = @($script:oldAgentPaneId) | Where-Object { $_ } |
| 157 | + $agentPane = Wait-NewAgentPaneSession -App $script:app ` |
| 158 | + -ExcludePaneSessionId $oldAgentPaneIds -TimeoutSec 30 |
| 159 | + Send-AgentPrompt -App $script:app -PaneSessionId $agentPane.PaneSessionId ` |
| 160 | + -Text 'What is 480 plus 1? Reply with only the number.' | Out-Null |
| 161 | + Assert-AgentPaneText -App $script:app -PaneSessionId $agentPane.PaneSessionId ` |
| 162 | + -Pattern '\b481\b' -TimeoutSec 150 |
| 163 | + } |
| 164 | +} |
0 commit comments