Merge remote-tracking branch 'origin/main' into ci/preflight-and-cache-evidence-20260923

This commit is contained in:
kuddev committed 2026-09-24 10:24:27 +08:00
commit efc0108c03
38 files changed
+1646 -181

No files matched your search

+98 -48
View File
@@ -277,13 +277,13 @@ end;
{ —— 按 WSL 发行版注册的右键项 ——
「在 Pebrel 中打开(Ubuntu)」这类菜单项按**安装那一刻**机器上的发行版动态生成:
WSL 发行版子菜单按**安装那一刻**机器上的发行版动态生成:
`Lxss` 里的 `DistributionName` 是唯一真相,枚举口径与 Rust 侧
`shell_detect::find_wsl_distros` 一致(跳过 `docker-desktop*` 这类 plumbing 发行版)。
走 `[Code]` 而不是 `[Registry]` 的原因只有一个:键的条数取决于机器上有几个发行版。
代价是这些键没有 `uninsdeletekey`,卸载由 `RemoveOwnedWslContextMenus` 按名字前缀
加命令串里的可执行文件与 `--shell` 认领后删除。 }
加精确命令与级联菜单归属标记认领后删除。 }
const
LxssRoot = 'Software\Microsoft\Windows\CurrentVersion\Lxss';
@@ -308,67 +308,117 @@ begin
SetArrayLength(Result, Count);
end;
{ 删掉本安装器写下的 WSL 右键项:键名以 `PebrelWsl` 开头、命令串里同时有我们的
可执行文件和 `--shell`,两条都满足才算我们写的。发行版后来被移除也不会留下
点不动的死项。 }
{ WSL 右键项按精确可执行文件前缀归属;级联菜单还校验所有子项,保留用户编辑。
发行版移除后,下次安装重建菜单时不留下旧项。 }
function IsOwnedWslCommand(Command, Executable: string): Boolean;
var
Prefix: string;
begin
Prefix := '"' + Executable + '" --gpui --shell "wsl:';
Result := CompareText(Copy(Command, 1, Length(Prefix)), Prefix) = 0;
end;
function IsOwnedWslMenu(Key, Executable: string): Boolean;
var
Owner, Command: string;
Children, Subkeys: TArrayOfString;
Index: Integer;
begin
Result := False;
if not RegQueryStringValue(HKCU, Key, 'PebrelOwner', Owner) or
(CompareText(Owner, Executable) <> 0) then
Exit;
if not RegGetSubkeyNames(HKCU, Key, Subkeys) then
Exit;
for Index := 0 to GetArrayLength(Subkeys) - 1 do
if CompareText(Subkeys[Index], 'shell') <> 0 then
Exit;
if GetArrayLength(Subkeys) > 0 then begin
if not RegGetSubkeyNames(HKCU, Key + '\shell', Children) then
Exit;
for Index := 0 to GetArrayLength(Children) - 1 do begin
if not RegQueryStringValue(HKCU, Key + '\shell\' + Children[Index] + '\command', '', Command) or
not IsOwnedWslCommand(Command, Executable) then
Exit;
end;
end;
Result := True;
end;
procedure RemoveOwnedWslContextMenusAt(Root, Executable: string);
var
Key, Command: string;
Names: TArrayOfString;
NameIndex: Integer;
begin
if RegGetSubkeyNames(HKCU, Root, Names) then
for NameIndex := 0 to GetArrayLength(Names) - 1 do
if Pos('PebrelWsl', Names[NameIndex]) = 1 then begin
Key := Root + '\' + Names[NameIndex];
Command := '';
if IsOwnedWslMenu(Key, Executable) or
(RegQueryStringValue(HKCU, Key + '\command', '', Command) and
IsOwnedWslCommand(Command, Executable)) then
if not RegDeleteKeyIncludingSubkeys(HKCU, Key) then
RaiseException('Unable to remove an owned WSL context menu: ' + Key);
end;
end;
procedure RemoveOwnedWslContextMenus;
var
Roots: array[0..1] of string;
Root, Key, Command: string;
Names: TArrayOfString;
Index, NameIndex: Integer;
Index: Integer;
begin
Roots[0] := 'Software\Classes\Directory\shell';
Roots[1] := 'Software\Classes\Directory\Background\shell';
for Index := 0 to 1 do begin
Root := Roots[Index];
if RegGetSubkeyNames(HKCU, Root, Names) then
for NameIndex := 0 to GetArrayLength(Names) - 1 do
if Pos('PebrelWsl', Names[NameIndex]) = 1 then begin
Key := Root + '\' + Names[NameIndex];
Command := '';
if RegQueryStringValue(HKCU, Key + '\command', '', Command) and
(Pos(ExpandConstant('{app}\pebrel.exe'), Command) > 0) and
(Pos('--shell', Command) > 0) then
if not RegDeleteKeyIncludingSubkeys(HKCU, Key) then
RaiseException('Unable to remove an owned WSL context menu: ' + Key);
end;
for Index := 0 to 1 do
RemoveOwnedWslContextMenusAt(Roots[Index], ExpandConstant('{app}\pebrel.exe'));
end;
procedure RegisterWslContextMenuAt(Root, Executable, DirectoryArgument: string;
Distros: TArrayOfString);
var
Index: Integer;
Distro, Verb, Command, Key, Menu: string;
begin
Menu := Root + '\PebrelWslMenu';
{ Never overwrite an unknown installation or an edited submenu. }
if RegKeyExists(HKCU, Menu) and not IsOwnedWslMenu(Menu, Executable) then
RaiseException(FmtMessage(CustomMessage('WslMenuConflict'), [Menu]));
RemoveOwnedWslContextMenusAt(Root, Executable);
if GetArrayLength(Distros) = 0 then
Exit;
if not RegWriteStringValue(HKCU, Menu, 'MUIVerb', CustomMessage('OpenInPebrelWsl')) or
not RegWriteStringValue(HKCU, Menu, 'Icon', Executable + ',0') or
not RegWriteStringValue(HKCU, Menu, 'SubCommands', '') or
not RegWriteStringValue(HKCU, Menu, 'PebrelOwner', Executable) then
RaiseException(CustomMessage('WslMenuRegistrationFailed'));
for Index := 0 to GetArrayLength(Distros) - 1 do begin
Distro := Distros[Index];
{ 键名用序号而不是发行版名:注册表键名里带空格与非 ASCII 只会给自己找麻烦,
何况我们靠前缀认领。 }
Verb := 'PebrelWsl' + IntToStr(Index);
{ `--shell` 必须排在 `--working-directory` 之前:盘根(`D:\`)时后者的收尾
反斜杠会吃掉它的收尾引号,并把后面整段并进同一个参数(issue #36 的另一面),
顺序写反会静默开出一个既没有 cwd、也没用上指定发行版的标签。 }
Command := '"' + Executable + '" --gpui --shell "wsl:' + Distro + '" --working-directory ';
Key := Menu + '\shell\' + Verb;
if not RegWriteStringValue(HKCU, Key, 'MUIVerb', Distro) or
not RegWriteStringValue(HKCU, Key, 'Icon', Executable + ',0') or
not RegWriteStringValue(HKCU, Key + '\command', '', Command + '"' + DirectoryArgument + '"') then
RaiseException('Unable to register the WSL context menu for ' + Distro + '.');
end;
end;
procedure RegisterWslContextMenus;
var
Distros: TArrayOfString;
Index: Integer;
Distro, Verb, LabelText, Executable, Command, Key: string;
Executable: string;
begin
{ 先清掉上一次写下的:发行版被移除后,残留的索引键会指向一个已经不存在的发行版。 }
RemoveOwnedWslContextMenus;
Distros := WslDistroNames;
if GetArrayLength(Distros) = 0 then
Exit;
Executable := ExpandConstant('{app}\pebrel.exe');
for Index := 0 to GetArrayLength(Distros) - 1 do begin
Distro := Distros[Index];
{ 键名用序号而不是发行版名:注册表键名里带空格与非 ASCII 只会给自己找麻烦,
何况我们靠前缀认领。 }
Verb := 'PebrelWsl' + IntToStr(Index);
LabelText := CustomMessage('OpenInPebrelWsl') + ' (' + Distro + ')';
{ `--shell` 必须排在 `--working-directory` 之前:盘根(`D:\`)时后者的收尾
反斜杠会吃掉它的收尾引号,并把后面整段并进同一个参数(issue #36 的另一面),
顺序写反会静默开出一个既没有 cwd、也没用上指定发行版的标签。 }
Command := '"' + Executable + '" --gpui --shell "wsl:' + Distro + '" --working-directory ';
Key := 'Software\Classes\Directory\shell\' + Verb;
if not RegWriteStringValue(HKCU, Key, 'MUIVerb', LabelText) or
not RegWriteStringValue(HKCU, Key, 'Icon', Executable + ',0') or
not RegWriteStringValue(HKCU, Key + '\command', '', Command + '"%1"') then
RaiseException('Unable to register the WSL context menu for ' + Distro + '.');
Key := 'Software\Classes\Directory\Background\shell\' + Verb;
if not RegWriteStringValue(HKCU, Key, 'MUIVerb', LabelText) or
not RegWriteStringValue(HKCU, Key, 'Icon', Executable + ',0') or
not RegWriteStringValue(HKCU, Key + '\command', '', Command + '"%V"') then
RaiseException('Unable to register the WSL background context menu for ' + Distro + '.');
end;
RegisterWslContextMenuAt('Software\Classes\Directory\shell', Executable, '%1', Distros);
RegisterWslContextMenuAt('Software\Classes\Directory\Background\shell', Executable, '%V', Distros);
end;
procedure MigrateLegacyIntegrations;
+6 -2
View File
@@ -77,7 +77,9 @@ english.AutoStart=Start Pebrel when I sign in to Windows
english.InstallFont=Install Maple Mono font for the current user
english.AddToPath=Add Pebrel to the user PATH
english.OpenInPebrel=Open in Pebrel
english.OpenInPebrelWsl=Open in Pebrel
english.OpenInPebrelWsl=Open in Pebrel (WSL)
english.WslMenuConflict=An existing WSL submenu belongs to another installation or was edited, so it was preserved: %1.
english.WslMenuRegistrationFailed=Unable to register the WSL context submenu.
english.LaunchProgram=Launch Pebrel
english.UninstallProgram=Uninstall Pebrel
english.CloseLegacyProgram=Close the application at %1, then retry the installation.
@@ -89,7 +91,9 @@ chinesesimplified.AutoStart=登录 Windows 后启动 Pebrel
chinesesimplified.InstallFont=为当前用户安装 Maple Mono 字体
chinesesimplified.AddToPath=将 Pebrel 添加到当前用户 PATH
chinesesimplified.OpenInPebrel=在 Pebrel 中打开
chinesesimplified.OpenInPebrelWsl=在 Pebrel 中打开
chinesesimplified.OpenInPebrelWsl=在 Pebrel 中打开(WSL)
chinesesimplified.WslMenuConflict=现有 WSL 子菜单属于其他安装或已被修改,因此已保留:%1。
chinesesimplified.WslMenuRegistrationFailed=无法注册 WSL 右键子菜单。
chinesesimplified.LaunchProgram=启动 Pebrel
chinesesimplified.UninstallProgram=卸载 Pebrel
chinesesimplified.CloseLegacyProgram=请关闭 %1 中运行的程序,然后重试安装。
+5 -1
View File
@@ -17,7 +17,11 @@ class UpdateProcess {
if (!File.Exists(Path.Combine(directory, "parent-exited"))) return 3;
if (Environment.GetEnvironmentVariable("PEBREL_HANDOFF_FAIL_INSTALL") == "1") return 17;
if (Environment.GetEnvironmentVariable("PEBREL_HANDOFF_NOOP_INSTALL") == "1") return 0;
File.Copy(Path.Combine(directory, "candidate.exe"), Path.Combine(target, "pebrel.exe"), true);
try {
File.Copy(Path.Combine(directory, "candidate.exe"), Path.Combine(target, "pebrel.exe"), true);
Directory.CreateDirectory(Path.Combine(target, "runtime"));
File.WriteAllText(Path.Combine(target, "runtime", "pebrel-hook.exe"), "repaired helper fixture");
} catch (IOException) { return 5; }
return 0;
#else
if (args.Length > 0 && args[0] == "--version") {
+70 -1
View File
@@ -1,4 +1,4 @@
#ifndef FixtureRoot
#ifndef FixtureRoot
#error FixtureRoot must point at an isolated directory under the workspace tmp directory.
#endif
#define MigrationFixture
@@ -15,6 +15,11 @@ OutputBaseFilename=installer-migration-fixture
Compression=none
SetupLogging=no
[CustomMessages]
OpenInPebrelWsl=Open in Pebrel (WSL)
WslMenuConflict=Preserved an edited or foreign submenu: %1.
WslMenuRegistrationFailed=Unable to register the WSL context submenu.
[Code]
#include "..\installer-migration.iss"
@@ -180,6 +185,69 @@ begin
Check(FileExists(NewLink), 'shortcut pointing at another program is preserved');
end;
procedure CheckContextMenus(Root: string);
var
RegistryRoot, ShellRoot, Executable, Value, Menu, ForeignCommand: string;
Distros, Names: TArrayOfString;
Index: Integer;
Failed: Boolean;
begin
RegistryRoot := 'Software\PebrelTestFixtures\' + ExtractFileName(Root);
Executable := Root + '\Program Files\pebrel.exe';
ForeignCommand := '"D:\other\pebrel.exe" --gpui --shell "wsl:Other" --working-directory "%1"';
try
for Index := 0 to 1 do begin
ShellRoot := RegistryRoot + '\root' + IntToStr(Index);
Menu := ShellRoot + '\PebrelWslMenu';
Check(RegWriteStringValue(HKCU, ShellRoot + '\Pebrel\command', '', 'ordinary-open'), 'seed ordinary entry');
Check(RegWriteStringValue(HKCU, ShellRoot + '\PebrelWslForeign\command', '', ForeignCommand), 'seed foreign entry');
Check(RegWriteStringValue(HKCU, ShellRoot + '\PebrelWsl0\command', '',
'"' + Executable + '" --gpui --shell "wsl:Old" --working-directory "%1"'), 'seed old flat entry');
SetArrayLength(Distros, 3);
Distros[0] := 'Debian';
Distros[1] := 'Ubuntu Test';
Distros[2] := '开发环境';
if Index = 0 then Value := '%1' else Value := '%V';
RegisterWslContextMenuAt(ShellRoot, Executable, Value, Distros);
Check(not RegKeyExists(HKCU, ShellRoot + '\PebrelWsl0'), 'migrate old flat entry');
Check(RegGetSubkeyNames(HKCU, ShellRoot, Names) and (GetArrayLength(Names) = 3), 'only normal, cascade and foreign roots remain');
Check(RegGetSubkeyNames(HKCU, Menu + '\shell', Names) and (GetArrayLength(Names) = 3), 'three distros live inside cascade');
Check(RegQueryStringValue(HKCU, Menu, 'SubCommands', Value) and (Value = ''), 'enable static cascade');
Check(RegQueryStringValue(HKCU, Menu + '\shell\PebrelWsl1\command', '', Value), 'child command exists');
if Index = 0 then
Check(Value = '"' + Executable + '" --gpui --shell "wsl:Ubuntu Test" --working-directory "%1"', 'selected directory argv')
else
Check(Value = '"' + Executable + '" --gpui --shell "wsl:Ubuntu Test" --working-directory "%V"', 'background directory argv');
Check(RegQueryStringValue(HKCU, Menu + '\shell\PebrelWsl2', 'MUIVerb', Value) and (Value = '开发环境'), 'Unicode label retained');
RegisterWslContextMenuAt(ShellRoot, Executable, '%1', Distros);
Check(RegGetSubkeyNames(HKCU, Menu + '\shell', Names) and (GetArrayLength(Names) = 3), 'repeat registration has no duplicates');
SetArrayLength(Distros, 1);
RegisterWslContextMenuAt(ShellRoot, Executable, '%1', Distros);
Check(RegGetSubkeyNames(HKCU, Menu + '\shell', Names) and (GetArrayLength(Names) = 1), 'removed distros leave no stale entries');
Check(RegWriteStringValue(HKCU, Menu + '\shell\Custom\command', '', ForeignCommand), 'seed edited submenu');
Failed := False;
try
RegisterWslContextMenuAt(ShellRoot, Executable, '%1', Distros);
except
Failed := True;
end;
Check(Failed and RegKeyExists(HKCU, Menu + '\shell\Custom'), 'edited submenu is not overwritten');
RemoveOwnedWslContextMenusAt(ShellRoot, Executable);
Check(RegKeyExists(HKCU, Menu + '\shell\Custom'), 'uninstall preserves edited subtree');
Check(RegDeleteKeyIncludingSubkeys(HKCU, Menu + '\shell\Custom'), 'remove fixture edit');
RemoveOwnedWslContextMenusAt(ShellRoot, 'D:\other\install.exe');
Check(RegKeyExists(HKCU, Menu), 'other installation cannot remove this menu');
SetArrayLength(Distros, 0);
RegisterWslContextMenuAt(ShellRoot, Executable, '%1', Distros);
Check(not RegKeyExists(HKCU, Menu), 'zero distros removes empty cascade');
Check(RegQueryStringValue(HKCU, ShellRoot + '\PebrelWslForeign\command', '', Value) and (Value = ForeignCommand), 'foreign entry preserved');
Check(RegQueryStringValue(HKCU, ShellRoot + '\Pebrel\command', '', Value) and (Value = 'ordinary-open'), 'ordinary entry preserved');
end;
finally
RegDeleteKeyIncludingSubkeys(HKCU, RegistryRoot);
end;
end;
function InitializeSetup: Boolean;
var
Root, Report: string;
@@ -190,6 +258,7 @@ begin
CheckPayloadMigration(Root);
CheckFailures(Root);
CheckShortcuts(Root);
CheckContextMenus(Root);
Report := 'PASS: ' + IntToStr(Checks) + ' migration checks';
except
Report := 'FAIL: ' + GetExceptionMessage;
+28 -7
View File
@@ -36,7 +36,7 @@ function Write-Json([string]$Path, $Value) {
}
$results = @()
foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'installer-failure', 'other-process', 'late-process', 'reinstall', 'reinstall-noop')) {
foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'installer-failure', 'other-process', 'late-process', 'reinstall', 'repair-identical', 'upgrade-noop', 'helper-unlocked', 'helper-held')) {
$directory = Join-Path $OutputRoot $scenario
$installation = Join-Path $directory 'installed app'
$transaction = Join-Path $directory 'transaction'
@@ -44,17 +44,25 @@ foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'insta
$null = New-Item -ItemType Directory -Path $installation, $transaction, $config -Force
$executable = Join-Path $installation 'pebrel.exe'
Copy-Item -LiteralPath $old -Destination $executable
$payload = if ($scenario -eq 'reinstall') { $reinstall } else { $candidate }
$payload = if ($scenario -eq 'reinstall') { $reinstall } elseif ($scenario -eq 'repair-identical') { $old } else { $candidate }
Copy-Item -LiteralPath $payload -Destination (Join-Path $directory 'candidate.exe')
[System.IO.File]::WriteAllText((Join-Path $installation 'unins000.exe'), 'fixture marker')
$env:PEBREL_HANDOFF_FIXTURE = $directory
$env:PEBREL_HANDOFF_FAIL_INSTALL = if ($scenario -eq 'installer-failure') { '1' } else { '0' }
$env:PEBREL_HANDOFF_NOOP_INSTALL = if ($scenario -eq 'reinstall-noop') { '1' } else { '0' }
$env:PEBREL_HANDOFF_NOOP_INSTALL = if ($scenario -eq 'upgrade-noop') { '1' } else { '0' }
$parent = Start-Process -FilePath $executable -ArgumentList 'wait' -PassThru
$null = $parent.Handle
$runner = $null
$other = $null
$helperLock = $null
try {
if ($scenario -in @('helper-unlocked', 'helper-held')) {
$null = New-Item -ItemType Directory -Path (Join-Path $installation 'runtime') -Force
$helperPath = Join-Path $installation 'runtime\pebrel-hook.exe'
[System.IO.File]::WriteAllText($helperPath, 'old helper fixture')
$helperLock = [System.IO.FileStream]::new($helperPath, [System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read, [System.IO.FileShare]::Read)
}
$plan = @{
schema = 1; transaction = $scenario; executable = $executable; installation = $installation
config_directory = $config; installer = $installer
@@ -64,7 +72,7 @@ foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'insta
participants = @(@{ pid = $parent.Id; created = $parent.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() })
}
if ($scenario -eq 'checksum') { $plan.sha256 = '0' * 64 }
if ($scenario -in @('reinstall', 'reinstall-noop')) { $plan.version = '1.8.0' }
if ($scenario -in @('reinstall', 'repair-identical')) { $plan.version = '1.8.0' }
if ($scenario -eq 'creation-time') { $plan.participants[0].created = '1' }
if ($scenario -eq 'other-process') {
$other = Start-Process -FilePath $executable -ArgumentList 'wait-other' -PassThru
@@ -104,8 +112,14 @@ foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'insta
Assert (-not (Test-Path (Join-Path $directory 'installer-started'))) 'Setup ran while the old process was alive'
[System.IO.File]::WriteAllText((Join-Path $directory 'exit-parent'), 'exit')
Assert ($parent.WaitForExit(5000)) 'Fixture parent did not exit'
if ($scenario -eq 'helper-unlocked') {
Start-Sleep -Milliseconds 350
Assert (-not (Test-Path (Join-Path $directory 'installer-started'))) 'Setup started with the helper still locked'
$helperLock.Dispose()
$helperLock = $null
}
Wait-File $resultPath
if ($scenario -in @('success', 'installer-failure', 'reinstall', 'reinstall-noop')) {
if ($scenario -in @('success', 'installer-failure', 'reinstall', 'repair-identical', 'upgrade-noop', 'helper-unlocked', 'helper-held')) {
Wait-File (Join-Path $directory 'new-launch')
$launch = [System.IO.File]::ReadAllLines((Join-Path $directory 'new-launch'))
Assert ($launch[0] -eq $executable) 'Relaunch selected another installation'
@@ -117,24 +131,31 @@ foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'insta
}
Assert ($runner.WaitForExit(5000)) 'Helper did not finish'
$result = Get-Content -LiteralPath $resultPath -Raw -Encoding UTF8 | ConvertFrom-Json
Assert ($result.success -eq ($scenario -in @('success', 'reinstall'))) 'Unexpected helper outcome'
Assert ($result.success -eq ($scenario -in @('success', 'reinstall', 'repair-identical', 'helper-unlocked'))) 'Unexpected helper outcome'
if ($result.success) {
$expected = (Get-FileHash -LiteralPath $payload -Algorithm SHA256).Hash
Assert ($result.executable_sha256 -eq $expected) 'Installed binary differs from candidate'
Assert (Test-Path -LiteralPath (Join-Path $installation 'runtime\pebrel-hook.exe')) 'Successful setup did not repair the helper'
Assert ([System.IO.File]::ReadAllText((Join-Path $installation 'runtime\pebrel-hook.exe')) -eq 'repaired helper fixture') 'Helper still contains the original payload'
}
if ($scenario -in @('other-process', 'late-process')) {
Assert (-not $other.HasExited) 'Update stopped an unprepared process'
Assert (-not (Test-Path (Join-Path $directory 'installer-started'))) 'Setup ran with an unprepared process'
}
if ($scenario -in @('installer-failure', 'reinstall-noop')) {
if ($scenario -in @('installer-failure', 'upgrade-noop', 'helper-held')) {
Assert $result.recovered_original 'Untouched old executable was not recovered'
}
if ($scenario -eq 'helper-held') {
Assert (-not (Test-Path (Join-Path $directory 'installer-started'))) 'A blocked helper allowed a partial installation'
Assert ($result.error -match 'pebrel-hook.exe') 'The blocked file is missing from the failure details'
}
$probe = [System.IO.FileStream]::new($plan.guard_path, [System.IO.FileMode]::OpenOrCreate,
[System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
$probe.Dispose()
$results += @{ scenario = $scenario; passed = $true }
Write-Output "$scenario : passed"
} finally {
if ($helperLock) { $helperLock.Dispose() }
if ($runner -and -not $runner.HasExited) { $runner.Kill(); $runner.WaitForExit() }
if (-not $parent.HasExited) {
[System.IO.File]::WriteAllText((Join-Path $directory 'exit-parent'), 'exit')
@@ -0,0 +1,69 @@
# 用户反馈回归测试清单
本清单覆盖 2026-09-23 汇总的 Hook、更新、窗口材质、链接、Codex 显示与
Windows 右键菜单反馈。它是验收合同,不表示下列功能已经全部修复。
`待验证`、`等待日志`、`部分覆盖` 均不得作为 Issue 已解决的依据。
## 执行与证据
- 每轮记录源码提交、未提交补丁、产品版本、Windows build、DPI、主题、字体、
Shell 与 Codex 版本。记录实际命令、通过/失败数量及截图或日志位置。
- 自动回归使用实际生产函数、真实终端网格或进程夹具;不能只检查源码中出现
某个字符串。原生截图不替代行为测试,编译成功也不替代视觉验收。
- 对修复先验证旧实现失败,再验证新实现通过;保留相邻功能的反例。
- 浅色/深色、100%/125%/150%/200% DPI、宽/窄窗口、分屏、滚动和主题热切换
按条目覆盖。平台条件导致无法执行的项必须注明,不计入通过数量。
- 实机操作使用隔离配置与测试窗口。安装/注册表夹具只修改其临时测试根,
不关闭用户会话,不覆盖正式安装,不触碰用户 Hook、凭据和发行版配置。
- 仅关闭已有代码与验收证据覆盖的问题单;部分修复说明剩余范围。
发布版本尚未包含修复时必须说明交付状态,不把本地通过写成已发布。
## 回归项目
| ID | 问题与来源 | 自动回归 / 通过条件 | 实机验收 / 反例 | 当前证据 |
| --- | --- | --- | --- | --- |
| HOOK-255 | [#255](https://github.com/Kuddev/pebrel/issues/255):Windows Codex Hook 被 PowerShell 解析为字符串而未执行 | 生成命令实际执行;`codex`、`--hooks=full/turns` 和 stdin JSON 完整传入;带空格、单引号、`$`、反引号的路径保持字面值 | Windows PowerShell 5.1 与 PowerShell 7;Codex 0.155.1/0.156.0;新会话、工具调用、提问、停止都无 ParserError | Windows 5.1 进程夹具通过;完整 Codex/PowerShell 7 会话待验收 |
| HOOK-LIFETIME | #258 前置故障:关闭 Pebrel 后 Hook 仍占用安装文件 | 保持 stdin 打开、连接后不读取命名管道时,真实 Hook 子进程仍在期限内以 0 退出;Cursor 继续允许提交;用户原有 notifier 仍执行 | 已运行的旧版 Hook 不会自动获得新期限;分别验证新启动的 native Hook 与旧进程恢复步骤 | 7 项单元 + 5 项真实进程回归通过 |
| HOOK-MIGRATE | 更新后旧 `hooks.json` / 归属标记仍含失效命令 | 旧受管命令迁移;重复安装幂等;保留 notify、第三方 Hook、显式禁用及用户编辑;卸载只移除自有条目 | 重启 Pebrel 后自动修复;用户修改过的冲突条目不能静默覆盖 | `ai_hook::` 120 通过、1 项既有忽略;后续改动需重跑相关项 |
| UPDATE-403 | 手动检查更新返回 HTTP 403 | 公共 Release 无需用户 token;覆盖 403/429、正常 JSON、其他错误、代理、受限响应;下载仍须官方 URL、精确资产名和 SHA-256 | 同一网络直连/代理、启动检查/手动检查;离线不得假报最新或成功 | **等待日志,按用户要求暂停**;未验收、不结项 |
| UPDATE-EXIT5 | 安装器 `exit code 5`,当前/最新版本显示相同 | 失败不得标成功;保留原快照与安装日志;已退出进程、安装失败、部分替换、相同版本、校验失败分别验证 | 基于反馈者 `installer.log` 重现;核对原版本、目标版本、实际 EXE;不能把 Inno 退出码 5 当作 Win32 拒绝访问码 | 后续 #258 已提供修复安装成功日志;无限等待的 Hook 已补期限;安装前等待短暂占用释放,持续占用时在启动安装器前失败,避免部分替换;旧版本已有挂起进程仍需另行处理 |
| UPDATE-258 | [#258](https://github.com/Kuddev/pebrel/issues/258):同版本修复成功却因主程序哈希不变报错 | 同版本主程序字节不变、Hook 文件修复后应成功;安装器失败或目标版本仍错误必须失败 | 同版本重装、首次升级部分成功后的修复;日志、实际版本和结果提示一致;原会话快照保留 | 12 项原生 handoff 场景通过,包含 `repair-identical` 与 `upgrade-noop` 正反例 |
| BLUR-236-A | [#236](https://github.com/Kuddev/pebrel/issues/236):Acrylic 在屏幕边缘透出背景内容 | 切换材质/透明度不重复创建资源;新窗口、关闭、失焦、最大化/还原、队列退出及运行库失败路径 | 0% 与非零不透明度;明显文字/色块背景;左右/上下屏幕边缘、最大化、任务栏恢复、混合 DPI;失焦不出现不透明灰底 | [PR #250](https://github.com/Kuddev/pebrel/pull/250) 尚未合并;其跨平台 CI 通过不等于本轮实机验收 |
| BLUR-236-B | #236 的 Aero 与 Acrylic 兼容回退范围 | Windows 10、Win11 22H2+、缺失/不匹配运行库与失败挂接均可正常启动/切换/退出 | Aero 需单独验收;缺少受支持 Windows App Runtime 时旧边缘现象不能标为已解决 | #250 仅覆盖受支持 Acrylic;**Aero 和回退范围仍开放** |
| LINK-CJK | OSC 8 中文/全角字符的虚线只画半字、错位 | 从真实 VT 输出构建网格,中文两格、空格、ASCII、组合字符均按列覆盖;逐格绘制与连续区间的物理像素集合相同 | `A开始 菜单Z`、纯中文、混排;DPI/分屏/窄窗/换行;不随字体回补或字形分段重置虚线 | 真实 OSC 8 网格与 16 种 DPI/格宽组合像素覆盖回归通过;原生 Nord / Paper 混排截图已核对 |
| LINK-PATH | 英文路径下划线断开,带空格路径只匹配前半段 | 连续虚线相位;引号包围的盘符/UNC/`~/` 路径完整匹配;hover、点击使用同一边界;普通正文不误连 | `"C:\Program Files\Example Tools\probe.exe"`、中文与括号目录、相邻命令参数、路径换行 | 8 项 hint 测试与 5 项配置解析测试通过;原生带空格路径截图已核对 |
| LINK-PROMPT | WSL `user@host:~/.codex$` 吞入 `$`;绝对路径提示符没有下划线 | `~/` 与 `/mnt/d/...` 提示符中的 `$`/`#` 不可点;真实文件名 `~/cost$`、URL 尾部 `$`、路径内部 `$` 保留;下划线与命中边界一致 | Bash/WSL 彩色提示符、root 提示符、输入命令后、换行;OSC 8 显式 URI 不被启发式改写 | 正反匹配/命中回归通过;绝对路径打开应使用所属 WSL 发行版,不得套用宿主目录或 SSH 目标 |
| CODEX-SPARKLE | 对照终端 中可见的 Codex 星点在 Pebrel 不显示 | 核对真彩色能力、OSC 10/11 回答、盲文点字与前/背景色;真实 PTY 录制/回放;关闭动画时不强制产生动画 | 相同 Codex 可执行文件/模型/设置,对照 WT;分别覆盖 low/high/xhigh/max/ultra、Max→xhigh 回退、持续星点与一次性等级动画;以 CLI 实际输出为准,不能由终端自行按等级添加/过滤星点;不能伪造 `WT_SESSION` 来冒充 WT | 完整 Windows 子环境漏真彩色声明已修复;8 项环境回归、10 项身份/WSL 透传回归及 15 主题 VT 回放通过;真实产品星点字形/背景截图已核对;已观察到 Codex 0.154.0 在 xhigh 持续输出变化的星点;其源码持续星点与 Max/Ultra 一次性动画分别控制;与对照终端的等级切换差异仍待验收 |
| CODEX-MESSAGE | **所有主题**的 Codex 用户消息与 AI 回复背景缺少区分 | 保留/正确映射应用给出的用户消息背景;未提供背景时不得凭屏幕文字猜测角色;消息文本对比度不退化 | 遍历主题注册表中的全部内置主题;输入框与已发送的单/多行、中英混排消息;深浅切换、滚动回看;用户消息应有可辨认浅色背景区 | 15 个内置主题 + 225 种主题切换的背景保留回归通过;真实产品 Nord / Paper 的背景和星点截图已核对;其余主题有自动回放覆盖,仍需原生视觉抽查 |
| SHELL-MENU | 多个 WSL 发行版把资源管理器右键菜单铺满 | 保留一个普通“在 Pebrel 中打开”;其余发行版集中到一个级联菜单;零/一/多个发行版、重复安装、移除发行版、卸载与旧项迁移 | 文件夹对象/文件夹空白处;中文/空格/盘根路径;点击普通项与每个子项验证 Shell 和 cwd;保留第三方/其他安装拥有的键 | 已实现;真实 Inno 夹具累计 117 项通过,完整安装器编译通过;正式 Explorer 点击验收仍需进行 |
## 修复与评审入口
- Hook 命令及退出期限:[PR #260](https://github.com/Kuddev/pebrel/pull/260),关联 #254 / #255 及 #258 的前置占用故障。
- 同版本修复判定:[PR #261](https://github.com/Kuddev/pebrel/pull/261),关联 #258。
- 下划线与路径边界:[PR #262](https://github.com/Kuddev/pebrel/pull/262)。
- Windows / WSL 真彩色能力:[PR #263](https://github.com/Kuddev/pebrel/pull/263),关联 #136。
- WSL 级联菜单:[PR #264](https://github.com/Kuddev/pebrel/pull/264)。
本地组合回归与各 PR 的 CI、Code Owner 评审是不同证据;未合并、未发布不能写成已经交付。
## 当前测试入口
```powershell
cargo test --locked -p nebula_hook
cargo test --locked -p nebula --bin pebrel --features gpui-shell platform::environment::tests
cargo test --locked -p nebula --bin pebrel --features gpui-shell ai_hook::
cargo test --locked -p nebula --bin pebrel --features gpui-shell display::hint::tests
cargo test --locked -p nebula --bin pebrel --features gpui-shell config::ui_config::tests
cargo test --locked -p nebula --bin pebrel --features gpui-shell gpui_shell::terminal::link_underline::tests
cargo test --locked -p nebula --bin pebrel --features gpui-shell gpui_shell::terminal::element::tests
cargo test --locked -p nebula --bin pebrel --features gpui-shell display::terminal_color::tests
```
安装器既有入口为 `scripts/tests/installer.tests.ps1`、
`scripts/tests/installer-migration.tests.ps1`、`scripts/tests/test_update_handoff.ps1`。
菜单夹具已扩展零/一/多个发行版及所有权保护;handoff 已扩展同版本修复正例与错误版本反例。它们不代替正式安装或 Explorer 点击验收。
修改完成后还需按 `CONTRIBUTING.md` 执行架构、格式、受影响模块与真实产品检查;
架构门禁使用实际目标分支基线。新增门禁或 GitHub 强制保护需单独验证,
本清单不宣称启用了服务端保护。