Files
pebrel/scripts/tests/installer.tests.ps1

167 lines
9.2 KiB
PowerShell

[CmdletBinding()]
param([string] $TargetDirectory)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$repo = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$installerPath = Join-Path $repo 'scripts\installer.iss'
$migrationPath = Join-Path $repo 'scripts\installer-migration.iss'
$builderPath = Join-Path $repo 'scripts\build-installer.ps1'
$installer = Get-Content -LiteralPath $installerPath -Raw -Encoding UTF8
$migration = Get-Content -LiteralPath $migrationPath -Raw -Encoding UTF8
$requiredPatterns = [ordered]@{
'migration-aware installation directory' = 'DefaultDirName=\{code:DefaultInstallDir\}'
'registered previous-directory reuse' = 'UsePreviousAppDir=yes'
'Pebrel start-menu group' = 'UsePreviousGroup=no'
'non-admin installation' = 'PrivilegesRequired=lowest'
'Windows 10 1809 floor' = 'MinVersion=10\.0\.17763'
'application closing' = 'CloseApplications=yes'
'no application restart during uninstall' = 'RestartApplications=no'
'desktop shortcut task' = 'Tasks: desktopicon'
'login startup task' = '\{userstartup\}\\Pebrel'
'hook cleanup command' = 'Parameters: "setup-ai --remove"'
'gpui start-menu shortcut' = 'Parameters: "--gpui"'
'idempotent cleanup entry' = 'RunOnceId: "RemovePebrelAiHooks"'
'hook helper payload' = 'pebrel-hook\.exe'
'ConPTY payload' = 'conpty\.dll'
'ConPTY host payload' = 'OpenConsole\.exe'
'font payload' = 'MapleMonoNormal-NF-CN-Regular\.ttf'
'optional font installation task' = 'Tasks: installfont'
'pinned Chinese language file' = 'target\\installer-tools\\ChineseSimplified\.isl'
'localized context menu label' = 'english\.OpenInPebrel=Open in Pebrel'
'Pebrel display name' = 'AppName=Pebrel'
'compatible installer identity' = 'AppId=\{\{61022144-7D0A-4E54-94F2-C329A8F58656\}'
'Pebrel default asset name' = '#define PackageBrand "Pebrel"'
'architecture-specific package name' = 'OutputBaseFilename=\{#PackageBrand\}-v\{#AppVersion\}-windows-\{#Architecture\}-setup'
'native ARM64 gate' = '(?s)#if Architecture == "arm64".*ArchitecturesAllowed=arm64'
'localized Chinese context menu label' = 'chinesesimplified\.OpenInPebrel=\S.+'
'localized WSL context menu label' = 'english\.OpenInPebrelWsl=Open in Pebrel'
'localized Chinese WSL context menu label' = 'chinesesimplified\.OpenInPebrelWsl=\S.+'
'WSL context menu uninstall cleanup' = 'RemoveOwnedWslContextMenus;'
'directory background context menu' = 'Software\\Classes\\Directory\\Background\\shell\\Pebrel'
'selected directory context menu' = 'Software\\Classes\\Directory\\shell\\Pebrel'
'context menu executable icon' = 'ValueName: "Icon"; ValueData: "\{app\}\\pebrel\.exe,0"'
'background working-directory command' = '--gpui --working-directory ""%V""'
'selected directory working-directory command' = '--gpui --working-directory ""%1""'
'PATH task' = 'Name: "addtopath"; Description: "\{cm:AddToPath\}"'
'PATH registry entry' = 'Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"'
'PATH ownership marker' = 'ValueName: "InstallerAddedToPath"'
'Win+R App Paths registration' = 'App Paths\\pebrel\.exe'
'notification identity on shortcuts' = 'AppUserModelID: "com\.pebrel\.terminal"'
'environment change notification' = 'ChangesEnvironment=yes'
'PATH uninstall cleanup' = 'CurUninstallStepChanged\(CurUninstallStep: TUninstallStep\)'
'isolated acceptance installer identity' = 'AppId=\{\{76B778B5-76C6-4F60-9431-9E67C2A351AF\}'
'runtime control API documentation' = 'Source: "\{#RepoRoot\}\\docs\\runtime-control-api\.md"; DestDir: "\{app\}\\docs";'
'runtime API schema' = 'Source: "\{#RepoRoot\}\\docs\\runtime-api-v1\.schema\.json"; DestDir: "\{app\}\\docs";'
'Pebrel Runtime skill instructions' = 'Source: "\{#RepoRoot\}\\docs\\skills\\pebrel-runtime\\SKILL\.md"; DestDir: "\{app\}\\skills\\pebrel-runtime";'
'Pebrel Runtime skill metadata' = 'Source: "\{#RepoRoot\}\\docs\\skills\\pebrel-runtime\\agents\\openai\.yaml"; DestDir: "\{app\}\\skills\\pebrel-runtime\\agents";'
}
foreach ($entry in $requiredPatterns.GetEnumerator()) {
if ($installer -notmatch $entry.Value) {
throw "Installer is missing $($entry.Key): $($entry.Value)"
}
}
$acceptanceIsolationPatterns = [ordered]@{
'tasks' = '(?s)\[Tasks\]\s*#ifndef AcceptanceFixture.*?#endif'
'per-user font installation' = '(?s)#ifndef AcceptanceFixture\s*Source:.*?\{autofonts\}.*?#endif'
'shortcuts' = '(?s)\[Icons\]\s*#ifndef AcceptanceFixture.*?#endif'
'registry integrations' = '(?s)\[Registry\]\s*#ifndef AcceptanceFixture.*?#endif'
'AI hook uninstall action' = '(?s)\[UninstallRun\]\s*#ifndef AcceptanceFixture.*?#endif'
'PATH uninstall cleanup' = '(?s)#ifndef AcceptanceFixture\s*procedure CurUninstallStepChanged.*?end;\s*#endif'
}
foreach ($entry in $acceptanceIsolationPatterns.GetEnumerator()) {
if ($installer -notmatch $entry.Value) {
throw "Acceptance installer must exclude $($entry.Key)."
}
}
$uninstallRun = $installer.IndexOf('[UninstallRun]', [System.StringComparison]::Ordinal)
$cleanup = $installer.IndexOf('setup-ai --remove', [System.StringComparison]::Ordinal)
if ($uninstallRun -lt 0 -or $cleanup -lt $uninstallRun) {
throw 'Hook cleanup must be an [UninstallRun] action so it executes before installed files are deleted.'
}
$contextMenuRoots = @(
'Software\Classes\Directory\Background\shell\Pebrel'
'Software\Classes\Directory\shell\Pebrel'
)
foreach ($root in $contextMenuRoots) {
$escapedRoot = [regex]::Escape($root)
if ($installer -notmatch "Subkey: `"$escapedRoot`";.*Flags: uninsdeletekey") {
throw "Context-menu key must be removed during uninstall: $root"
}
}
$migrationPatterns = [ordered]@{
'per-user Pebrel default' = '\{localappdata\}\\Programs\\Pebrel'
'registered previous installation' = 'Inno Setup: App Path'
'known previous directory rename' = "ExtractFileName\(Result\), 'Nebula Terminal'"
'running legacy executable detection' = 'IsExecutableRunning\(Executable\)'
'retryable migration record' = 'PendingLegacyInstallDir'
'visible migration failures' = "CustomMessage\('MigrationFailed'\)"
'nonzero migration failure exit' = 'GetCustomSetupExitCode'
'precise legacy payload cleanup' = 'RemoveLegacyPayload'
'linked path protection' = 'Attributes and \$400'
'isolated acceptance settings identity' = "ProductSettingsKey = 'Software\\PebrelUpdateAcceptance'"
'isolated acceptance legacy identity' = "LegacySettingsKey = 'Software\\PebrelUpdateAcceptanceLegacy'"
}
foreach ($entry in $migrationPatterns.GetEnumerator()) {
if ($migration -notmatch $entry.Value) {
throw "Installer migration is missing $($entry.Key): $($entry.Value)"
}
}
if ($migration -match 'DelTree\(|TerminateProcess\(|taskkill') {
throw 'Migration must not recursively delete user data or forcefully terminate applications.'
}
# 按 WSL 发行版注册的右键项(installer-migration.iss 里的 [Code]):
$wslPatterns = [ordered]@{
'WSL distribution registry' = 'Software\\Microsoft\\Windows\\CurrentVersion\\Lxss'
'plumbing distros are skipped' = "Pos\('docker-desktop'"
'distinct verb namespace' = "'PebrelWsl' \+ IntToStr\(Index\)"
'registration runs at post-install' = 'RegisterWslContextMenus;'
'owned keys are reclaimed by prefix and command' = "Pos\('PebrelWsl', Names\[NameIndex\]\) = 1"
'uninstall sweeps both roots' = "Directory\\Background\\shell'"
}
foreach ($entry in $wslPatterns.GetEnumerator()) {
if ($migration -notmatch $entry.Value) {
throw "Installer migration is missing $($entry.Key): $($entry.Value)"
}
}
# 命令串里 `--shell` 必须排在 `--working-directory` 之前:盘根(`D:\`)时后者的
# 收尾反斜杠会吃掉收尾引号并把后面整段并进同一个参数(issue #36 的另一面),顺序
# 写反会静默开出一个既没有 cwd、也没用上指定发行版的标签。
$commandTemplates = @(
$migration -split "`n" | Where-Object { $_ -match '^\s*Command :=' -and $_ -match '--shell' }
)
if ($commandTemplates.Count -ne 1) {
throw "Expected exactly one WSL context-menu command template, found $($commandTemplates.Count)."
}
$shellAt = $commandTemplates[0].IndexOf('--shell')
$directoryAt = $commandTemplates[0].IndexOf('--working-directory')
if ($shellAt -lt 0 -or $directoryAt -lt 0 -or $shellAt -gt $directoryAt) {
throw "The WSL context-menu command must pass --shell before --working-directory: $($commandTemplates[0].Trim())"
}
$validationArguments = @{ SkipBuild = $true; AllowStale = $true; ValidateOnly = $true }
if (-not [string]::IsNullOrWhiteSpace($TargetDirectory)) {
$validationArguments.TargetDirectory = $TargetDirectory
}
& $builderPath @validationArguments
$builder = Get-Content -LiteralPath $builderPath -Raw -Encoding UTF8
if ($builder -notmatch 'Stale binary') {
throw 'build-installer.ps1 must refuse stale binaries (freshness guard missing).'
}
if ($builder -notmatch 'c495623a97376d524f298b1b160e8fd612375c62' -or
$builder -notmatch '6753BE2C5E2740D859900FD902824DB2EC568DA5C5B52486524C9762D778B0B0') {
throw 'The Chinese installer translation must use a pinned source commit and SHA-256.'
}
Write-Output "installer.tests.ps1: PASS ($($requiredPatterns.Count) invariants)"