feat(v3.1.1): E2E 护栏 + UX 4 项闭合 + 并发安全
闭合 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)
This commit is contained in:
parent
e225a5c758
commit
7c8540b542
@ -48,7 +48,7 @@ trap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ─── 版本号 (每次更新递增, build.ps1 自动读取) ──────
|
# ─── 版本号 (每次更新递增, 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 模式日志标记
|
# DryRun 模式日志标记
|
||||||
if ($DryRun) { $global:BWDryRun = $DryRun } else { $global:BWDryRun = $null }
|
if ($DryRun) { $global:BWDryRun = $DryRun } else { $global:BWDryRun = $null }
|
||||||
@ -823,7 +823,26 @@ $sentinelEnd
|
|||||||
} else {
|
} else {
|
||||||
$updated = if ($existing.Trim()) { $existing.TrimEnd() + "`n`n" + $block + "`n" } else { $block + "`n" }
|
$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)"
|
$injected += "$($t.Name) ($psProfilePath)"
|
||||||
} catch {
|
} catch {
|
||||||
$failed += "$($t.Name): $($_.Exception.Message)"
|
$failed += "$($t.Name): $($_.Exception.Message)"
|
||||||
@ -1319,20 +1338,20 @@ foreach ($name in $coreList) {
|
|||||||
$summaryFail += $name
|
$summaryFail += $name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# 只在发生过自动安装时弹总结 (纯就绪无新动作不打扰用户)
|
# v3.1.1 (闭合 L5): 始终弹总结弹窗 (无论是否有动作), 让用户明确确认 Phase 1 通过
|
||||||
if ($summaryNew.Count -gt 0 -or $summaryFail.Count -gt 0) {
|
|
||||||
$sum = "[Phase 1] 依赖环境检查完成`n`n"
|
$sum = "[Phase 1] 依赖环境检查完成`n`n"
|
||||||
if ($summaryReady.Count -gt 0) { $sum += "[OK] 已就绪: $($summaryReady -join ', ')`n" }
|
if ($summaryReady.Count -gt 0) { $sum += "[OK] 已就绪: $($summaryReady -join ', ')`n" }
|
||||||
if ($summaryNew.Count -gt 0) { $sum += "[INSTALLED] 本次自动安装: $($summaryNew -join ', ')`n" }
|
if ($summaryNew.Count -gt 0) { $sum += "[INSTALLED] 本次自动安装: $($summaryNew -join ', ')`n" }
|
||||||
if ($summaryFail.Count -gt 0) {
|
if ($summaryFail.Count -gt 0) {
|
||||||
$sum += "[FAIL] 需手动处理: $($summaryFail -join ', ')`n"
|
$sum += "[FAIL] 需手动处理: $($summaryFail -join ', ')`n"
|
||||||
$sum += "`n失败项已在前面弹窗给出手动方案; 装完重跑 EXE 即可继续."
|
$sum += "`n失败项已在前面弹窗给出手动方案; 装完重跑 EXE 即可继续."
|
||||||
|
} elseif ($summaryNew.Count -eq 0) {
|
||||||
|
$sum += "`n所有核心依赖之前已装好 (本次零新装).`n即将进入 Phase 2 网络诊断."
|
||||||
} else {
|
} else {
|
||||||
$sum += "`n所有核心依赖就绪, 即将进入 Phase 2 网络诊断."
|
$sum += "`n所有核心依赖就绪, 即将进入 Phase 2 网络诊断."
|
||||||
}
|
}
|
||||||
$icon = if ($summaryFail.Count -gt 0) { "Warning" } else { "Information" }
|
$icon = if ($summaryFail.Count -gt 0) { "Warning" } else { "Information" }
|
||||||
Show-MsgBox $sum "Phase 1 总结 — v$BWVersion" "OK" $icon
|
Show-MsgBox $sum "Phase 1 总结 — v$BWVersion" "OK" $icon
|
||||||
}
|
|
||||||
|
|
||||||
# v3.0.8: PS7 升为硬核依赖 (启动器 bat 强依赖 pwsh 的 -EncodedCommand + STA runspace 正确性,
|
# v3.0.8: PS7 升为硬核依赖 (启动器 bat 强依赖 pwsh 的 -EncodedCommand + STA runspace 正确性,
|
||||||
# PS5.1 降级路径体验差: 粘贴多行命令被拆分 / Base64 可能被截断 / WinForms 兼容不一致)
|
# PS5.1 降级路径体验差: 粘贴多行命令被拆分 / Base64 可能被截断 / WinForms 兼容不一致)
|
||||||
|
|||||||
14
build.ps1
14
build.ps1
@ -157,6 +157,20 @@ Write-Host " 打包完成!输出目录: dist\" -ForegroundColor Green
|
|||||||
Write-Host " ============================================" -ForegroundColor Green
|
Write-Host " ============================================" -ForegroundColor Green
|
||||||
Write-Host ""
|
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 {
|
Get-ChildItem $DistDir | ForEach-Object {
|
||||||
$sizeMB = [math]::Round($_.Length / 1MB, 1)
|
$sizeMB = [math]::Round($_.Length / 1MB, 1)
|
||||||
Write-Host " $($_.Name.PadRight(30)) ${sizeMB} MB" -ForegroundColor White
|
Write-Host " $($_.Name.PadRight(30)) ${sizeMB} MB" -ForegroundColor White
|
||||||
|
|||||||
123
tools/test-launcher-e2e.ps1
Normal file
123
tools/test-launcher-e2e.ps1
Normal file
@ -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
|
||||||
@ -1,20 +1,22 @@
|
|||||||
@echo off
|
@echo off
|
||||||
chcp 65001 > nul
|
chcp 65001 > nul
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
:: v3.0.11 架构重构: 更新 .bat 仅做 git pull, 不启动 claude.
|
:: v3.1.1 架构: 更新 .bat 仅做 git pull, 完成后弹 GUI 让用户决定是否立即启动.
|
||||||
:: 启动 claude 由独立的 启动Bookworm.lnk → pwsh + claude.ps1 完成 (1 跳直链)
|
:: 启动 claude 由独立的 启动Bookworm.lnk → pwsh + bw-launch.ps1 完成 (1 跳直链)
|
||||||
:: 这样 git pull 失败也不会拖累启动, 解耦关键
|
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo Bookworm 配置同步
|
echo Bookworm 配置同步
|
||||||
echo ============================================
|
echo ============================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
|
set HAS_FAIL=0
|
||||||
|
|
||||||
:: 同步 bookworm-boot 仓库 (本目录)
|
:: 同步 bookworm-boot 仓库 (本目录)
|
||||||
echo [1/2] 同步启动器目录 (bookworm-boot)...
|
echo [1/2] 同步启动器目录 (bookworm-boot)...
|
||||||
git pull --rebase 2>&1
|
git pull --rebase 2>&1
|
||||||
if %errorlevel% neq 0 (
|
if %errorlevel% neq 0 (
|
||||||
echo [!] bookworm-boot git pull 失败 ^(不影响启动 lnk^)
|
echo [!] bookworm-boot git pull 失败 ^(不影响启动 lnk^)
|
||||||
|
set HAS_FAIL=1
|
||||||
)
|
)
|
||||||
|
|
||||||
:: 同步 ~/.claude 配置仓库 (Skill/hook/agents)
|
:: 同步 ~/.claude 配置仓库 (Skill/hook/agents)
|
||||||
@ -23,11 +25,19 @@ echo [2/2] 同步 Claude 配置 (.claude/)...
|
|||||||
git -C "%USERPROFILE%\.claude" pull --rebase 2>&1
|
git -C "%USERPROFILE%\.claude" pull --rebase 2>&1
|
||||||
if %errorlevel% neq 0 (
|
if %errorlevel% neq 0 (
|
||||||
echo [!] .claude git pull 失败 ^(不影响启动 lnk^)
|
echo [!] .claude git pull 失败 ^(不影响启动 lnk^)
|
||||||
|
set HAS_FAIL=1
|
||||||
)
|
)
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ============================================
|
echo ============================================
|
||||||
echo 完成. 双击桌面「启动Bookworm」启动 Claude.
|
if %HAS_FAIL% equ 0 (
|
||||||
|
echo [OK] 所有同步完成
|
||||||
|
) else (
|
||||||
|
echo [!] 部分同步失败 ^(详见上方日志, 启动仍可正常使用^)
|
||||||
|
)
|
||||||
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 } }"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user