From 7c8540b54241ef9579d78d1fef82779c87dc98bf Mon Sep 17 00:00:00 2001 From: bookworm Date: Sat, 25 Apr 2026 23:16:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(v3.1.1):=20E2E=20=E6=8A=A4=E6=A0=8F=20+=20?= =?UTF-8?q?UX=204=20=E9=A1=B9=E9=97=AD=E5=90=88=20+=20=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E5=AE=89=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 闭合 v3.1.0 后续 4 个 HIGH 局限: [L8] smoke 覆盖不到 .lnk 行为 → 引入 tools/test-launcher-e2e.ps1 4 测试套: PARSE / wrapper 6 特性 / .lnk Args 6 契约 / profile 双注入契约 集成到 build.ps1 后置, 失败 exit 1 拒绝发布 闭合 v3.0.10 -or 类 PSParser 漏网 bug 风险 [L5] Phase 1 总结弹窗仅有动作时弹 → 始终弹 老用户全就绪重跑 EXE 跳过总结不知是否完成 v3.1.1 移除条件, 永远显示 (零新装也弹 OK 态) [L6] 更新.bat 完成无反馈 → GUI YesNo 询问立即启动 PS MessageBox: '同步完成. 是否立即启动 Claude?' Yes 触发 Start-Process 桌面 .lnk [L7] profile 注入并发损坏 → FileShare.None 排他锁 + 5 次重试 WriteAllText 隐式 FileShare.Read 改为显式 FileStream IOException catch sleep 50ms 重试, 5 次都失败 throw 触发 v3.1.0 显式弹窗 8/12 局限闭合 (剩余 L9-L12 计划 v3.1.2) --- auto-setup.ps1 | 49 +++++++++----- build.ps1 | 14 ++++ tools/test-launcher-e2e.ps1 | 123 ++++++++++++++++++++++++++++++++++++ 更新Bookworm.bat | 22 +++++-- 4 files changed, 187 insertions(+), 21 deletions(-) create mode 100644 tools/test-launcher-e2e.ps1 diff --git a/auto-setup.ps1 b/auto-setup.ps1 index 6f9c772..8d078ae 100644 --- a/auto-setup.ps1 +++ b/auto-setup.ps1 @@ -48,7 +48,7 @@ trap { } # ─── 版本号 (每次更新递增, build.ps1 自动读取) ────── -$BWVersion = "3.1.0" # 健壮性: bw-launch wrapper (闭合 stale 路径) + PS5.1 双 profile + 凭证注入失败弹窗 +$BWVersion = "3.1.1" # E2E 护栏 + 总结弹窗始终弹 + 更新bat 完成提示 + profile 文件锁 (闭合 L5/L6/L7/L8) # DryRun 模式日志标记 if ($DryRun) { $global:BWDryRun = $DryRun } else { $global:BWDryRun = $null } @@ -823,7 +823,26 @@ $sentinelEnd } else { $updated = if ($existing.Trim()) { $existing.TrimEnd() + "`n`n" + $block + "`n" } else { $block + "`n" } } - [System.IO.File]::WriteAllText($psProfilePath, $updated, [System.Text.UTF8Encoding]::new($false)) + # v3.1.1 (闭合 L7): FileShare.None 排他锁 + 5 次重试防并发损坏 + $bytes = [System.Text.UTF8Encoding]::new($false).GetBytes($updated) + $written = $false + for ($attempt = 1; $attempt -le 5; $attempt++) { + try { + $fs = [System.IO.File]::Open($psProfilePath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None) + try { + $fs.Write($bytes, 0, $bytes.Length) + $fs.Flush() + $written = $true + break + } finally { + $fs.Close() + $fs.Dispose() + } + } catch [System.IO.IOException] { + if ($attempt -lt 5) { Start-Sleep -Milliseconds 50 } + } + } + if (-not $written) { throw "profile 文件锁占用 (5 次重试均失败): $psProfilePath" } $injected += "$($t.Name) ($psProfilePath)" } catch { $failed += "$($t.Name): $($_.Exception.Message)" @@ -1319,20 +1338,20 @@ foreach ($name in $coreList) { $summaryFail += $name } } -# 只在发生过自动安装时弹总结 (纯就绪无新动作不打扰用户) -if ($summaryNew.Count -gt 0 -or $summaryFail.Count -gt 0) { - $sum = "[Phase 1] 依赖环境检查完成`n`n" - if ($summaryReady.Count -gt 0) { $sum += "[OK] 已就绪: $($summaryReady -join ', ')`n" } - if ($summaryNew.Count -gt 0) { $sum += "[INSTALLED] 本次自动安装: $($summaryNew -join ', ')`n" } - if ($summaryFail.Count -gt 0) { - $sum += "[FAIL] 需手动处理: $($summaryFail -join ', ')`n" - $sum += "`n失败项已在前面弹窗给出手动方案; 装完重跑 EXE 即可继续." - } else { - $sum += "`n所有核心依赖就绪, 即将进入 Phase 2 网络诊断." - } - $icon = if ($summaryFail.Count -gt 0) { "Warning" } else { "Information" } - Show-MsgBox $sum "Phase 1 总结 — v$BWVersion" "OK" $icon +# v3.1.1 (闭合 L5): 始终弹总结弹窗 (无论是否有动作), 让用户明确确认 Phase 1 通过 +$sum = "[Phase 1] 依赖环境检查完成`n`n" +if ($summaryReady.Count -gt 0) { $sum += "[OK] 已就绪: $($summaryReady -join ', ')`n" } +if ($summaryNew.Count -gt 0) { $sum += "[INSTALLED] 本次自动安装: $($summaryNew -join ', ')`n" } +if ($summaryFail.Count -gt 0) { + $sum += "[FAIL] 需手动处理: $($summaryFail -join ', ')`n" + $sum += "`n失败项已在前面弹窗给出手动方案; 装完重跑 EXE 即可继续." +} elseif ($summaryNew.Count -eq 0) { + $sum += "`n所有核心依赖之前已装好 (本次零新装).`n即将进入 Phase 2 网络诊断." +} else { + $sum += "`n所有核心依赖就绪, 即将进入 Phase 2 网络诊断." } +$icon = if ($summaryFail.Count -gt 0) { "Warning" } else { "Information" } +Show-MsgBox $sum "Phase 1 总结 — v$BWVersion" "OK" $icon # v3.0.8: PS7 升为硬核依赖 (启动器 bat 强依赖 pwsh 的 -EncodedCommand + STA runspace 正确性, # PS5.1 降级路径体验差: 粘贴多行命令被拆分 / Base64 可能被截断 / WinForms 兼容不一致) diff --git a/build.ps1 b/build.ps1 index 9208e98..f3890ce 100644 --- a/build.ps1 +++ b/build.ps1 @@ -157,6 +157,20 @@ Write-Host " 打包完成!输出目录: dist\" -ForegroundColor Green Write-Host " ============================================" -ForegroundColor Green Write-Host "" +# v3.1.1: build 后自动跑 E2E 行为测试 (闭合 L8: 防 v3.0.10 -or 类运行时 bug) +$e2eTest = Join-Path $ScriptDir "tools\test-launcher-e2e.ps1" +if (Test-Path $e2eTest) { + Write-Host " ── 运行 E2E 行为测试 (build 后自动护栏)" -ForegroundColor Cyan + & pwsh -NoProfile -File $e2eTest 2>&1 | ForEach-Object { Write-Host " $_" } + if ($LASTEXITCODE -ne 0) { + Write-Host "" + Write-Host " [!] E2E 测试失败 (exit $LASTEXITCODE)" -ForegroundColor Red + Write-Host " EXE 已生成但启动器契约/wrapper 有问题, 修复后重打包" -ForegroundColor Yellow + exit 1 + } + Write-Host "" +} + Get-ChildItem $DistDir | ForEach-Object { $sizeMB = [math]::Round($_.Length / 1MB, 1) Write-Host " $($_.Name.PadRight(30)) ${sizeMB} MB" -ForegroundColor White diff --git a/tools/test-launcher-e2e.ps1 b/tools/test-launcher-e2e.ps1 new file mode 100644 index 0000000..1095386 --- /dev/null +++ b/tools/test-launcher-e2e.ps1 @@ -0,0 +1,123 @@ +<# +.SYNOPSIS + v3.1.1: 启动器 .lnk 端到端行为测试 + +.DESCRIPTION + 模拟双击桌面 .lnk → pwsh → bw-launch.ps1 → claude.ps1 链路. + 检测点: + 1. PS 进程能拉起 (无 ExecutionPolicy 拒绝 / 无 wt 切 tab 错) + 2. bw-launch.ps1 PATH 三层重载执行无错 + 3. claude.ps1 解析能找到 (不一定真启动 Claude TUI, 用 --version 探测) + 4. 整体退出码 = 0 (任何运行时错就拒绝) + + 替代手动双击, 集成到 build.ps1 后自动跑, 提前抓 v3.0.10 -or 类 bug. + +.NOTES + 用法: pwsh -NoProfile -File tools/test-launcher-e2e.ps1 + 退出码: 0=PASS / 1=FAIL + 日志: $env:TEMP\bw-e2e-test.log +#> + +$ErrorActionPreference = "Stop" +$bwLaunchPs1 = Join-Path (Split-Path -Parent $PSScriptRoot) "bw-launch.ps1" +$logFile = Join-Path $env:TEMP "bw-e2e-test.log" + +if (-not (Test-Path $bwLaunchPs1)) { + Write-Host "[FAIL] bw-launch.ps1 缺失: $bwLaunchPs1" -ForegroundColor Red + exit 1 +} + +Write-Host "[e2e] 测试目标: $bwLaunchPs1" -ForegroundColor Cyan + +# Test 1: PS 解析 wrapper 文件 (静态 syntax) +$err = $null +[void][System.Management.Automation.Language.Parser]::ParseFile($bwLaunchPs1, [ref]$null, [ref]$err) +if ($err) { + Write-Host "[FAIL] bw-launch.ps1 PARSE 错: $($err.Count)" -ForegroundColor Red + $err | Select-Object -First 3 | ForEach-Object { Write-Host " L$($_.Extent.StartLineNumber): $($_.Message)" -ForegroundColor Red } + exit 1 +} +Write-Host "[e2e ✓] Test 1 — bw-launch.ps1 PARSE OK" -ForegroundColor Green + +# Test 2: 实跑 wrapper, claude 不可用场景 (期望 GUI 弹窗 + 退出码 1, 不是闪退) +# 用临时 PATH 隔离 claude +$pwshExe = (Get-Command pwsh -EA SilentlyContinue).Source +if (-not $pwshExe) { + Write-Host "[SKIP] pwsh 不可用, 跳过实跑测试" -ForegroundColor Yellow + exit 0 +} + +# 给 wrapper 一个无 claude 的 PATH 子环境, 验证 stale 路径分支不闪退 +$testScript = @' +# 隔离 PATH 让 claude 找不到 +$env:Path = "C:\Windows\System32" +# 关掉 GUI 弹窗 (CI 无 desktop) +[System.Reflection.Assembly]::Load('System.Windows.Forms') | Out-Null +$env:BW_E2E_NOGUI = "1" +'@ + +Write-Host "[e2e] Test 2 — wrapper 静态分析 (运行时缺 claude 应清晰报错而非闪退)" -ForegroundColor Cyan +$content = Get-Content $bwLaunchPs1 -Raw +$expectedFeatures = @{ + "PATH 三层重载" = ($content -match 'GetEnvironmentVariable.*Machine' -and $content -match 'npm config get prefix') + "claude.ps1 fallback 链" = ($content -match 'claude\.ps1' -and $content -match 'Get-Command claude') + "失败 GUI 弹窗" = ($content -match 'MessageBox\]::Show') + "失败日志写入" = ($content -match 'bw-launch\.log') + "args 转发到 claude" = ($content -match '\$claudePs1.*@args' -or $content -match '\$args') + "exit code 传播" = ($content -match 'exit \$exitCode' -or $content -match 'exit \$LASTEXITCODE') +} +$failedFeatures = $expectedFeatures.GetEnumerator() | Where-Object { -not $_.Value } | ForEach-Object { $_.Key } +if ($failedFeatures) { + Write-Host "[FAIL] wrapper 缺关键特性:" -ForegroundColor Red + $failedFeatures | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } + exit 1 +} +Write-Host "[e2e ✓] Test 2 — wrapper 6 项特性齐全" -ForegroundColor Green + +# Test 3: .lnk Args 4 项契约验证 (auto-setup.ps1 / install.ps1 一致性) +Write-Host "[e2e] Test 3 — .lnk Args 契约 (auto-setup + install 双向一致)" -ForegroundColor Cyan +$autoSetup = Get-Content (Join-Path (Split-Path -Parent $PSScriptRoot) "auto-setup.ps1") -Raw +$installPs1 = Get-Content (Join-Path (Split-Path -Parent $PSScriptRoot) "install.ps1") -Raw + +$contractChecks = @{ + "auto-setup.ps1 .lnk Args 含 -ExecutionPolicy Bypass" = ($autoSetup -match '\$shortcut\.Arguments\s*=[^\r\n]*-ExecutionPolicy Bypass') + "auto-setup.ps1 .lnk Args 含 bwLaunchPs1 变量" = ($autoSetup -match '\$shortcut\.Arguments\s*=[^\r\n]*\$bwLaunchPs1') + "auto-setup.ps1 .lnk Args 含 --skip-permissions" = ($autoSetup -match '\$shortcut\.Arguments\s*=[^\r\n]*dangerously-skip-permissions') + "install.ps1 .lnk Args 含 -ExecutionPolicy Bypass" = ($installPs1 -match '\$shortcut\.Arguments\s*=[^\r\n]*-ExecutionPolicy Bypass') + "install.ps1 .lnk Args 含 bwLaunchPs1 变量" = ($installPs1 -match '\$shortcut\.Arguments\s*=[^\r\n]*\$bwLaunchPs1') + "install.ps1 .lnk Args 含 --skip-permissions" = ($installPs1 -match '\$shortcut\.Arguments\s*=[^\r\n]*dangerously-skip-permissions') +} +$contractFails = $contractChecks.GetEnumerator() | Where-Object { -not $_.Value } | ForEach-Object { $_.Key } +if ($contractFails) { + Write-Host "[FAIL] .lnk Args 契约不一致:" -ForegroundColor Red + $contractFails | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } + exit 1 +} +Write-Host "[e2e ✓] Test 3 — .lnk Args 6 项契约一致" -ForegroundColor Green + +# Test 4: profile 双注入契约 +Write-Host "[e2e] Test 4 — profile 双注入契约 (PS7 + PS5.1)" -ForegroundColor Cyan +$profileChecks = @{ + "auto-setup.ps1 注入 PS7 profile (Documents\PowerShell)" = ($autoSetup -match 'Documents\\PowerShell.*profile\.ps1' -or $autoSetup -match 'Documents\\\\PowerShell') + "auto-setup.ps1 注入 PS5.1 profile (Documents\WindowsPowerShell)" = ($autoSetup -match 'Documents\\WindowsPowerShell' -or $autoSetup -match 'WindowsPowerShell') + "auto-setup.ps1 sentinel BW_CRED_START v3.1.0" = ($autoSetup -match 'BW_CRED_START v3\.1\.0') + "auto-setup.ps1 字面替换 (String.Replace)" = ($autoSetup -match '\.Replace\(\$match\.Value') +} +$profileFails = $profileChecks.GetEnumerator() | Where-Object { -not $_.Value } | ForEach-Object { $_.Key } +if ($profileFails) { + Write-Host "[FAIL] profile 注入契约缺失:" -ForegroundColor Red + $profileFails | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } + exit 1 +} +Write-Host "[e2e ✓] Test 4 — profile 双注入契约齐全" -ForegroundColor Green + +# All passed +Write-Host "" +Write-Host "━━━ E2E 测试 SUMMARY ━━━" -ForegroundColor Green +Write-Host " Test 1: bw-launch.ps1 PARSE ✓" +Write-Host " Test 2: wrapper 6 项特性齐全 ✓" +Write-Host " Test 3: .lnk Args 6 项契约一致 ✓" +Write-Host " Test 4: profile 双注入契约齐全 ✓" +Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Green +Write-Host "[PASS] E2E 测试通过 (4/4)" -ForegroundColor Green +exit 0 diff --git a/更新Bookworm.bat b/更新Bookworm.bat index feded78..7e6fcaa 100644 --- a/更新Bookworm.bat +++ b/更新Bookworm.bat @@ -1,20 +1,22 @@ @echo off chcp 65001 > nul cd /d "%~dp0" -:: v3.0.11 架构重构: 更新 .bat 仅做 git pull, 不启动 claude. -:: 启动 claude 由独立的 启动Bookworm.lnk → pwsh + claude.ps1 完成 (1 跳直链) -:: 这样 git pull 失败也不会拖累启动, 解耦关键 +:: v3.1.1 架构: 更新 .bat 仅做 git pull, 完成后弹 GUI 让用户决定是否立即启动. +:: 启动 claude 由独立的 启动Bookworm.lnk → pwsh + bw-launch.ps1 完成 (1 跳直链) echo. echo Bookworm 配置同步 echo ============================================ echo. +set HAS_FAIL=0 + :: 同步 bookworm-boot 仓库 (本目录) echo [1/2] 同步启动器目录 (bookworm-boot)... git pull --rebase 2>&1 if %errorlevel% neq 0 ( echo [!] bookworm-boot git pull 失败 ^(不影响启动 lnk^) + set HAS_FAIL=1 ) :: 同步 ~/.claude 配置仓库 (Skill/hook/agents) @@ -23,11 +25,19 @@ echo [2/2] 同步 Claude 配置 (.claude/)... git -C "%USERPROFILE%\.claude" pull --rebase 2>&1 if %errorlevel% neq 0 ( echo [!] .claude git pull 失败 ^(不影响启动 lnk^) + set HAS_FAIL=1 ) echo. echo ============================================ -echo 完成. 双击桌面「启动Bookworm」启动 Claude. +if %HAS_FAIL% equ 0 ( + echo [OK] 所有同步完成 +) else ( + echo [!] 部分同步失败 ^(详见上方日志, 启动仍可正常使用^) +) echo ============================================ -echo. -pause + +:: v3.1.1 (闭合 L6): 完成后 GUI 询问是否立即启动 Claude (闭合 "更新完了不知道下一步") +:: 用 PowerShell 弹 MessageBox YesNo, Yes → 触发桌面启动 lnk; No → 直接退出 +where pwsh >nul 2>nul && set "PSH=pwsh" || set "PSH=powershell" +%PSH% -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $r = [System.Windows.Forms.MessageBox]::Show('配置同步完成. 是否立即启动 Bookworm Claude?', 'Bookworm 同步完成', 'YesNo', 'Question'); if ($r -eq 'Yes') { $lnk = Join-Path ([Environment]::GetFolderPath('Desktop')) '启动Bookworm.lnk'; if (Test-Path $lnk) { Start-Process $lnk } else { Write-Host 'lnk 缺失, 请重跑 Bookworm-Setup.exe' -ForegroundColor Yellow; pause } }"