tests/cases/40-exe21sp.ps1

# exe21sp 提取:普通/TinySharp/压缩/Core 产物还原、资源参数与图标往返、保存到文件分支。
$deps = @('exe21sp.ps1', 'src/Interact/exe21sp.ps1', 'src/TaskbarProgress.ps1') + $script:CoreCompileDeps

# 生成确定性的 1x1 32bpp ICO 测试夹具(源码里没有该图标,用于验证资源往返)。
$script:IconFixture = Join-Path (Get-TestRepoRoot) 'tests/.cache/fixtures/resource.ico'
if (-not (Test-Path -LiteralPath $script:IconFixture)) {
    New-Item -ItemType Directory -Path (Split-Path $script:IconFixture -Parent) -Force | Out-Null
    $ico = [System.Collections.Generic.List[byte]]::new()
    $ico.AddRange([byte[]](0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 32, 0))
    $img = [System.Collections.Generic.List[byte]]::new()
    $img.AddRange([byte[]](40, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0))
    1..4 | ForEach-Object { $img.AddRange([byte[]](0, 0, 0, 0)) }
    $img.AddRange([byte[]](0, 0, 255, 255, 0, 0, 0, 0))
    $ico.AddRange([BitConverter]::GetBytes([uint32]$img.Count))
    $ico.AddRange([BitConverter]::GetBytes([uint32]22))
    $ico.AddRange($img)
    [System.IO.File]::WriteAllBytes($script:IconFixture, $ico.ToArray())
}

Add-Test @{
    Name  = 'exe21sp.normal'
    Group = 'exe21sp'
    Deps  = $deps
    Build = @{ Name = 'normal'; InputText = "Get-Date | Out-Null; Write-Output 'normal-embed'"; Output = 'normal.exe' }
    Run   = {
        param($ctx)
        $exe = $ctx.Builds['normal']
        $content = Get-Exe21spContent -ExePath $exe
        Assert-Match $content 'normal-embed' "exe21sp 未还原 normal 脚本:$content"
        $fromPipe = ($exe | exe21sp | Out-String)
        Assert-Match $fromPipe 'normal-embed' "exe21sp 管道输入未还原脚本:$fromPipe"
        $r = Invoke-ExeCaptureMergedOutput -ExePath $exe
        Assert-Match $r.Output 'normal-embed' 'normal exe 运行输出'
    }
}

Add-Test @{
    Name  = 'exe21sp.tinysharp'
    Group = 'exe21sp'
    Deps  = $deps
    Builds = @(
        @{ Name = 'c0'; InputText = "'tinysharp-console-zero'"; Output = 'ts_console_0.exe' }
        @{ Name = 'c42'; InputText = "'tinysharp-console-42'; exit 42"; Output = 'ts_console_42.exe' }
        @{ Name = 'g0'; InputText = "'tinysharp-gui-zero'"; Params = @{ App = @{ Windowed = $true }; Resources = @{ Title = 'CI' } }; Output = 'ts_gui_0.exe' }
        @{ Name = 'g42'; InputText = "'tinysharp-gui-42'; exit 42"; Params = @{ App = @{ Windowed = $true }; Resources = @{ Title = 'CI' } }; Output = 'ts_gui_42.exe' }
    )
    Run   = {
        param($ctx)
        $c0 = Get-Exe21spContent -ExePath $ctx.Builds['c0']
        Assert-Match $c0 'tinysharp-console-zero' "exe21sp TinySharp console 0 内容:$c0"
        & $ctx.Builds['c0'] | Out-Null
        Assert-Equal 0 $LASTEXITCODE 'ts_console_0 退出码'

        $c42 = Get-Exe21spContent -ExePath $ctx.Builds['c42']
        Assert-Match $c42 'tinysharp-console-42' "exe21sp TinySharp console 42 内容:$c42"
        Assert-Match $c42 'exit 42' "exe21sp TinySharp console 42 未保留 exit:$c42"
        & $ctx.Builds['c42'] | Out-Null
        Assert-Equal 42 $LASTEXITCODE 'ts_console_42 退出码'

        $g0 = Get-Exe21spContent -ExePath $ctx.Builds['g0']
        Assert-Match $g0 'tinysharp-gui-zero' "exe21sp TinySharp GUI 0 内容:$g0"
        $g42 = Get-Exe21spContent -ExePath $ctx.Builds['g42']
        Assert-Match $g42 'tinysharp-gui-42' "exe21sp TinySharp GUI 42 内容:$g42"
        Assert-Match $g42 'exit 42' "exe21sp TinySharp GUI 42 未保留 exit:$g42"
    }
}

