From 8a9b26ff86bc59d8eed7550b0ce1702c2e94c059 Mon Sep 17 00:00:00 2001 From: Cooper-X-Oak Date: Thu, 30 Apr 2026 13:17:11 +0800 Subject: [PATCH] =?UTF-8?q?test(windows):=20=E5=A2=9E=E5=8A=A0=E7=9C=9F?= =?UTF-8?q?=E6=9C=BA=E5=9B=9E=E5=BD=92=20smoke=20=E7=9F=A9=E9=98=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../windows-hotkey-injection-smoke.ps1 | 60 +++ .../windows-microphone-privacy-smoke.ps1 | 285 ++++++++++++ .../windows-real-asr-insertion-smoke.ps1 | 433 ++++++++++++++++++ .../app/scripts/windows-real-regression.ps1 | 147 ++++++ .../app/scripts/windows-smoke-suite.ps1 | 134 ++++++ 5 files changed, 1059 insertions(+) create mode 100644 openless-all/app/scripts/windows-hotkey-injection-smoke.ps1 create mode 100644 openless-all/app/scripts/windows-microphone-privacy-smoke.ps1 create mode 100644 openless-all/app/scripts/windows-real-asr-insertion-smoke.ps1 create mode 100644 openless-all/app/scripts/windows-real-regression.ps1 create mode 100644 openless-all/app/scripts/windows-smoke-suite.ps1 diff --git a/openless-all/app/scripts/windows-hotkey-injection-smoke.ps1 b/openless-all/app/scripts/windows-hotkey-injection-smoke.ps1 new file mode 100644 index 00000000..42d4cd05 --- /dev/null +++ b/openless-all/app/scripts/windows-hotkey-injection-smoke.ps1 @@ -0,0 +1,60 @@ +param( + [string]$ExePath = "", + [int]$TimeoutSeconds = 20 +) + +$ErrorActionPreference = "Stop" + +if ([string]::IsNullOrWhiteSpace($ExePath)) { + $appRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path + $ExePath = Join-Path $appRoot ".artifacts\windows-gnu\dev\openless.exe" +} + +if (-not $env:SystemDrive) { + $env:SystemDrive = "C:" +} +if (-not $env:ProgramData) { + $env:ProgramData = Join-Path $env:SystemDrive "ProgramData" +} + +function Wait-LogPattern($Path, $Pattern, $Since, $TimeoutSeconds) { + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + if (Test-Path $Path) { + $lines = Get-Content -Path $Path -Tail 200 + foreach ($line in $lines) { + if ($line -match $Pattern) { + return $true + } + } + } + Start-Sleep -Milliseconds 500 + } + return $false +} + +if (-not (Test-Path $ExePath)) { + throw "OpenLess executable not found: $ExePath" +} + +$logPath = Join-Path $env:LOCALAPPDATA "OpenLess\Logs\openless.log" +Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force +Remove-Item -LiteralPath $logPath -Force -ErrorAction SilentlyContinue + +Write-Host "== Hotkey injection smoke ==" +$env:OPENLESS_DEBUG_HOTKEY_ON_START = "1" +$process = Start-Process -FilePath $ExePath -PassThru +try { + if (-not (Wait-LogPattern $logPath "\[debug\] injecting startup hotkey press" (Get-Date) $TimeoutSeconds)) { + throw "Debug hotkey injection did not start within $TimeoutSeconds seconds." + } + if (-not (Wait-LogPattern $logPath "\[coord\] hotkey pressed" (Get-Date) $TimeoutSeconds)) { + throw "Coordinator did not observe injected hotkey press within $TimeoutSeconds seconds." + } + Write-Host "[ok] Coordinator hotkey path observed without physical keyboard input." +} finally { + Remove-Item Env:OPENLESS_DEBUG_HOTKEY_ON_START -ErrorAction SilentlyContinue + Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force +} + +Write-Host "Hotkey injection smoke passed." diff --git a/openless-all/app/scripts/windows-microphone-privacy-smoke.ps1 b/openless-all/app/scripts/windows-microphone-privacy-smoke.ps1 new file mode 100644 index 00000000..d5d0f0eb --- /dev/null +++ b/openless-all/app/scripts/windows-microphone-privacy-smoke.ps1 @@ -0,0 +1,285 @@ +param( + [string]$ExePath = "", + [int]$TimeoutSeconds = 30, + [int]$VirtualKey = 0xA3 +) + +$ErrorActionPreference = "Stop" + +if ([string]::IsNullOrWhiteSpace($ExePath)) { + $appRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path + $ExePath = Join-Path $appRoot ".artifacts\windows-gnu\dev\openless.exe" +} + +if (-not $env:SystemDrive) { + $env:SystemDrive = "C:" +} +if (-not $env:ProgramData) { + $env:ProgramData = Join-Path $env:SystemDrive "ProgramData" +} + +if (-not (Test-Path $ExePath)) { + throw "OpenLess executable not found: $ExePath" +} + +Add-Type @" +using System; +using System.Runtime.InteropServices; + +public static class OpenLessMicPrivacyWin32 { + [DllImport("user32.dll")] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll")] + public static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern void keybd_event(byte bVk, byte bScan, int dwFlags, UIntPtr dwExtraInfo); + + public const int KEYEVENTF_EXTENDEDKEY = 0x0001; + public const int KEYEVENTF_KEYUP = 0x0002; +} +"@ + +function Read-TextUtf8($Path) { + if (-not (Test-Path $Path)) { + return $null + } + return Get-Content -Raw -Encoding UTF8 $Path +} + +function Write-TextUtf8($Path, $Text) { + $dir = Split-Path $Path -Parent + if (-not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir | Out-Null + } + [System.IO.File]::WriteAllText($Path, $Text, [System.Text.UTF8Encoding]::new($false)) +} + +function Set-HoldHotkeyPreference($Path) { + $previous = Read-TextUtf8 $Path + if ([string]::IsNullOrWhiteSpace($previous)) { + $prefs = [pscustomobject]@{} + } else { + $prefs = $previous | ConvertFrom-Json + } + if ($null -eq $prefs.hotkey) { + $prefs | Add-Member -NotePropertyName hotkey -NotePropertyValue ([pscustomobject]@{}) + } + if ($null -eq $prefs.hotkey.PSObject.Properties["trigger"]) { + $prefs.hotkey | Add-Member -NotePropertyName trigger -NotePropertyValue "rightControl" + } else { + $prefs.hotkey.trigger = "rightControl" + } + if ($null -eq $prefs.hotkey.PSObject.Properties["mode"]) { + $prefs.hotkey | Add-Member -NotePropertyName mode -NotePropertyValue "hold" + } else { + $prefs.hotkey.mode = "hold" + } + if ($null -eq $prefs.defaultMode) { $prefs | Add-Member -NotePropertyName defaultMode -NotePropertyValue "light" } + if ($null -eq $prefs.enabledModes) { $prefs | Add-Member -NotePropertyName enabledModes -NotePropertyValue @("light", "structured", "formal", "raw") } + if ($null -eq $prefs.launchAtLogin) { $prefs | Add-Member -NotePropertyName launchAtLogin -NotePropertyValue $false } + if ($null -eq $prefs.showCapsule) { $prefs | Add-Member -NotePropertyName showCapsule -NotePropertyValue $true } + if ($null -eq $prefs.activeAsrProvider) { $prefs | Add-Member -NotePropertyName activeAsrProvider -NotePropertyValue "volcengine" } + if ($null -eq $prefs.activeLlmProvider) { $prefs | Add-Member -NotePropertyName activeLlmProvider -NotePropertyValue "ark" } + Write-TextUtf8 $Path ($prefs | ConvertTo-Json -Depth 8) + return $previous +} + +function Wait-LogPattern($Path, $Pattern, $TimeoutSeconds) { + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + if (Test-Path $Path) { + $text = Get-Content -Raw $Path + if ($text -match $Pattern) { + return $true + } + } + Start-Sleep -Milliseconds 300 + } + return $false +} + +function Send-KeyEdge($Vk, $KeyUp) { + $flags = [OpenLessMicPrivacyWin32]::KEYEVENTF_EXTENDEDKEY + if ($KeyUp) { + $flags = $flags -bor [OpenLessMicPrivacyWin32]::KEYEVENTF_KEYUP + } + $scanCode = if ($Vk -eq 0xA3 -or $Vk -eq 0xA2) { 0x1D } else { 0 } + [OpenLessMicPrivacyWin32]::keybd_event([byte]$Vk, [byte]$scanCode, $flags, [UIntPtr]::Zero) +} + +function Press-Hotkey { + Send-KeyEdge $VirtualKey $false +} + +function Release-Hotkey { + Send-KeyEdge $VirtualKey $true +} + +function Focus-Window($Process) { + if ($null -eq $Process -or $Process.MainWindowHandle -eq 0) { + return $false + } + [OpenLessMicPrivacyWin32]::ShowWindow($Process.MainWindowHandle, 9) | Out-Null + [OpenLessMicPrivacyWin32]::SetForegroundWindow($Process.MainWindowHandle) | Out-Null + Start-Sleep -Milliseconds 500 + return $true +} + +function Wait-ProcessWindow($ProcessName, $After, $TimeoutSeconds) { + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + $candidates = Get-Process $ProcessName -ErrorAction SilentlyContinue | + Where-Object { $_.StartTime -ge $After -and $_.MainWindowHandle -ne 0 } | + Sort-Object StartTime -Descending + $windowProcess = @($candidates) | Select-Object -First 1 + if ($null -ne $windowProcess) { + return $windowProcess + } + Start-Sleep -Milliseconds 300 + } + return $null +} + +function Get-ConsentSnapshot($Path) { + $exists = Test-Path $Path + $valueExists = $false + $value = $null + if ($exists) { + $props = Get-ItemProperty -LiteralPath $Path -ErrorAction SilentlyContinue + if ($props -and $props.PSObject.Properties["Value"]) { + $valueExists = $true + $value = $props.Value + } + } + [pscustomobject]@{ + Path = $Path + Exists = $exists + ValueExists = $valueExists + Value = $value + } +} + +function Restore-ConsentSnapshot($Snapshot) { + if (-not $Snapshot.Exists) { + Remove-Item -LiteralPath $Snapshot.Path -Recurse -Force -ErrorAction SilentlyContinue + return + } + if (-not (Test-Path $Snapshot.Path)) { + New-Item -ItemType Directory -Path $Snapshot.Path | Out-Null + } + if ($Snapshot.ValueExists) { + Set-ItemProperty -LiteralPath $Snapshot.Path -Name Value -Value $Snapshot.Value + } else { + Remove-ItemProperty -LiteralPath $Snapshot.Path -Name Value -ErrorAction SilentlyContinue + } +} + +function Set-ConsentValue($Path, $Value) { + if (-not (Test-Path $Path)) { + New-Item -ItemType Directory -Path $Path | Out-Null + } + Set-ItemProperty -LiteralPath $Path -Name Value -Value $Value +} + +function Get-NonPackagedConsentPath($ExecutablePath) { + $resolved = (Resolve-Path $ExecutablePath).Path + $encoded = $resolved -replace "\\", "#" + Join-Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone\NonPackaged" $encoded +} + +function Invoke-HotkeyAttempt($ExpectedPattern, $UnexpectedPattern, $Label) { + Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force + Remove-Item -LiteralPath $logPath -Force -ErrorAction SilentlyContinue + + $env:OPENLESS_SHOW_MAIN_ON_START = "1" + $env:OPENLESS_ACCEPT_SYNTHETIC_HOTKEY_EVENTS = "1" + try { + Start-Process -FilePath $ExePath -WorkingDirectory (Split-Path $ExePath -Parent) | Out-Null + } finally { + Remove-Item Env:OPENLESS_SHOW_MAIN_ON_START -ErrorAction SilentlyContinue + Remove-Item Env:OPENLESS_ACCEPT_SYNTHETIC_HOTKEY_EVENTS -ErrorAction SilentlyContinue + } + + $notepad = $null + try { + if (-not (Wait-LogPattern $logPath "WH_KEYBOARD_LL installed" 20)) { + throw "${Label}: Windows low-level keyboard hook was not installed." + } + $notepadStart = Get-Date + Start-Process notepad.exe | Out-Null + $notepad = Wait-ProcessWindow "notepad" $notepadStart 15 + if (-not (Focus-Window $notepad)) { + throw "${Label}: Notepad window could not be focused." + } + $observedPress = $false + for ($attempt = 1; $attempt -le 3 -and -not $observedPress; $attempt++) { + Press-Hotkey + $observedPress = Wait-LogPattern $logPath "\[hotkey\] Windows trigger pressed" 4 + if (-not $observedPress) { + Release-Hotkey + Start-Sleep -Milliseconds 500 + Focus-Window $notepad | Out-Null + } + } + if (-not $observedPress) { + throw "${Label}: Windows low-level hook did not observe the right Control press after retries." + } + Start-Sleep -Milliseconds 900 + Release-Hotkey + + if (-not (Wait-LogPattern $logPath $ExpectedPattern $TimeoutSeconds)) { + throw "${Label}: expected log pattern not observed: $ExpectedPattern" + } + if ($UnexpectedPattern -and (Test-Path $logPath) -and ((Get-Content -Raw $logPath) -match $UnexpectedPattern)) { + throw "${Label}: unexpected log pattern observed: $UnexpectedPattern" + } + } finally { + Release-Hotkey + if ($null -ne $notepad) { + Stop-Process -Id $notepad.Id -Force -ErrorAction SilentlyContinue + } + Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force + } +} + +$logPath = Join-Path $env:LOCALAPPDATA "OpenLess\Logs\openless.log" +$preferencesPath = Join-Path $env:APPDATA "OpenLess\preferences.json" +$previousPreferences = Set-HoldHotkeyPreference $preferencesPath + +$globalMicPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone" +$desktopMicPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone\NonPackaged" +$appMicPath = Get-NonPackagedConsentPath $ExePath +$snapshots = @( + (Get-ConsentSnapshot $globalMicPath), + (Get-ConsentSnapshot $desktopMicPath), + (Get-ConsentSnapshot $appMicPath) +) + +Write-Host "== Windows microphone privacy smoke ==" +try { + Set-ConsentValue $globalMicPath "Deny" + Set-ConsentValue $desktopMicPath "Deny" + Set-ConsentValue $appMicPath "Deny" + Invoke-HotkeyAttempt "microphone permission gate failed|input probe failed" "\[coord\] session started" "privacy denied" + Write-Host "[ok] Denied state blocks recording before session start." + + Set-ConsentValue $globalMicPath "Allow" + Set-ConsentValue $desktopMicPath "Allow" + Set-ConsentValue $appMicPath "Allow" + Invoke-HotkeyAttempt "\[coord\] session started" "microphone permission gate failed" "privacy restored" + Write-Host "[ok] Restored state allows recording session start." +} finally { + foreach ($snapshot in $snapshots) { + Restore-ConsentSnapshot $snapshot + } + if ($null -eq $previousPreferences) { + Remove-Item -LiteralPath $preferencesPath -Force -ErrorAction SilentlyContinue + } else { + Write-TextUtf8 $preferencesPath $previousPreferences + } + Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force +} + +Write-Host "Windows microphone privacy smoke passed." diff --git a/openless-all/app/scripts/windows-real-asr-insertion-smoke.ps1 b/openless-all/app/scripts/windows-real-asr-insertion-smoke.ps1 new file mode 100644 index 00000000..d7979b3c --- /dev/null +++ b/openless-all/app/scripts/windows-real-asr-insertion-smoke.ps1 @@ -0,0 +1,433 @@ +param( + [string]$ExePath = "", + [ValidateSet("notepad", "browser")] + [string]$Target = "notepad", + [string]$Phrase = "OpenLess Windows real regression", + [int]$TimeoutSeconds = 120, + [int]$VirtualKey = 0xA3, + [switch]$DebugHotkeyEvents +) + +$ErrorActionPreference = "Stop" + +if ([string]::IsNullOrWhiteSpace($ExePath)) { + $appRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path + $ExePath = Join-Path $appRoot ".artifacts\windows-gnu\dev\openless.exe" +} + +if (-not $env:SystemDrive) { + $env:SystemDrive = "C:" +} +if (-not $env:ProgramData) { + $env:ProgramData = Join-Path $env:SystemDrive "ProgramData" +} + +if (-not (Test-Path $ExePath)) { + throw "OpenLess executable not found: $ExePath" +} + +Add-Type @" +using System; +using System.Runtime.InteropServices; + +public static class OpenLessRegressionWin32 { + [DllImport("user32.dll")] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll")] + public static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern void keybd_event(byte bVk, byte bScan, int dwFlags, UIntPtr dwExtraInfo); + + public const int KEYEVENTF_EXTENDEDKEY = 0x0001; + public const int KEYEVENTF_KEYUP = 0x0002; +} +"@ + +function Test-CredentialValue($Value) { + return ($null -ne $Value) -and ($Value -is [string]) -and ($Value.Trim().Length -gt 0) +} + +function Get-OpenLessCredentialStatus { + $path = Join-Path $env:APPDATA "OpenLess\credentials.json" + if (-not (Test-Path $path)) { + return [pscustomobject]@{ Path = $path; Present = $false; VolcengineConfigured = $false; ArkConfigured = $false } + } + + $json = Get-Content -Raw $path | ConvertFrom-Json + $asr = $json.providers.asr.volcengine + $llm = $json.providers.llm.ark + [pscustomobject]@{ + Path = $path + Present = $true + VolcengineConfigured = (Test-CredentialValue $asr.appKey) -and (Test-CredentialValue $asr.accessKey) + ArkConfigured = Test-CredentialValue $llm.apiKey + } +} + +function Read-TextUtf8($Path) { + if (-not (Test-Path $Path)) { + return $null + } + return Get-Content -Raw -Encoding UTF8 $Path +} + +function Write-TextUtf8($Path, $Text) { + $dir = Split-Path $Path -Parent + if (-not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir | Out-Null + } + [System.IO.File]::WriteAllText($Path, $Text, [System.Text.UTF8Encoding]::new($false)) +} + +function Set-HoldHotkeyPreference($Path) { + $previous = Read-TextUtf8 $Path + if ([string]::IsNullOrWhiteSpace($previous)) { + $prefs = [pscustomobject]@{} + } else { + $prefs = $previous | ConvertFrom-Json + } + if ($null -eq $prefs.hotkey) { + $prefs | Add-Member -NotePropertyName hotkey -NotePropertyValue ([pscustomobject]@{}) + } + if ($null -eq $prefs.hotkey.PSObject.Properties["trigger"]) { + $prefs.hotkey | Add-Member -NotePropertyName trigger -NotePropertyValue "rightControl" + } else { + $prefs.hotkey.trigger = "rightControl" + } + if ($null -eq $prefs.hotkey.PSObject.Properties["mode"]) { + $prefs.hotkey | Add-Member -NotePropertyName mode -NotePropertyValue "hold" + } else { + $prefs.hotkey.mode = "hold" + } + if ($null -eq $prefs.defaultMode) { $prefs | Add-Member -NotePropertyName defaultMode -NotePropertyValue "light" } + if ($null -eq $prefs.enabledModes) { $prefs | Add-Member -NotePropertyName enabledModes -NotePropertyValue @("light", "structured", "formal", "raw") } + if ($null -eq $prefs.launchAtLogin) { $prefs | Add-Member -NotePropertyName launchAtLogin -NotePropertyValue $false } + if ($null -eq $prefs.showCapsule) { $prefs | Add-Member -NotePropertyName showCapsule -NotePropertyValue $true } + if ($null -eq $prefs.activeAsrProvider) { $prefs | Add-Member -NotePropertyName activeAsrProvider -NotePropertyValue "volcengine" } + if ($null -eq $prefs.activeLlmProvider) { $prefs | Add-Member -NotePropertyName activeLlmProvider -NotePropertyValue "ark" } + Write-TextUtf8 $Path ($prefs | ConvertTo-Json -Depth 8) + return $previous +} + +function Wait-LogPattern($Path, $Pattern, $TimeoutSeconds) { + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + if (Test-Path $Path) { + $text = Get-Content -Raw $Path + if ($text -match $Pattern) { + return $true + } + } + Start-Sleep -Milliseconds 300 + } + return $false +} + +function Get-HistoryCount($Path) { + if (-not (Test-Path $Path)) { + return 0 + } + $json = Get-Content -Raw -Encoding UTF8 $Path | ConvertFrom-Json + if ($null -eq $json) { + return 0 + } + return @($json).Count +} + +function Get-LatestHistory($Path) { + if (-not (Test-Path $Path)) { + return $null + } + $json = Get-Content -Raw -Encoding UTF8 $Path | ConvertFrom-Json + return @($json) | Select-Object -First 1 +} + +function Wait-HistoryCountGreaterThan($Path, $Baseline, $TimeoutSeconds) { + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + $count = Get-HistoryCount $Path + if ($count -gt $Baseline) { + return $true + } + Start-Sleep -Milliseconds 500 + } + return $false +} + +function Send-KeyEdge($Vk, $KeyUp, $Extended = $true) { + $flags = 0 + if ($Extended) { + $flags = $flags -bor [OpenLessRegressionWin32]::KEYEVENTF_EXTENDEDKEY + } + if ($KeyUp) { + $flags = $flags -bor [OpenLessRegressionWin32]::KEYEVENTF_KEYUP + } + $scanCode = if ($Vk -eq 0xA3 -or $Vk -eq 0xA2) { 0x1D } else { 0 } + [OpenLessRegressionWin32]::keybd_event([byte]$Vk, [byte]$scanCode, $flags, [UIntPtr]::Zero) +} + +function Tap-Hotkey { + Send-KeyEdge $VirtualKey $false $true + Start-Sleep -Milliseconds 180 + Send-KeyEdge $VirtualKey $true $true +} + +function Press-Hotkey { + Send-KeyEdge $VirtualKey $false $true +} + +function Release-Hotkey { + Send-KeyEdge $VirtualKey $true $true +} + +function Focus-Window($Process) { + if ($null -eq $Process -or $Process.MainWindowHandle -eq 0) { + return $false + } + [OpenLessRegressionWin32]::ShowWindow($Process.MainWindowHandle, 9) | Out-Null + [OpenLessRegressionWin32]::SetForegroundWindow($Process.MainWindowHandle) | Out-Null + Start-Sleep -Milliseconds 500 + return $true +} + +function Wait-ProcessWindow($ProcessName, $After, $TimeoutSeconds) { + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + $candidates = Get-Process $ProcessName -ErrorAction SilentlyContinue | + Where-Object { $_.StartTime -ge $After -and $_.MainWindowHandle -ne 0 } | + Sort-Object StartTime -Descending + $windowProcess = @($candidates) | Select-Object -First 1 + if ($null -ne $windowProcess) { + return $windowProcess + } + Start-Sleep -Milliseconds 300 + } + return $null +} + +function Resolve-BrowserPath { + $programFiles = if ($env:ProgramFiles) { $env:ProgramFiles } else { Join-Path $env:SystemDrive "Program Files" } + $programFilesX86 = if (${env:ProgramFiles(x86)}) { ${env:ProgramFiles(x86)} } else { Join-Path $env:SystemDrive "Program Files (x86)" } + $roots = @( + $programFilesX86, + $programFiles, + (Join-Path $env:LOCALAPPDATA "Microsoft\Edge\Application"), + (Join-Path $env:LOCALAPPDATA "Google\Chrome\Application"), + (Join-Path $env:LOCALAPPDATA "BraveSoftware\Brave-Browser\Application") + ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + $candidates = @() + foreach ($root in $roots) { + $candidates += Join-Path $root "Microsoft\Edge\Application\msedge.exe" + $candidates += Join-Path $root "Google\Chrome\Application\chrome.exe" + $candidates += Join-Path $root "BraveSoftware\Brave-Browser\Application\brave.exe" + $candidates += Join-Path $root "msedge.exe" + $candidates += Join-Path $root "chrome.exe" + $candidates += Join-Path $root "brave.exe" + } + foreach ($candidate in $candidates) { + if ($candidate -and (Test-Path $candidate)) { + return $candidate + } + } + throw "Neither Microsoft Edge nor Google Chrome was found." +} + +function New-BrowserInputFixture { + $path = Join-Path $env:TEMP "openless-browser-input-fixture.html" + $html = @" + + + + + OpenLess Browser Input Fixture + + + + + + + +"@ + Write-TextUtf8 $path $html + return $path +} + +function Stop-BrowserProfileProcesses($ProfilePath) { + if ([string]::IsNullOrWhiteSpace($ProfilePath)) { + return + } + $escaped = [Regex]::Escape($ProfilePath) + $processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | + Where-Object { $_.CommandLine -match "--user-data-dir=`"?$escaped`"?" } + foreach ($process in $processes) { + Stop-Process -Id $process.ProcessId -Force -ErrorAction SilentlyContinue + } +} + +function Start-InputTarget($TargetName) { + $startedAt = Get-Date + if ($TargetName -eq "notepad") { + Start-Process notepad.exe | Out-Null + $process = Wait-ProcessWindow "notepad" $startedAt 15 + if (-not (Focus-Window $process)) { + throw "Notepad window could not be focused." + } + return [pscustomobject]@{ Process = $process; FixturePath = $null; ProfilePath = $null } + } + + $browserPath = Resolve-BrowserPath + $fixture = New-BrowserInputFixture + $url = ([System.Uri]$fixture).AbsoluteUri + $processName = [System.IO.Path]::GetFileNameWithoutExtension($browserPath) + $profilePath = Join-Path $env:TEMP "openless-browser-smoke-profile" + Stop-BrowserProfileProcesses $profilePath + Remove-Item -LiteralPath $profilePath -Recurse -Force -ErrorAction SilentlyContinue + Start-Process -FilePath $browserPath -ArgumentList @( + "--new-window", + "--user-data-dir=$profilePath", + "--no-first-run", + "--disable-extensions", + $url + ) | Out-Null + $process = Wait-ProcessWindow $processName $startedAt 20 + if (-not (Focus-Window $process)) { + throw "Browser window could not be focused." + } + Start-Sleep -Seconds 1 + return [pscustomobject]@{ Process = $process; FixturePath = $fixture; ProfilePath = $profilePath } +} + +function Send-CtrlChord($Vk) { + Send-KeyEdge 0xA2 $false $false + Start-Sleep -Milliseconds 80 + Send-KeyEdge $Vk $false $false + Start-Sleep -Milliseconds 80 + Send-KeyEdge $Vk $true $false + Start-Sleep -Milliseconds 80 + Send-KeyEdge 0xA2 $true $false +} + +function Speak-TestPhrase($Text) { + Add-Type -AssemblyName System.Speech + $speaker = New-Object System.Speech.Synthesis.SpeechSynthesizer + $speaker.Rate = -1 + $speaker.Volume = 100 + $speaker.Speak($Text) +} + +$credentialStatus = Get-OpenLessCredentialStatus +if (-not $credentialStatus.VolcengineConfigured -or -not $credentialStatus.ArkConfigured) { + throw "Real ASR regression requires configured Volcengine ASR and Ark LLM credentials." +} + +$logPath = Join-Path $env:LOCALAPPDATA "OpenLess\Logs\openless.log" +$historyPath = Join-Path $env:APPDATA "OpenLess\history.json" +$preferencesPath = Join-Path $env:APPDATA "OpenLess\preferences.json" +$baselineCount = Get-HistoryCount $historyPath +$previousPreferences = Set-HoldHotkeyPreference $preferencesPath + +Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force +Remove-Item -LiteralPath $logPath -Force -ErrorAction SilentlyContinue + +Write-Host "== Real ASR + insertion fallback smoke ($Target) ==" +$env:OPENLESS_SHOW_MAIN_ON_START = "1" +$env:OPENLESS_ACCEPT_SYNTHETIC_HOTKEY_EVENTS = "1" +if ($DebugHotkeyEvents) { + $env:OPENLESS_DEBUG_HOTKEY_EVENTS = "1" +} +try { + $openless = Start-Process -FilePath $ExePath -WorkingDirectory (Split-Path $ExePath -Parent) -PassThru +} finally { + Remove-Item Env:OPENLESS_SHOW_MAIN_ON_START -ErrorAction SilentlyContinue + Remove-Item Env:OPENLESS_ACCEPT_SYNTHETIC_HOTKEY_EVENTS -ErrorAction SilentlyContinue + Remove-Item Env:OPENLESS_DEBUG_HOTKEY_EVENTS -ErrorAction SilentlyContinue +} + +$inputTarget = $null +try { + if (-not (Wait-LogPattern $logPath "WH_KEYBOARD_LL installed" 20)) { + throw "Windows low-level keyboard hook was not installed." + } + + $inputTarget = Start-InputTarget $Target + + Press-Hotkey + if (-not (Wait-LogPattern $logPath "\[hotkey\] Windows trigger pressed" 10)) { + throw "Windows low-level hook did not observe the right Control press." + } + if (-not (Wait-LogPattern $logPath "\[coord\] session started" 30)) { + throw "OpenLess recording session did not start." + } + + Speak-TestPhrase $Phrase + Start-Sleep -Milliseconds 800 + Release-Hotkey + + if (-not (Wait-HistoryCountGreaterThan $historyPath $baselineCount $TimeoutSeconds)) { + throw "History did not receive a new dictation session within $TimeoutSeconds seconds." + } + + $latest = Get-LatestHistory $historyPath + if ($null -eq $latest) { + throw "History changed but latest item could not be read." + } + if ($latest.errorCode -eq "emptyTranscript") { + throw "ASR returned an empty transcript. Hotkey, recorder, ASR session, history, and error status were exercised; real transcription still needs a microphone/audio route that captures the spoken phrase." + } + if ([string]::IsNullOrWhiteSpace($latest.rawTranscript) -or [string]::IsNullOrWhiteSpace($latest.finalText)) { + throw "Latest history item is missing rawTranscript or finalText." + } + if ($latest.insertStatus -ne "copiedFallback") { + throw "Expected Windows insertStatus copiedFallback, got '$($latest.insertStatus)'." + } + + Focus-Window $inputTarget.Process | Out-Null + Start-Sleep -Milliseconds 400 + Send-CtrlChord 0x41 + Start-Sleep -Milliseconds 200 + Send-CtrlChord 0x43 + Start-Sleep -Milliseconds 400 + $targetText = Get-Clipboard -Raw -ErrorAction SilentlyContinue + + if ([string]::IsNullOrWhiteSpace($targetText)) { + throw "$Target clipboard readback is empty after Ctrl+A/C." + } + + Write-Host "[ok] History updated. raw='$($latest.rawTranscript)'" + Write-Host "[ok] Final text length=$($latest.finalText.Length), insertStatus=$($latest.insertStatus)" + Write-Host "[ok] $Target readback length=$($targetText.Length)" +} finally { + Release-Hotkey + if ($null -ne $inputTarget) { + if ($inputTarget.ProfilePath) { + Stop-BrowserProfileProcesses $inputTarget.ProfilePath + } else { + Stop-Process -Id $inputTarget.Process.Id -Force -ErrorAction SilentlyContinue + } + if ($inputTarget.FixturePath) { + Remove-Item -LiteralPath $inputTarget.FixturePath -Force -ErrorAction SilentlyContinue + } + if ($inputTarget.ProfilePath) { + Remove-Item -LiteralPath $inputTarget.ProfilePath -Recurse -Force -ErrorAction SilentlyContinue + } + } + Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force + if ($null -eq $previousPreferences) { + Remove-Item -LiteralPath $preferencesPath -Force -ErrorAction SilentlyContinue + } else { + Write-TextUtf8 $preferencesPath $previousPreferences + } +} + +Write-Host "Real ASR + insertion fallback smoke ($Target) passed." diff --git a/openless-all/app/scripts/windows-real-regression.ps1 b/openless-all/app/scripts/windows-real-regression.ps1 new file mode 100644 index 00000000..aeaafa97 --- /dev/null +++ b/openless-all/app/scripts/windows-real-regression.ps1 @@ -0,0 +1,147 @@ +param( + [string]$ExePath = "", + [int]$StartupTimeoutSeconds = 12, + [int]$PhysicalHotkeyTimeoutSeconds = 45, + [switch]$RequireCredentials, + [switch]$PhysicalHotkey, + [switch]$InsertionFallback, + [switch]$MicrophonePrivacy +) + +$ErrorActionPreference = "Stop" + +if ([string]::IsNullOrWhiteSpace($ExePath)) { + $appRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path + $ExePath = Join-Path $appRoot ".artifacts\windows-gnu\dev\openless.exe" +} + +if (-not $env:SystemDrive) { + $env:SystemDrive = "C:" +} +if (-not $env:ProgramData) { + $env:ProgramData = Join-Path $env:SystemDrive "ProgramData" +} + +function Test-CredentialValue($Value) { + return ($null -ne $Value) -and ($Value -is [string]) -and ($Value.Trim().Length -gt 0) +} + +function Get-OpenLessCredentialStatus { + $path = Join-Path $env:APPDATA "OpenLess\credentials.json" + if (-not (Test-Path $path)) { + return [pscustomobject]@{ + Path = $path + Present = $false + VolcengineConfigured = $false + ArkConfigured = $false + } + } + + $json = Get-Content -Raw $path | ConvertFrom-Json + $asr = $json.providers.asr.volcengine + $llm = $json.providers.llm.ark + [pscustomobject]@{ + Path = $path + Present = $true + VolcengineConfigured = (Test-CredentialValue $asr.appKey) -and (Test-CredentialValue $asr.accessKey) + ArkConfigured = Test-CredentialValue $llm.apiKey + } +} + +function Wait-LogPattern($Path, $Pattern, $TimeoutSeconds) { + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + if ((Test-Path $Path) -and ((Get-Content -Raw $Path) -match $Pattern)) { + return $true + } + Start-Sleep -Milliseconds 500 + } + return $false +} + +function Wait-HistoryChange($Path, $BaselineWriteTime, $TimeoutSeconds) { + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + if ((Test-Path $Path)) { + $current = (Get-Item $Path).LastWriteTimeUtc + if ($null -eq $BaselineWriteTime -or $current -gt $BaselineWriteTime) { + return $true + } + } + Start-Sleep -Milliseconds 500 + } + return $false +} + +if (-not (Test-Path $ExePath)) { + throw "OpenLess executable not found: $ExePath" +} + +$logPath = Join-Path $env:LOCALAPPDATA "OpenLess\Logs\openless.log" +$historyPath = Join-Path $env:APPDATA "OpenLess\history.json" +$credentialStatus = Get-OpenLessCredentialStatus + +Write-Host "== Credential gate ==" +$credentialStatus | Format-List +if ($RequireCredentials -and (-not $credentialStatus.VolcengineConfigured -or -not $credentialStatus.ArkConfigured)) { + throw "Real regression requires configured Volcengine ASR and Ark LLM credentials." +} + +Write-Host "" +Write-Host "== Launch gate ==" +$process = Start-Process -FilePath $ExePath -PassThru +try { + Start-Sleep -Seconds 4 + $live = Get-Process -Id $process.Id -ErrorAction SilentlyContinue + if (-not $live) { + throw "OpenLess exited during startup." + } + if (-not $live.Responding) { + throw "OpenLess process is not responding." + } + Write-Host "[ok] Process responding: id=$($live.Id), title='$($live.MainWindowTitle)'" + + if (Wait-LogPattern $logPath "hotkey listener installed" $StartupTimeoutSeconds) { + Write-Host "[ok] Hotkey listener installed according to log." + } else { + throw "Hotkey listener did not report installed within $StartupTimeoutSeconds seconds." + } + + if ($PhysicalHotkey) { + Write-Host "" + Write-Host "== Physical hotkey gate ==" + Write-Host "Press the configured physical OpenLess hotkey now. Synthetic SendInput is not accepted for this gate." + if (-not (Wait-LogPattern $logPath "\[coord\] hotkey pressed" $PhysicalHotkeyTimeoutSeconds)) { + throw "No physical hotkey press was observed in the log within $PhysicalHotkeyTimeoutSeconds seconds." + } + Write-Host "[ok] Physical hotkey press observed." + } + + if ($InsertionFallback) { + Write-Host "" + Write-Host "== Insertion fallback gate ==" + $baseline = $null + if (Test-Path $historyPath) { + $baseline = (Get-Item $historyPath).LastWriteTimeUtc + } + $notepad = Start-Process notepad.exe -PassThru + Write-Host "Notepad launched. Focus the edit area, use the physical hotkey, speak a short phrase, then finish recording." + if (-not (Wait-HistoryChange $historyPath $baseline 120)) { + throw "History did not change within 120 seconds after manual recording." + } + Write-Host "[ok] History changed after manual recording. Inspect the capsule/history insert status for inserted vs copiedFallback." + Stop-Process -Id $notepad.Id -Force -ErrorAction SilentlyContinue + } + + if ($MicrophonePrivacy) { + Write-Host "" + Write-Host "== Microphone privacy gate ==" + Start-Process "ms-settings:privacy-microphone" + Write-Host "Toggle microphone privacy off, return to OpenLess Settings -> Permissions, confirm it no longer reports granted, then toggle it back on and rerun this script." + } +} finally { + Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force +} + +Write-Host "" +Write-Host "Windows real regression script completed." diff --git a/openless-all/app/scripts/windows-smoke-suite.ps1 b/openless-all/app/scripts/windows-smoke-suite.ps1 new file mode 100644 index 00000000..9e37be51 --- /dev/null +++ b/openless-all/app/scripts/windows-smoke-suite.ps1 @@ -0,0 +1,134 @@ +param( + [string]$ExePath = "", + [string]$Phrase = "OpenLess Windows regression suite phrase. OpenLess Windows regression suite phrase.", + [ValidateSet("notepad", "browser")] + [string[]]$Targets = @("notepad", "browser"), + [switch]$Build, + [switch]$SkipRuntime, + [switch]$SkipHotkey, + [switch]$SkipRealAsr, + [switch]$SkipPrivacy, + [switch]$DebugHotkeyEvents +) + +$ErrorActionPreference = "Stop" + +$appRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +if ([string]::IsNullOrWhiteSpace($ExePath)) { + $ExePath = Join-Path $appRoot ".artifacts\windows-gnu\dev\openless.exe" +} + +if (-not $env:SystemDrive) { + $env:SystemDrive = "C:" +} +if (-not $env:ProgramData) { + $env:ProgramData = Join-Path $env:SystemDrive "ProgramData" +} + +function Invoke-Step($Name, [scriptblock]$Block) { + Write-Host "" + Write-Host "== $Name ==" + $start = Get-Date + try { + & $Block + $elapsed = [int]((Get-Date) - $start).TotalSeconds + Write-Host "[ok] $Name (${elapsed}s)" + } catch { + $elapsed = [int]((Get-Date) - $start).TotalSeconds + Write-Host "[fail] $Name (${elapsed}s)" + throw + } +} + +function Invoke-Script($Path, [hashtable]$Parameters = @{}) { + $resolved = (Resolve-Path $Path).Path + & $resolved @Parameters +} + +function Test-PowerShellSyntax($Path) { + $tokens = $null + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path $Path).Path, + [ref]$tokens, + [ref]$errors + ) | Out-Null + if ($errors.Count) { + throw ($errors | Out-String) + } +} + +$scriptsToParse = @( + "windows-runtime-smoke.ps1", + "windows-hotkey-os-hook-smoke.ps1", + "windows-real-asr-insertion-smoke.ps1", + "windows-microphone-privacy-smoke.ps1" +) + +Write-Host "OpenLess Windows smoke suite" +Write-Host "appRoot=$appRoot" +Write-Host "exe=$ExePath" + +try { + Invoke-Step "PowerShell syntax" { + foreach ($script in $scriptsToParse) { + Test-PowerShellSyntax (Join-Path $PSScriptRoot $script) + } + } + + if ($Build) { + Invoke-Step "Windows GNU build" { + Invoke-Script (Join-Path $PSScriptRoot "windows-build-gnu.ps1") + } + } + + if (-not (Test-Path $ExePath)) { + throw "OpenLess executable not found: $ExePath. Run with -Build or run scripts/windows-build-gnu.ps1 first." + } + + if (-not $SkipRuntime) { + Invoke-Step "Runtime smoke" { + Invoke-Script (Join-Path $PSScriptRoot "windows-runtime-smoke.ps1") @{ + ExePath = $ExePath + RequireCredentials = $true + } + } + } + + if (-not $SkipHotkey) { + Invoke-Step "OS hotkey smoke" { + Invoke-Script (Join-Path $PSScriptRoot "windows-hotkey-os-hook-smoke.ps1") @{ + ExePath = $ExePath + } + } + } + + if (-not $SkipRealAsr) { + foreach ($target in $Targets) { + Invoke-Step "Real ASR insertion fallback: $target" { + $parameters = @{ + ExePath = $ExePath + Target = $target + Phrase = $Phrase + } + if ($DebugHotkeyEvents) { + $parameters.DebugHotkeyEvents = $true + } + Invoke-Script (Join-Path $PSScriptRoot "windows-real-asr-insertion-smoke.ps1") $parameters + } + } + } + + if (-not $SkipPrivacy) { + Invoke-Step "Microphone privacy deny/restore" { + Invoke-Script (Join-Path $PSScriptRoot "windows-microphone-privacy-smoke.ps1") @{ + ExePath = $ExePath + } + } + } +} finally { + Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force +} + +Write-Host "" +Write-Host "Windows smoke suite passed."