test(windows): 增加真机回归 smoke 矩阵 (#73)

Co-authored-by: Cooper-X-Oak <Cooper-X-Oak@users.noreply.github.com>
This commit is contained in:
Cooper OakandCooper-X-Oak authored and GitHub committed 2026-04-30 14:19:29 +08:00
1 parent b8988c3330
commit 96ff66eb80
5 files changed
+1059

No files matched your search

@@ -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."
@@ -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."
@@ -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 = @"
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>OpenLess Browser Input Fixture</title>
<style>
body { font: 16px system-ui, sans-serif; margin: 32px; }
textarea { width: 720px; height: 220px; font: 18px Consolas, monospace; }
</style>
</head>
<body>
<textarea id="target" autofocus></textarea>
<script>
const target = document.getElementById('target');
target.focus();
target.select();
window.addEventListener('focus', () => target.focus());
document.body.addEventListener('click', () => target.focus());
</script>
</body>
</html>
"@
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."
@@ -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."
@@ -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."