Add-Test @{
    Name  = 'exe21sp.packed'
    Group = 'exe21sp'
    Deps  = $deps
    Build = @{ Name = 'packed'; InputText = "Get-Date | Out-Null; Write-Output 'packed-embed'"; Output = 'packed.exe' }
    Run   = {
        param($ctx)
        $content = Get-Exe21spContent -ExePath $ctx.Builds['packed']
        Assert-Match $content 'packed-embed' "exe21sp 未解包 packed 负载:$content"
    }
}

Add-Test @{
    Name  = 'exe21sp.core'
    Group = 'exe21sp'
    Deps  = $deps
    Build = @{ Name = 'core'; InputText = "Get-Date | Out-Null; Write-Output 'core-packed-embed'"; Params = @{ Build = @{ Target = 'Core' } }; Output = 'core_packed.exe' }
    Run   = {
        param($ctx)
        if (-not (Get-Command dotnet -ErrorAction Ignore)) { throw 'Core 目标需要 .NET SDK(dotnet)' }
        $exe = $ctx.Builds['core']
        $content = Get-Exe21spContent -ExePath $exe
        Assert-Match $content 'core-packed-embed' "exe21sp Core 未还原:$content"

        # Windows PowerShell(.NET Framework 无 BrotliStream)下转交 pwsh 解压后同样能还原。
        $repoEsc = $ctx.RepoRoot -replace "'", "''"
        $exeEsc = $exe -replace "'", "''"
        $winPs = (& powershell -NoProfile -Command "Import-Module '$repoEsc' -Force; exe21sp -inputFile '$exeEsc'" 2>$null | Out-String)
        Assert-Match $winPs 'core-packed-embed' "exe21sp Core 在 Windows PowerShell 下未还原:$winPs"
    }
}

Add-Test @{
    Name  = 'exe21sp.save-file'
    Group = 'exe21sp'
    Deps  = $deps
    Build = @{ Name = 'normal'; InputText = "Get-Date | Out-Null; Write-Output 'normal-embed'"; Output = 'normal.exe' }
    Run   = {
        param($ctx)
        $exe = Copy-BuildAs -BuildPath $ctx.Builds['normal'] -WorkDir $ctx.WorkDir -Name 'savefile.exe'
        $expected = [System.IO.Path]::ChangeExtension($exe, '.ps1')
        if (Test-Path -LiteralPath $expected) { Remove-Item -LiteralPath $expected -Force }
        $exit = Invoke-Exe21spInPrivateConsole -RepoRoot $ctx.RepoRoot -ExePath $exe
        Assert-Equal 0 $exit 'exe21sp 私有 console 退出码'
        Assert-FileExists $expected 'exe21sp 未重定向时应写出 <exe>.ps1'
        Assert-Match (Get-Content -LiteralPath $expected -Raw -Encoding UTF8) 'normal-embed' '保存的 ps1 内容不符'
    }
}

Add-Test @{
    Name  = 'exe21sp.resources'
    Group = 'exe21sp'
    Deps  = $deps
    Build = @{
        Name    = 'resource'
        Output  = 'resource.exe'
        InputText = "Get-Date | Out-Null; Write-Output 'resource-roundtrip'"
        Params  = @{ Resources = @{ Title = 'RT Title'; Description = 'RT Desc'; Company = 'RT Co'; Version = '2.3.4.5'; Icon = $script:IconFixture } }
    }
    Run   = {
        param($ctx)
        $exe = Copy-BuildAs -BuildPath $ctx.Builds['resource'] -WorkDir $ctx.WorkDir -Name 'resource.exe'
        $extractOut = Join-Path $ctx.WorkDir 'resource.extracted.ps1'
        exe21sp -inputFile $exe -outputFile $extractOut | Out-Null
        $text = Get-Content -LiteralPath $extractOut -Raw -Encoding UTF8
        foreach ($expected in @("#_pragma Resources.Title 'RT Title'", "#_pragma Resources.Description 'RT Desc'", "#_pragma Resources.Company 'RT Co'", "#_pragma Resources.Version '2.3.4.5'", '#_pragma Resources.Icon')) {
            Assert-True ($text -like "*$expected*") "exe21sp 资源往返缺少 [$expected]:$text"
        }
        $releasedIcon = Join-Path $ctx.WorkDir 'resource.extracted.ico'
        Assert-FileExists $releasedIcon 'exe21sp 未释放图标到输出目录'

        $recompiled = Join-Path $ctx.WorkDir 'resource.recompiled.exe'
        ps12exe -inputFile $extractOut -outputFile $recompiled -NoUpdateCheck | Out-Null
        $info = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($recompiled)
        Assert-Equal 'RT Title' $info.FileDescription 'exe21sp 重编译标题丢失'
        Assert-Equal 'RT Co' $info.CompanyName 'exe21sp 重编译公司丢失'
        Assert-Equal '2.3.4.5' $info.FileVersion 'exe21sp 重编译版本丢失'
        Assert-Match ((& $recompiled | Out-String)) 'resource-roundtrip' 'exe21sp 重编译运行输出不符'

        $extractOut2 = Join-Path $ctx.WorkDir 'resource.recompiled.ps1'
        exe21sp -inputFile $recompiled -outputFile $extractOut2 | Out-Null
        $text2 = Get-Content -LiteralPath $extractOut2 -Raw -Encoding UTF8
        Assert-Equal 1 ([regex]::Matches($text2, '(?m)^\s*#_pragma\s+Resources\.Title\b')).Count 'exe21sp 幂等性:Title pragma 重复'
        Assert-Equal 1 ([regex]::Matches($text2, '(?m)^\s*#_pragma\s+Resources\.Icon\b')).Count 'exe21sp 幂等性:Icon pragma 重复'
    }
}

Add-Test @{
    Name  = 'exe21sp.core-defaults'
    Group = 'exe21sp'
    Deps  = $deps
    Build = @{ Name = 'coreplain'; InputText = "Get-Date | Out-Null; Write-Output 'core-resource'"; Params = @{ Build = @{ Target = 'Core' } }; Output = 'core-resource-plain.exe' }
    Run   = {
        param($ctx)
        if (-not (Get-Command dotnet -ErrorAction Ignore)) { throw 'Core 目标需要 .NET SDK(dotnet)' }
        $exe = Copy-BuildAs -BuildPath $ctx.Builds['coreplain'] -WorkDir $ctx.WorkDir -Name 'core-resource-plain.exe'
        $out = Join-Path $ctx.WorkDir 'core-resource-plain.ps1'
        exe21sp -inputFile $exe -outputFile $out | Out-Null
        $text = Get-Content -LiteralPath $out -Raw -Encoding UTF8
        foreach ($unexpected in @('#_pragma Resources.Company', '#_pragma Resources.Product', '#_pragma Resources.Version', '#_pragma Resources.Title')) {
            Assert-True ($text -notlike "*$unexpected*") "exe21sp Core 默认值不应补回 [$unexpected]:$text"
        }
    }
}

Add-Test @{
    Name   = 'exe21sp.windowed'
    Group  = 'exe21sp'
    Deps   = $deps
    Builds = @(
        @{ Name = 'win'; InputText = "Get-Date | Out-Null; Write-Output 'windowed-embed'"; Params = @{ App = @{ Windowed = $true } }; Output = 'windowed_std.exe' }
        @{ Name = 'con'; InputText = "Get-Date | Out-Null; Write-Output 'console-embed'"; Output = 'console_std.exe' }
        @{ Name = 'srcwin'; InputText = "#_pragma App.Windowed`nGet-Date | Out-Null; Write-Output 'srcwin-embed'"; Output = 'src_windowed.exe' }
        @{ Name = 'tsgui'; InputText = "'tinysharp-gui-windowed'"; Params = @{ App = @{ Windowed = $true }; Resources = @{ Title = 'CI' } }; Output = 'ts_windowed.exe' }
    )
    Run    = {
        param($ctx)
        $win = Get-Exe21spContent -ExePath $ctx.Builds['win']
        Assert-Match $win 'windowed-embed' "windowed 脚本内容:$win"
        Assert-Equal 1 ([regex]::Matches($win, '(?m)^\s*#_pragma\s+App\.Windowed\b')).Count "windowed exe 应补回一次 App.Windowed:$win"

        $con = Get-Exe21spContent -ExePath $ctx.Builds['con']
        Assert-Match $con 'console-embed' "console 脚本内容:$con"
        Assert-True ($con -notmatch '(?m)^\s*#_pragma\s+App\.Windowed\b') "console exe 不应补 App.Windowed:$con"

        $srcwin = Get-Exe21spContent -ExePath $ctx.Builds['srcwin']
        Assert-Match $srcwin 'srcwin-embed' "源码含 pragma 的脚本内容:$srcwin"
        Assert-Equal 1 ([regex]::Matches($srcwin, '(?m)^\s*#_pragma\s+App\.Windowed\b')).Count "源码已有 pragma 时不应重复:$srcwin"

        $tsgui = Get-Exe21spContent -ExePath $ctx.Builds['tsgui']
        Assert-Match $tsgui 'tinysharp-gui-windowed' "TinySharp windowed 脚本内容:$tsgui"
        Assert-Equal 1 ([regex]::Matches($tsgui, '(?m)^\s*#_pragma\s+App\.Windowed\b')).Count "TinySharp windowed 应补回 App.Windowed:$tsgui"
    }
}

Add-Test @{
    Name  = 'exe21sp.windowed-core'
    Group = 'exe21sp'
    Deps  = $deps
    Build = @{ Name = 'corewin'; InputText = "Get-Date | Out-Null; Write-Output 'core-windowed-embed'"; Params = @{ Build = @{ Target = 'Core' }; App = @{ Windowed = $true } }; Output = 'core_windowed.exe' }
    Run   = {
        param($ctx)
        if (-not (Get-Command dotnet -ErrorAction Ignore)) { throw 'Core 目标需要 .NET SDK(dotnet)' }
        $text = Get-Exe21spContent -ExePath $ctx.Builds['corewin']
        Assert-Match $text 'core-windowed-embed' "Core windowed 脚本内容:$text"
        Assert-Equal 1 ([regex]::Matches($text, '(?m)^\s*#_pragma\s+App\.Windowed\b')).Count "Core windowed 应补回 App.Windowed:$text"
    }
}

Add-Test @{
    Name   = 'exe21sp.require'
    Group  = 'exe21sp'
    Deps   = $deps
    Builds = @(
        @{ Name = 'single'; InputText = "#_require ps12exe`nWrite-Output 'require-single'"; Output = 'require_single.exe' }
        @{ Name = 'multi'; InputText = "#_require ps12exe`n#_require foo-bar`nWrite-Output 'require-multi'"; Output = 'require_multi.exe' }
        # 非完全匹配(把 -ea Stop 改成 -ea Continue)应原样保留,不误还原。
        @{ Name = 'fake'; InputText = "if(!(gmo ps12exe -ListAvailable -ea SilentlyContinue)){try{Import-PackageProvider NuGet}catch{Install-PackageProvider NuGet -Scope CurrentUser -Force -ea Ignore;Import-PackageProvider NuGet -ea Ignore};Install-Module ps12exe -Scope CurrentUser -Force -ea Continue}`nWrite-Output 'require-fake'"; Output = 'require_fake.exe' }
    )
    Run    = {
        param($ctx)
        $single = Get-Exe21spContent -ExePath $ctx.Builds['single']
        Assert-Match $single '(?m)^#_require ps12exe\s*$' "单模块 #_require 未还原:$single"
        Assert-NotMatch $single 'Install-Module' "单模块头代码未被替换:$single"

        $multi = Get-Exe21spContent -ExePath $ctx.Builds['multi']
        Assert-Match $multi '(?m)^#_require ps12exe\s*$' "多模块第一行未还原:$multi"
        Assert-Match $multi '(?m)^#_require foo-bar\s*$' "多模块第二行未还原:$multi"
        Assert-NotMatch $multi 'Install-Module' "多模块头代码未被替换:$multi"

        $fake = Get-Exe21spContent -ExePath $ctx.Builds['fake']
        Assert-NotMatch $fake '(?m)^#_require' "非完全匹配不应还原:$fake"
        Assert-Match $fake 'Install-Module ps12exe' "非完全匹配应保留原头代码:$fake"

        # 往返:还原出的 #_require 重新编译后应再次还原为同样的 #_require。
        $rtInput = Join-Path $ctx.WorkDir 'require_rt.ps1'
        [System.IO.File]::WriteAllText($rtInput, $single, [System.Text.UTF8Encoding]::new($false))
        $rtExe = Join-Path $ctx.WorkDir 'require_rt.exe'
        ps12exe -inputFile $rtInput -outputFile $rtExe -NoUpdateCheck | Out-Null
        $rt = Get-Exe21spContent -ExePath $rtExe
        Assert-Match $rt '(?m)^#_require ps12exe\s*$' "往返后 #_require 丢失:$rt"
    }
}