test(windows): 增加真机回归 smoke 矩阵#73
Conversation
Reviewer's GuideAdds a Windows real-device regression smoke matrix implemented as PowerShell scripts that exercise runtime startup, hotkey injection, real ASR insertion into Notepad/browser with fallback, microphone privacy registry gates, and a top-level suite/orchestrator for these checks. Sequence diagram for real ASR insertion fallback smoke scriptsequenceDiagram
participant RealAsrScript as windows_real_asr_insertion_smoke_ps1
participant OpenLess as openless_exe
participant WinHook as WH_KEYBOARD_LL
participant InputApp as Notepad_or_Browser
participant Speech as System_Speech_Synthesizer
participant History as history_json
participant Log as openless_log
participant Clipboard as Clipboard
RealAsrScript->>RealAsrScript: Get_OpenLessCredentialStatus
RealAsrScript->>RealAsrScript: Set_HoldHotkeyPreference
RealAsrScript->>OpenLess: Start_Process with env flags
OpenLess->>WinHook: install low_level_keyboard_hook
WinHook-->>Log: write WH_KEYBOARD_LL installed
RealAsrScript->>Log: Wait_LogPattern WH_KEYBOARD_LL installed
RealAsrScript->>InputApp: Start_InputTarget(Target)
RealAsrScript->>WinHook: Press_Hotkey (synthetic keybd_event)
WinHook-->>Log: write [hotkey] Windows trigger pressed
RealAsrScript->>Log: Wait_LogPattern [hotkey] Windows trigger pressed
OpenLess->>Log: write [coord] session started
RealAsrScript->>Log: Wait_LogPattern [coord] session started
RealAsrScript->>Speech: Speak_TestPhrase(Phrase)
Speech-->>OpenLess: audio via microphone route
RealAsrScript->>WinHook: Release_Hotkey
OpenLess->>History: append new dictation entry
RealAsrScript->>History: Wait_HistoryCountGreaterThan(baseline)
RealAsrScript->>History: Get_LatestHistory
RealAsrScript->>RealAsrScript: Assert rawTranscript, finalText, insertStatus=copiedFallback
RealAsrScript->>InputApp: Focus_Window
RealAsrScript->>InputApp: Send_CtrlChord(A) then C
InputApp->>Clipboard: set text from editable area
RealAsrScript->>Clipboard: Get_Clipboard
RealAsrScript->>RealAsrScript: Assert clipboard text non_empty
RealAsrScript->>OpenLess: Stop_Process
RealAsrScript->>RealAsrScript: Restore previous preferences_json
Sequence diagram for Windows microphone privacy smoke scriptsequenceDiagram
participant MicScript as windows_microphone_privacy_smoke_ps1
participant Registry as CapabilityAccessManager_registry
participant OpenLess as openless_exe
participant WinHook as WH_KEYBOARD_LL
participant Log as openless_log
participant Notepad as notepad_exe
MicScript->>Registry: Get_ConsentSnapshot(global, desktop, app)
MicScript->>MicScript: Set_HoldHotkeyPreference
MicScript->>Registry: Set global, desktop, app Value=Deny
loop privacy_denied_attempt
MicScript->>OpenLess: Start_Process with ACCEPT_SYNTHETIC_HOTKEY
OpenLess->>WinHook: install low_level_keyboard_hook
WinHook-->>Log: write WH_KEYBOARD_LL installed
MicScript->>Log: Wait_LogPattern WH_KEYBOARD_LL installed
MicScript->>Notepad: Start_Process and focus
MicScript->>WinHook: Press_Hotkey (synthetic)
WinHook-->>Log: write [hotkey] Windows trigger pressed
MicScript->>WinHook: Release_Hotkey
OpenLess-->>Log: write microphone permission gate failed or input probe failed
MicScript->>Log: Wait_LogPattern ExpectedPattern and assert no session_started
MicScript->>OpenLess: Stop_Process
end
MicScript->>Registry: Set global, desktop, app Value=Allow
loop privacy_restored_attempt
MicScript->>OpenLess: Start_Process with ACCEPT_SYNTHETIC_HOTKEY
OpenLess->>WinHook: install low_level_keyboard_hook
WinHook-->>Log: write WH_KEYBOARD_LL installed
MicScript->>Log: Wait_LogPattern WH_KEYBOARD_LL installed
MicScript->>Notepad: Start_Process and focus
MicScript->>WinHook: Press_Hotkey (synthetic)
WinHook-->>Log: write [hotkey] Windows trigger pressed
MicScript->>WinHook: Release_Hotkey
OpenLess-->>Log: write [coord] session started
MicScript->>Log: Wait_LogPattern [coord] session started and assert no permission_gate_failure
MicScript->>OpenLess: Stop_Process
end
MicScript->>Registry: Restore_ConsentSnapshot for global, desktop, app
MicScript->>MicScript: Restore preferences_json
Sequence diagram for hotkey injection smoke without physical keyboardsequenceDiagram
participant InjectScript as windows_hotkey_injection_smoke_ps1
participant OpenLess as openless_exe
participant WinHook as WH_KEYBOARD_LL
participant Log as openless_log
InjectScript->>InjectScript: Resolve ExePath
InjectScript->>InjectScript: Kill existing openless, delete log
InjectScript->>OpenLess: Start_Process with OPENLESS_DEBUG_HOTKEY_ON_START=1
OpenLess->>WinHook: install low_level_keyboard_hook
OpenLess-->>Log: write [debug] injecting startup hotkey press
OpenLess-->>Log: write [coord] hotkey pressed
InjectScript->>Log: Wait_LogPattern [debug] injecting startup hotkey press
InjectScript->>Log: Wait_LogPattern [coord] hotkey pressed
InjectScript->>OpenLess: Stop_Process
Flow diagram for windows smoke suite orchestratorflowchart TD
Start([Start windows_smoke_suite_ps1])
Init["Resolve appRoot and ExePath
Ensure SystemDrive and ProgramData"]
ParseStep["Invoke-Step PowerShell syntax
Parse windows-*.ps1 scripts"]
BuildCheck{Build switch?}
BuildStep["Invoke-Step Windows GNU build
Call windows-build-gnu.ps1"]
CheckExe{ExePath exists?}
RuntimeCheck{SkipRuntime switch?}
RuntimeStep["Invoke-Step Runtime smoke
windows-runtime-smoke.ps1 -RequireCredentials"]
HotkeyCheck{SkipHotkey switch?}
HotkeyStep["Invoke-Step OS hotkey smoke
windows-hotkey-os-hook-smoke.ps1"]
RealAsrCheck{SkipRealAsr switch?}
ForEachTarget[[For each Target in Targets]]
RealAsrStep["Invoke-Step Real ASR insertion fallback
windows-real-asr-insertion-smoke.ps1
with ExePath, Target, Phrase, DebugHotkeyEvents"]
PrivacyCheck{SkipPrivacy switch?}
PrivacyStep["Invoke-Step Microphone privacy deny/restore
windows-microphone-privacy-smoke.ps1"]
StopOpenLess["Stop all openless processes"]
End([Windows smoke suite passed])
Start --> Init --> ParseStep --> BuildCheck
BuildCheck -- yes --> BuildStep --> CheckExe
BuildCheck -- no --> CheckExe
CheckExe -- no --> ErrExe[[Error: openless.exe not found]]
CheckExe -- yes --> RuntimeCheck
RuntimeCheck -- no_skip --> RuntimeStep --> HotkeyCheck
RuntimeCheck -- skip --> HotkeyCheck
HotkeyCheck -- no_skip --> HotkeyStep --> RealAsrCheck
HotkeyCheck -- skip --> RealAsrCheck
RealAsrCheck -- no_skip --> ForEachTarget --> RealAsrStep --> PrivacyCheck
RealAsrCheck -- skip --> PrivacyCheck
PrivacyCheck -- no_skip --> PrivacyStep --> StopOpenLess --> End
PrivacyCheck -- skip --> StopOpenLess --> End
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- Several helper functions like
Set-HoldHotkeyPreference,Read-TextUtf8, andWait-LogPatternare duplicated across scripts; consider extracting them into a shared helper module to reduce repetition and keep behavior consistent. - In
windows-hotkey-injection-smoke.ps1, theWait-LogPatternfunction takes a$Sinceargument that is never used; either incorporate it into the filtering logic or remove the parameter to avoid confusion.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Several helper functions like `Set-HoldHotkeyPreference`, `Read-TextUtf8`, and `Wait-LogPattern` are duplicated across scripts; consider extracting them into a shared helper module to reduce repetition and keep behavior consistent.
- In `windows-hotkey-injection-smoke.ps1`, the `Wait-LogPattern` function takes a `$Since` argument that is never used; either incorporate it into the filtering logic or remove the parameter to avoid confusion.
## Individual Comments
### Comment 1
<location path="openless-all/app/scripts/windows-hotkey-injection-smoke.ps1" line_range="20-29" />
<code_context>
+ $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
+}
+
</code_context>
<issue_to_address>
**issue (bug_risk):** The `$Since` parameter is unused in `Wait-LogPattern`, which can be misleading and suggests incomplete filtering logic.
Since `$Since` is never read and the function always tails the last 200 lines, callers can’t rely on it to avoid matching older log entries. Consider either removing `$Since` from the signature or implementing actual since-based filtering (e.g., by tracking timestamps or line offsets).
</issue_to_address>
### Comment 2
<location path="openless-all/app/scripts/windows-microphone-privacy-smoke.ps1" line_range="164-173" />
<code_context>
+ }
+}
+
+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
</code_context>
<issue_to_address>
**suggestion:** Registry restoration uses `New-Item -ItemType Directory` on HKCU paths, which works but is semantically off for registry keys.
In the registry provider this happens to create a key, but it’s misleading and relies on provider-specific behavior. Prefer `New-Item -ItemType Key -Force` (or omit `-ItemType` and let the registry provider infer it) so the intent is clear and behavior is more robust across environments and PowerShell versions.
Suggested implementation:
```
if (-not (Test-Path $Snapshot.Path)) {
New-Item -ItemType Key -Path $Snapshot.Path -Force | Out-Null
}
```
```
if (-not (Test-Path $Path)) {
New-Item -ItemType Key -Path $Path -Force | Out-Null
}
```
</issue_to_address>
### Comment 3
<location path="openless-all/app/scripts/windows-real-regression.ps1" line_range="25-16" />
<code_context>
+}
+"@
+
+function Test-CredentialValue($Value) {
+ return ($null -ne $Value) -and ($Value -is [string]) -and ($Value.Trim().Length -gt 0)
+}
+
+function Get-OpenLessCredentialStatus {
</code_context>
<issue_to_address>
**suggestion:** Credential parsing and validation logic is duplicated across scripts and could be centralized.
`windows-real-regression.ps1` and `windows-real-asr-insertion-smoke.ps1` both define nearly identical `Test-CredentialValue` and `Get-OpenLessCredentialStatus` helpers. Please extract these into a shared script (e.g., dot-sourced by each entrypoint) to keep credential validation consistent as the schema or rules evolve.
Suggested implementation:
```
if (-not $env:SystemDrive) {
$env:SystemDrive = "C:"
}
if (-not $env:ProgramData) {
$env:ProgramData = Join-Path $env:SystemDrive "ProgramData"
}
# Load shared credential helpers (Test-CredentialValue, Get-OpenLessCredentialStatus, etc.)
. (Join-Path $PSScriptRoot "windows-credentials.ps1")
```
To fully implement the refactoring and remove duplication:
1. **Create a shared helper script** `openless-all/app/scripts/windows-credentials.ps1` with the common implementations (based on current logic), for example:
```powershell
function Test-CredentialValue {
param(
[Parameter(Mandatory = $true)]
[object]$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
}
}
# ...existing body of Get-OpenLessCredentialStatus from one of the scripts...
}
```
2. **In `openless-all/app/scripts/windows-real-regression.ps1`**, remove the now-duplicated implementations of `Test-CredentialValue` and `Get-OpenLessCredentialStatus` entirely (the edit block above replaces the top of that section; you should remove any remaining parts of these function bodies below the shown snippet if present).
3. **In `openless-all/app/scripts/windows-real-asr-insertion-smoke.ps1`**, add the same dot-sourcing line:
```powershell
. (Join-Path $PSScriptRoot "windows-credentials.ps1")
```
and remove its local definitions of `Test-CredentialValue` and `Get-OpenLessCredentialStatus`.
4. Ensure any future credential-related helpers are added only to `windows-credentials.ps1` and consumed via dot-sourcing, keeping schema/rule changes centralized.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
issue (bug_risk): The $Since parameter is unused in Wait-LogPattern, which can be misleading and suggests incomplete filtering logic.
Since $Since is never read and the function always tails the last 200 lines, callers can’t rely on it to avoid matching older log entries. Consider either removing $Since from the signature or implementing actual since-based filtering (e.g., by tracking timestamps or line offsets).
| 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 |
There was a problem hiding this comment.
suggestion: Registry restoration uses New-Item -ItemType Directory on HKCU paths, which works but is semantically off for registry keys.
In the registry provider this happens to create a key, but it’s misleading and relies on provider-specific behavior. Prefer New-Item -ItemType Key -Force (or omit -ItemType and let the registry provider infer it) so the intent is clear and behavior is more robust across environments and PowerShell versions.
Suggested implementation:
if (-not (Test-Path $Snapshot.Path)) {
New-Item -ItemType Key -Path $Snapshot.Path -Force | Out-Null
}
if (-not (Test-Path $Path)) {
New-Item -ItemType Key -Path $Path -Force | Out-Null
}
| if ([string]::IsNullOrWhiteSpace($ExePath)) { | ||
| $appRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path | ||
| $ExePath = Join-Path $appRoot ".artifacts\windows-gnu\dev\openless.exe" | ||
| } |
There was a problem hiding this comment.
suggestion: Credential parsing and validation logic is duplicated across scripts and could be centralized.
windows-real-regression.ps1 and windows-real-asr-insertion-smoke.ps1 both define nearly identical Test-CredentialValue and Get-OpenLessCredentialStatus helpers. Please extract these into a shared script (e.g., dot-sourced by each entrypoint) to keep credential validation consistent as the schema or rules evolve.
Suggested implementation:
if (-not $env:SystemDrive) {
$env:SystemDrive = "C:"
}
if (-not $env:ProgramData) {
$env:ProgramData = Join-Path $env:SystemDrive "ProgramData"
}
# Load shared credential helpers (Test-CredentialValue, Get-OpenLessCredentialStatus, etc.)
. (Join-Path $PSScriptRoot "windows-credentials.ps1")
To fully implement the refactoring and remove duplication:
-
Create a shared helper script
openless-all/app/scripts/windows-credentials.ps1with the common implementations (based on current logic), for example:function Test-CredentialValue { param( [Parameter(Mandatory = $true)] [object]$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 } } # ...existing body of Get-OpenLessCredentialStatus from one of the scripts... }
-
In
openless-all/app/scripts/windows-real-regression.ps1, remove the now-duplicated implementations ofTest-CredentialValueandGet-OpenLessCredentialStatusentirely (the edit block above replaces the top of that section; you should remove any remaining parts of these function bodies below the shown snippet if present). -
In
openless-all/app/scripts/windows-real-asr-insertion-smoke.ps1, add the same dot-sourcing line:. (Join-Path $PSScriptRoot "windows-credentials.ps1")
and remove its local definitions of
Test-CredentialValueandGet-OpenLessCredentialStatus. -
Ensure any future credential-related helpers are added only to
windows-credentials.ps1and consumed via dot-sourcing, keeping schema/rule changes centralized.
包含本轮所有合并: - Codex 终审两条 HIGH (cancel race) 修复 (PR #79) - 6 个 Cooper-X-Oak/Codex bot PRs 自动合并 (#44 #49 #53 #68 #72 #73) - 2 个有冲突 PR 本地 rebase 后合并 (#66 cancel + 空转写并存 / #67 Windows docs) - README 破图修复 (PR #80) - workflow-scope 受限的 #48 + #75 由用户在 GitHub UI 直接合并 3 处版本字段同步:package.json + tauri.conf.json + Cargo.toml
摘要
Fixes #无(不再新增 upstream issue)。
关联 fork 验证:Cooper-X-Oak#12。
本 PR 是从 fork/dev 已验证批次拆出的 Windows 真机回归维护项:把物理热键/合成热键、真实 ASR、Notepad 与浏览器输入框插入 fallback、麦克风隐私关闭/恢复等场景沉淀为可复跑的 PowerShell smoke 脚本。
修复 / 新增 / 改进
windows-hotkey-injection-smoke.ps1:无物理键触发 coordinator 热键链路。windows-real-asr-insertion-smoke.ps1:真实 ASR 后验证 Notepad / 浏览器输入框插入和 history 写入。windows-microphone-privacy-smoke.ps1:覆盖 Windows 麦克风隐私拒绝 / 恢复路径。windows-real-regression.ps1:检查真实凭据、本地偏好和回归入口。windows-smoke-suite.ps1:串联 runtime、hotkey、ASR 插入和隐私 smoke。兼容
测试计划
git diff --check -- openless-all/app/scriptsSummary by Sourcery
Add Windows real-device regression smoke scripts for hotkey handling, real ASR insertion, microphone privacy, and a composed smoke suite.
New Features: