#Requires -Version 5.1
<#
.SYNOPSIS
Windows / PowerShell host smoke for patchloom (create, replace, doc, tx, paths).
.DESCRIPTION
Fixrealloop-style dogfood under PowerShell: nested relative paths (backslash
form on Windows; forward-slash nested form on macOS/Linux pwsh), absolute
paths, JSON error_kind peels, insert-after, contain, multi-op tx, rename
--force overwrite, path-only binary rename (#2031), and hardlink
sibling replace.
Not part of make check. CI runs on ci-windows; local pwsh on macOS/Linux is
supported for peels/tx/rename dogfood (Windows-only path spelling is gated).
.PARAMETER Bin
Path to patchloom.exe (default: target\debug\patchloom.exe relative to repo root).
.EXAMPLE
pwsh -File scripts/windows-smoke.ps1 -Bin target\debug\patchloom.exe
#>
param(
[string]$Bin = ""
)
$ErrorActionPreference = "Stop"
$script:Passes = 0
$script:Fails = 0
function Pass([string]$Msg) {
Write-Host "OK: $Msg"
$script:Passes++
}
function Fail([string]$Msg) {
Write-Host "FAIL: $Msg" -ForegroundColor Red
$script:Fails++
}
# Resolve binary
$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
if (-not $Bin) {
$candidates = @(
(Join-Path $RepoRoot "target\debug\patchloom.exe"),
(Join-Path $RepoRoot "target\release\patchloom.exe"),
(Join-Path $RepoRoot "target\debug\patchloom"),
(Join-Path $RepoRoot "target\release\patchloom")
)
foreach ($c in $candidates) {
if (Test-Path -LiteralPath $c) { $Bin = $c; break }
}
}
if (-not $Bin -or -not (Test-Path -LiteralPath $Bin)) {
throw "patchloom binary not found. Build first (cargo build --all-features) or pass -Bin"
}
$Bin = (Resolve-Path -LiteralPath $Bin).Path
Write-Host "BIN=$Bin"
function Invoke-Pl {
param(
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$Args
)
$out = & $Bin @Args 2>&1 | Out-String
return @{
ExitCode = $LASTEXITCODE
Output = $out
}
}
function Get-JsonField {
param([string]$JsonText, [string]$Name)
try {
$obj = $JsonText | ConvertFrom-Json
$prop = $obj.PSObject.Properties[$Name]
if ($null -eq $prop) { return $null }
return $prop.Value
} catch {
return $null
}
}
# Host OS: full backslash + drive-letter paths only on Windows. On macOS/Linux
# with pwsh (local dogfood of peels/tx/rename), use native nested paths so
# `make windows-smoke` is an honest pass without a Windows runner.
$IsWin = $false
if ($PSVersionTable.PSEdition -eq "Core") {
$IsWin = [bool]$IsWindows
} else {
$IsWin = ($env:OS -eq "Windows_NT")
}
Write-Host "HOST_WINDOWS=$IsWin"
$ws = Join-Path ([System.IO.Path]::GetTempPath()) ("pl-win-smoke-" + [guid]::NewGuid().ToString("N").Substring(0, 8))
New-Item -ItemType Directory -Path $ws | Out-Null
try {
Push-Location $ws
git init -q 2>$null | Out-Null
git commit --allow-empty -q -m init 2>$null | Out-Null
# --- create / already_exists / force ---
Set-Content -Path (Join-Path $ws "taken.txt") -Value "old`n" -NoNewline
$r = Invoke-Pl --json --cwd $ws create taken.txt --content "new"
$kind = Get-JsonField $r.Output "error_kind"
$applied = Get-JsonField $r.Output "applied"
if ($r.ExitCode -eq 1 -and $kind -eq "already_exists" -and ($applied -eq $false -or "$applied" -eq "False")) {
Pass "create dest-exists already_exists (PowerShell)"
} else {
$snip = if ($r.Output.Length -gt 200) { $r.Output.Substring(0, 200) } else { $r.Output }
Fail "create dest-exists exit=$($r.ExitCode) kind=$kind applied=$applied out=$snip"
}
if ($r.Output -match "force") {
Pass "already_exists message mentions force"
} else {
Fail "already_exists message missing force hint: $($r.Output)"
}
$r = Invoke-Pl --json --cwd $ws create taken.txt --content "forced`n" --force --apply
$got = Get-Content -LiteralPath (Join-Path $ws "taken.txt") -Raw
if ($r.ExitCode -eq 0 -and $got -match "forced") {
Pass "create --force --apply"
} else {
Fail "create force exit=$($r.ExitCode) content=$got"
}
# --- nested relative path under --cwd (backslash form is Windows-agent style) ---
$sub = Join-Path $ws (Join-Path "sub" "nested")
New-Item -ItemType Directory -Path $sub -Force | Out-Null
Set-Content -LiteralPath (Join-Path $sub "app.rs") -Value "use std::io;`nfn main() {}`n"
if ($IsWin) {
$rel = "sub\nested\app.rs"
$pathLabel = "backslash relative path"
} else {
$rel = "sub/nested/app.rs"
$pathLabel = "nested relative path (non-Windows pwsh)"
}
$r = Invoke-Pl --cwd $ws replace "use std::io;" --insert-after "use std::fs;" $rel --apply
$body = Get-Content -LiteralPath (Join-Path $sub "app.rs") -Raw
if ($r.ExitCode -eq 0 -and $body -match "use std::io;" -and $body -match "use std::fs;") {
Pass "insert-after with $pathLabel"
} else {
Fail "insert-after exit=$($r.ExitCode) body=$body out=$($r.Output)"
}
# --- absolute path (native to this host) ---
$absFile = Join-Path $ws "abs_target.txt"
Set-Content -LiteralPath $absFile -Value "hello world`n"
$r = Invoke-Pl replace "world" --new "windows" $absFile --apply
$body = Get-Content -LiteralPath $absFile -Raw
if ($r.ExitCode -eq 0 -and $body -match "hello windows") {
Pass "replace via absolute path"
} else {
Fail "abs replace exit=$($r.ExitCode) body=$body"
}
# --- doc set on JSON ---
$cfg = Join-Path $ws "config.json"
Set-Content -LiteralPath $cfg -Value '{"server":{"port":8080}}' -NoNewline
$r = Invoke-Pl --cwd $ws doc set config.json server.port 9090 --apply
$cfgBody = Get-Content -LiteralPath $cfg -Raw
if ($r.ExitCode -eq 0 -and $cfgBody -match "9090") {
Pass "doc set"
} else {
Fail "doc set exit=$($r.ExitCode) body=$cfgBody"
}
# --- tx plan with key alias (ops + key) ---
$plan = Join-Path $ws "plan.json"
@'
{"ops":[{"op":"doc.set","path":"config.json","key":"server.port","value":7070}]}
'@ | Set-Content -LiteralPath $plan -NoNewline
$r = Invoke-Pl --cwd $ws tx plan.json --apply
$cfgBody = Get-Content -LiteralPath $cfg -Raw
if ($r.ExitCode -eq 0 -and $cfgBody -match "7070") {
Pass "tx plan key alias"
} else {
Fail "tx exit=$($r.ExitCode) body=$cfgBody out=$($r.Output)"
}
# --- multi-op tx create two files ---
$plan2 = Join-Path $ws "plan2.json"
@{
version = 1
operations = @(
@{ op = "file.create"; path = "t1.txt"; content = "a`n" }
@{ op = "file.create"; path = "t2.txt"; content = "b`n" }
)
} | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $plan2
$r = Invoke-Pl --cwd $ws --json tx plan2.json --apply
if ($r.ExitCode -eq 0 -and (Test-Path (Join-Path $ws "t1.txt")) -and (Test-Path (Join-Path $ws "t2.txt"))) {
Pass "tx multi file.create"
} else {
Fail "tx multi exit=$($r.ExitCode) out=$($r.Output)"
}
# --- preview exit 2, no write ---
Set-Content -LiteralPath (Join-Path $ws "prev.txt") -Value "alpha`n"
$r = Invoke-Pl --cwd $ws replace alpha --new beta prev.txt
$still = Get-Content -LiteralPath (Join-Path $ws "prev.txt") -Raw
if ($r.ExitCode -eq 2 -and $still -match "alpha") {
Pass "preview exit 2 no write"
} else {
Fail "preview exit=$($r.ExitCode) content=$still"
}
# --- --contain rejects escape (absolute path outside workspace) ---
Set-Content -LiteralPath (Join-Path $ws "in.txt") -Value "x`n"
# Path must exist so we hit PathGuard, not a not_found peel before the guard.
if ($IsWin) {
$escape = "C:\Windows\System32\drivers\etc\hosts"
} else {
$escape = "/etc/hosts"
if (-not (Test-Path -LiteralPath $escape)) {
$escape = "/"
}
}
$r = Invoke-Pl --json --cwd $ws --contain read $escape
$kind = Get-JsonField $r.Output "error_kind"
if ($kind -eq "guard_rejected") {
Pass "contain escape guard_rejected"
} else {
$snip = if ($r.Output.Length -gt 250) { $r.Output.Substring(0, 250) } else { $r.Output }
Fail "contain kind=$kind exit=$($r.ExitCode) out=$snip"
}
# --- not_found delete ---
$r = Invoke-Pl --json --cwd $ws delete missing-nope.txt --apply
$kind = Get-JsonField $r.Output "error_kind"
if ($kind -eq "not_found" -and $r.ExitCode -eq 1) {
Pass "delete missing not_found"
} else {
Fail "delete kind=$kind exit=$($r.ExitCode)"
}
# --- rename dest already_exists ---
Set-Content -LiteralPath (Join-Path $ws "from.txt") -Value "a`n"
Set-Content -LiteralPath (Join-Path $ws "to.txt") -Value "b`n"
$r = Invoke-Pl --json --cwd $ws rename from.txt to.txt --apply
$kind = Get-JsonField $r.Output "error_kind"
if ($kind -eq "already_exists") {
Pass "rename dest already_exists"
} else {
Fail "rename kind=$kind out=$($r.Output)"
}
# --- rename --force overwrites destination ---
$r = Invoke-Pl --json --cwd $ws rename from.txt to.txt --force --apply
$to = Get-Content -LiteralPath (Join-Path $ws "to.txt") -Raw
$fromExists = Test-Path -LiteralPath (Join-Path $ws "from.txt")
if ($r.ExitCode -eq 0 -and $to -match "a" -and -not $fromExists) {
Pass "rename --force overwrites dest"
} else {
Fail "rename force exit=$($r.ExitCode) to=$to fromExists=$fromExists out=$($r.Output)"
}
# --- replace keeps hardlink siblings in sync ---
$hlA = Join-Path $ws "hl-a.txt"
$hlB = Join-Path $ws "hl-b.txt"
Set-Content -LiteralPath $hlA -Value "shared`n"
try {
New-Item -ItemType HardLink -Path $hlB -Target $hlA | Out-Null
$r = Invoke-Pl --json --cwd $ws replace shared --new CHANGED hl-a.txt --apply
$aTxt = Get-Content -LiteralPath $hlA -Raw
$bTxt = Get-Content -LiteralPath $hlB -Raw
if ($r.ExitCode -eq 0 -and $aTxt -match "CHANGED" -and $bTxt -match "CHANGED") {
Pass "replace preserves hardlink siblings"
} else {
Fail "hardlink replace exit=$($r.ExitCode) a=$aTxt b=$bTxt out=$($r.Output)"
}
} catch {
Fail "hardlink setup failed: $_"
}
# --- replace via 8.3 name keeps the long directory entry ---
if ($IsWin) {
$long8 = Join-Path $ws "LongFileName.txt"
Set-Content -LiteralPath $long8 -Value "old8`n" -NoNewline
try {
$fso = New-Object -ComObject Scripting.FileSystemObject
$short8 = $fso.GetFile($long8).ShortPath
$shortLeaf = if ($short8) { Split-Path -Leaf $short8 } else { "" }
if ($shortLeaf -and ($shortLeaf -match '~')) {
$r = Invoke-Pl --json --cwd $ws replace old8 --new NEW8 $shortLeaf --apply
$longLeft = Test-Path -LiteralPath $long8
$longBody = if ($longLeft) { Get-Content -LiteralPath $long8 -Raw } else { "" }
$extraShort = Test-Path -LiteralPath (Join-Path $ws $shortLeaf)
if ($r.ExitCode -eq 0 -and $longLeft -and $longBody -match "NEW8") {
Pass "replace via 8.3 keeps long name"
} else {
Fail "8.3 replace exit=$($r.ExitCode) longLeft=$longLeft extra=$extraShort body=$longBody out=$($r.Output)"
}
} else {
Write-Host "SKIP: 8.3 short names disabled"
}
} catch {
Write-Host "SKIP: 8.3 probe failed ($_)"
}
}
# --- path-only binary rename (NUL byte; #2031 host contract on Windows) ---
$binSrc = Join-Path $ws "blob.bin"
$binDst = Join-Path $ws "blob-moved.bin"
[System.IO.File]::WriteAllBytes($binSrc, [byte[]](0x00, 0x01, 0x02, 0xff))
$r = Invoke-Pl --json --cwd $ws rename blob.bin blob-moved.bin --apply
$dstBytes = if (Test-Path -LiteralPath $binDst) { [System.IO.File]::ReadAllBytes($binDst) } else { @() }
$srcGone = -not (Test-Path -LiteralPath $binSrc)
if ($r.ExitCode -eq 0 -and $srcGone -and $dstBytes.Length -eq 4 -and $dstBytes[0] -eq 0) {
Pass "binary rename path-only preserves bytes"
} else {
$snip = if ($r.Output.Length -gt 200) { $r.Output.Substring(0, 200) } else { $r.Output }
Fail "binary rename exit=$($r.ExitCode) srcGone=$srcGone len=$($dstBytes.Length) out=$snip"
}
# --- batch PATH OLD NEW ---
Set-Content -LiteralPath (Join-Path $ws "ver.txt") -Value "v1`n"
$ops = Join-Path $ws "ops.txt"
"replace ver.txt v1 v2" | Set-Content -LiteralPath $ops
$r = Invoke-Pl --cwd $ws batch ops.txt --apply
$v = Get-Content -LiteralPath (Join-Path $ws "ver.txt") -Raw
if ($r.ExitCode -eq 0 -and $v -match "v2") {
Pass "batch replace PATH OLD NEW"
} else {
Fail "batch exit=$($r.ExitCode) ver=$v"
}
# --- search no_matches exit 3 ---
Set-Content -LiteralPath (Join-Path $ws "only.txt") -Value "only`n"
$r = Invoke-Pl --json --cwd $ws search nomatch only.txt
if ($r.ExitCode -eq 3) {
Pass "search no_matches exit 3"
} else {
Fail "search exit=$($r.ExitCode)"
}
# --- CRLF file insert-after keeps CR LF ---
$crlf = Join-Path $ws "crlf.txt"
[System.IO.File]::WriteAllBytes($crlf, [byte[]](0x6c, 0x31, 0x0d, 0x0a, 0x6c, 0x32, 0x0d, 0x0a)) # l1\r\nl2\r\n
$r = Invoke-Pl --cwd $ws replace "l1" --insert-after "// post" crlf.txt --apply
$bytes = [System.IO.File]::ReadAllBytes($crlf)
$hex = ($bytes | ForEach-Object { $_.ToString("x2") }) -join " "
# Expect l1\r\n// post\r\n...
$asText = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($r.ExitCode -eq 0 -and $asText -match "l1\r\n// post\r\n") {
Pass "CRLF insert-after preserves line endings"
} elseif ($r.ExitCode -eq 0 -and $asText -match "// post") {
# Still applied; mixed endings would be a product bug
if ($asText -match "l1\n// post" -and $asText -notmatch "l1\r\n// post") {
Fail "CRLF insert used bare LF: $hex"
} else {
Pass "CRLF insert-after applied ($hex)"
}
} else {
Fail "CRLF exit=$($r.ExitCode) hex=$hex"
}
# --- delete directory junction unlinks the link, not the target ---
if ($IsWin) {
$jreal = Join-Path $ws "jreal"
$jlink = Join-Path $ws "jlink"
New-Item -ItemType Directory -Path $jreal | Out-Null
Set-Content -LiteralPath (Join-Path $jreal "keep.txt") -Value "keep`n" -NoNewline
$mklink = cmd /c mklink /J $jlink $jreal
if ($LASTEXITCODE -eq 0) {
$r = Invoke-Pl --json --cwd $ws delete jlink --apply
$keep = Test-Path -LiteralPath (Join-Path $jreal "keep.txt")
$linkLeft = Test-Path -LiteralPath $jlink
$jApplied = Get-JsonField $r.Output "applied"
if ($r.ExitCode -eq 0 -and $keep -and -not $linkLeft -and ("$jApplied" -eq "True" -or $jApplied -eq $true)) {
Pass "delete junction unlinks link not target"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "junction delete exit=$($r.ExitCode) keep=$keep linkLeft=$linkLeft applied=$jApplied out=$snip"
}
} else {
Write-Host "SKIP: mklink /J failed ($mklink)"
}
# Name with `&` must unlink the reparse point (not become cmd operators).
$jampReal = Join-Path $ws "jamp-real"
$jampLink = Join-Path $ws "jamp&echo"
New-Item -ItemType Directory -Path $jampReal | Out-Null
Set-Content -LiteralPath (Join-Path $jampReal "keep.txt") -Value "keep`n" -NoNewline
try {
New-Item -ItemType Junction -LiteralPath $jampLink -Target $jampReal | Out-Null
$r = Invoke-Pl --json --cwd $ws delete "jamp&echo" --apply
$keep = Test-Path -LiteralPath (Join-Path $jampReal "keep.txt")
$linkLeft = Test-Path -LiteralPath $jampLink
$jApplied = Get-JsonField $r.Output "applied"
if ($r.ExitCode -eq 0 -and $keep -and -not $linkLeft -and ("$jApplied" -eq "True" -or $jApplied -eq $true)) {
Pass "delete junction with & unlinks link not target"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "junction & delete exit=$($r.ExitCode) keep=$keep linkLeft=$linkLeft applied=$jApplied out=$snip"
}
} catch {
Write-Host "SKIP: junction with & create failed ($_)"
}
}
# --- readonly replace fail-restore is applied:false (Windows attribute) ---
if ($IsWin) {
$ro = Join-Path $ws "ro-win.txt"
Set-Content -LiteralPath $ro -Value "hello-win`n" -NoNewline
attrib +R $ro
$r = Invoke-Pl --json --cwd $ws replace hello-win --new HELLO-WIN ro-win.txt --apply
$roKind = Get-JsonField $r.Output "error_kind"
$roApplied = Get-JsonField $r.Output "applied"
$roBody = Get-Content -LiteralPath $ro -Raw
attrib -R $ro
if ($r.ExitCode -eq 1 -and $roKind -eq "rollback_failed" -and ("$roApplied" -eq "False" -or $roApplied -eq $false) -and $roBody -match "hello-win") {
Pass "readonly replace fail-restore applied:false"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "readonly replace exit=$($r.ExitCode) kind=$roKind applied=$roApplied body=$roBody out=$snip"
}
}
# --- ADS create is invalid_input, not applied ---
if ($IsWin) {
$r = Invoke-Pl --json --cwd $ws create "ads.txt:stream" --content x --apply
$adsKind = Get-JsonField $r.Output "error_kind"
$adsApplied = Get-JsonField $r.Output "applied"
if ($r.ExitCode -eq 1 -and $adsKind -eq "invalid_input" -and ("$adsApplied" -ne "True")) {
Pass "ADS create invalid_input not applied"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "ADS create exit=$($r.ExitCode) kind=$adsKind applied=$adsApplied out=$snip"
}
$r = Invoke-Pl --json --cwd $ws create "bad<name.txt" --content x --apply
$badKind = Get-JsonField $r.Output "error_kind"
$badApplied = Get-JsonField $r.Output "applied"
if ($r.ExitCode -eq 1 -and $badKind -eq "invalid_input" -and ("$badApplied" -ne "True")) {
Pass "illegal dest create invalid_input not applied"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "illegal dest exit=$($r.ExitCode) kind=$badKind applied=$badApplied out=$snip"
}
Set-Content -LiteralPath (Join-Path $ws "file.txt") -Value "KEEP`n" -NoNewline
$r = Invoke-Pl --json --cwd $ws create "file.txt " --content OVER --force --apply
$spKind = Get-JsonField $r.Output "error_kind"
$spApplied = Get-JsonField $r.Output "applied"
$kept = (Get-Content -LiteralPath (Join-Path $ws "file.txt") -Raw)
if ($r.ExitCode -eq 1 -and $spKind -eq "invalid_input" -and ("$spApplied" -ne "True") -and $kept -eq "KEEP`n") {
Pass "trailing-space dest invalid_input does not overwrite"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "trailing-space dest exit=$($r.ExitCode) kind=$spKind applied=$spApplied body=$kept out=$snip"
}
}
# --- --glob *.txt matches Hit.TXT (Windows case-insensitive names) ---
if ($IsWin) {
Set-Content -LiteralPath (Join-Path $ws "Hit.TXT") -Value "needle`n" -NoNewline
$r = Invoke-Pl --json --cwd $ws --glob "*.txt" search needle
$gCount = Get-JsonField $r.Output "match_count"
if ($r.ExitCode -eq 0 -and "$gCount" -eq "1") {
Pass "glob *.txt matches Hit.TXT"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "glob case exit=$($r.ExitCode) count=$gCount out=$snip"
}
}
# --- .gitignore *.log drops app.LOG (git core.ignorecase on Windows) ---
if ($IsWin) {
$ig = Join-Path $ws "igcase"
New-Item -ItemType Directory -Path (Join-Path $ig ".git") | Out-Null
Set-Content -LiteralPath (Join-Path $ig ".gitignore") -Value "*.log`n" -NoNewline
Set-Content -LiteralPath (Join-Path $ig "app.LOG") -Value "needle`n" -NoNewline
Set-Content -LiteralPath (Join-Path $ig "keep.txt") -Value "needle`n" -NoNewline
$r = Invoke-Pl --json --cwd $ig search needle
$igCount = Get-JsonField $r.Output "match_count"
$igOut = $r.Output
if ($r.ExitCode -eq 0 -and "$igCount" -eq "1" -and ($igOut -notmatch "(?i)app\.log")) {
Pass "gitignore *.log drops app.LOG"
} else {
$snip = if ($igOut.Length -gt 240) { $igOut.Substring(0, 240) } else { $igOut }
Fail "gitignore case exit=$($r.ExitCode) count=$igCount out=$snip"
}
}
# --- --contain accepts in-workspace \\localhost\C$\... ---
if ($IsWin) {
$uncFile = Join-Path $ws "unc-in.txt"
Set-Content -LiteralPath $uncFile -Value "x`n" -NoNewline
$drive = $ws.Substring(0, 1)
$rest = $ws.Substring(2).TrimStart('\')
$unc = "\\localhost\${drive}`$\${rest}\unc-in.txt"
$r = Invoke-Pl --json --cwd $ws --contain replace x --new y $unc --apply
$uncBody = Get-Content -LiteralPath $uncFile -Raw
if ($r.ExitCode -eq 0 -and $uncBody -eq "y`n") {
Pass "contain localhost C`$ in-workspace"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "UNC contain exit=$($r.ExitCode) body=$uncBody out=$snip"
}
$unc6File = Join-Path $ws "unc-v6.txt"
Set-Content -LiteralPath $unc6File -Value "x`n" -NoNewline
$unc6 = "\\[::1]\${drive}`$\${rest}\unc-v6.txt"
$r6 = Invoke-Pl --json --cwd $ws --contain replace x --new y $unc6 --apply
$unc6Body = Get-Content -LiteralPath $unc6File -Raw
if ($r6.ExitCode -eq 0 -and $unc6Body -eq "y`n") {
Pass "contain IPv6 loopback C`$ in-workspace"
} else {
$snip = if ($r6.Output.Length -gt 240) { $r6.Output.Substring(0, 240) } else { $r6.Output }
Fail "IPv6 UNC contain exit=$($r6.ExitCode) body=$unc6Body out=$snip"
}
}
# --- create dest past MAX_PATH without \\?\ ---
if ($IsWin) {
$longLeaf = ("L" * 240) + ".txt"
$longDest = Join-Path $ws $longLeaf
$r = Invoke-Pl --json --cwd $ws create $longDest --content "hi`n" --apply
$longBody = $null
try {
$longBody = [System.IO.File]::ReadAllText("\\?\$longDest")
} catch {
$longBody = $null
}
if ($r.ExitCode -eq 0 -and $longBody -eq "hi`n") {
Pass "create long dest without verbatim prefix"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "long create exit=$($r.ExitCode) body=$longBody out=$snip"
}
}
# --- replace keeps Zone.Identifier (MOTW) through atomic persist ---
if ($IsWin) {
$motw = Join-Path $ws "motw.txt"
Set-Content -LiteralPath $motw -Value "old line`n" -NoNewline
Set-Content -LiteralPath "${motw}:Zone.Identifier" -Value "[ZoneTransfer]`r`nZoneId=3`r`n" -NoNewline
$r = Invoke-Pl --json --cwd $ws replace old --new new motw.txt --apply
$motwBody = Get-Content -LiteralPath $motw -Raw
$zone = $null
try { $zone = Get-Content -LiteralPath $motw -Stream Zone.Identifier -Raw -ErrorAction Stop } catch { $zone = $null }
if ($r.ExitCode -eq 0 -and $motwBody -match "new line" -and $zone -match "ZoneId=3") {
Pass "replace keeps Zone.Identifier MOTW"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "MOTW persist exit=$($r.ExitCode) body=$motwBody zone=$zone out=$snip"
}
$ads = Join-Path $ws "ads.txt"
Set-Content -LiteralPath $ads -Value "old line`n" -NoNewline
Set-Content -LiteralPath "${ads}:custom" -Value "secret" -NoNewline
Set-Content -LiteralPath "${ads}:Zone.Identifier" -Value "[ZoneTransfer]`r`nZoneId=3`r`n" -NoNewline
$r = Invoke-Pl --json --cwd $ws replace old --new new ads.txt --apply
$adsBody = Get-Content -LiteralPath $ads -Raw
$custom = $null
$adsZone = $null
try { $custom = Get-Content -LiteralPath $ads -Stream custom -Raw -ErrorAction Stop } catch { $custom = $null }
try { $adsZone = Get-Content -LiteralPath $ads -Stream Zone.Identifier -Raw -ErrorAction Stop } catch { $adsZone = $null }
if ($r.ExitCode -eq 0 -and $adsBody -match "new line" -and $custom -match "secret" -and $adsZone -match "ZoneId=3") {
Pass "replace keeps custom named stream and MOTW"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "custom ADS persist exit=$($r.ExitCode) body=$adsBody custom=$custom zone=$adsZone out=$snip"
}
}
# --- doc set JSON with UTF-8 BOM (Notepad/VS) ---
if ($IsWin) {
$bomJson = Join-Path $ws "bom.json"
[System.IO.File]::WriteAllBytes($bomJson, [byte[]](0xEF, 0xBB, 0xBF) + [Text.Encoding]::UTF8.GetBytes("{`"k`":1}`n"))
$r = Invoke-Pl --json --cwd $ws doc set bom.json k 2 --apply
$bomBody = [System.IO.File]::ReadAllText($bomJson)
if ($r.ExitCode -eq 0 -and $bomBody -match '"k"\s*:\s*2') {
Pass "doc set JSON UTF-8 BOM"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "doc BOM JSON exit=$($r.ExitCode) body=$bomBody out=$snip"
}
}
# --- doc get multi-doc YAML with UTF-8 BOM (Notepad --- first) ---
if ($IsWin) {
$bomYaml = Join-Path $ws "bom-multi.yaml"
[System.IO.File]::WriteAllBytes($bomYaml, [byte[]](0xEF, 0xBB, 0xBF) + [Text.Encoding]::UTF8.GetBytes("---`r`na: 1`r`n---`r`nb: 2`r`n"))
$r = Invoke-Pl --json --cwd $ws doc get bom-multi.yaml 0.a
if ($r.ExitCode -eq 0 -and $r.Output -match '"value"\s*:\s*1') {
Pass "doc get YAML multi-doc UTF-8 BOM"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "doc BOM YAML multi-doc exit=$($r.ExitCode) out=$snip"
}
}
# --- batch file with UTF-8 BOM (Notepad/VS) ---
if ($IsWin) {
$bomHit = Join-Path $ws "bom-batch.txt"
Set-Content -LiteralPath $bomHit -Value "old`n" -NoNewline
$bomBat = Join-Path $ws "bom-ops.batch"
[System.IO.File]::WriteAllBytes($bomBat, [byte[]](0xEF, 0xBB, 0xBF) + [Text.Encoding]::UTF8.GetBytes("replace bom-batch.txt old new`n"))
$r = Invoke-Pl --json --cwd $ws batch $bomBat --apply
$hitBody = [System.IO.File]::ReadAllText($bomHit)
if ($r.ExitCode -eq 0 -and $hitBody -eq "new`n") {
Pass "batch file UTF-8 BOM"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "batch BOM exit=$($r.ExitCode) body=$hitBody out=$snip"
}
}
# --- tx JSON plan with UTF-8 BOM (Notepad/VS) ---
if ($IsWin) {
$bomHit = Join-Path $ws "bom-tx.txt"
Set-Content -LiteralPath $bomHit -Value "old`n" -NoNewline
$bomPlan = Join-Path $ws "bom-plan.json"
$planJson = "{`"ops`":[{`"op`":`"replace`",`"path`":`"bom-tx.txt`",`"old`":`"old`",`"new`":`"new`"}]}"
[System.IO.File]::WriteAllBytes($bomPlan, [byte[]](0xEF, 0xBB, 0xBF) + [Text.Encoding]::UTF8.GetBytes($planJson))
$r = Invoke-Pl --json --cwd $ws tx $bomPlan --apply
$hitBody = [System.IO.File]::ReadAllText($bomHit)
if ($r.ExitCode -eq 0 -and $hitBody -eq "new`n") {
Pass "tx JSON plan UTF-8 BOM"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "tx BOM plan exit=$($r.ExitCode) body=$hitBody out=$snip"
}
}
# --- replace via //?/C:/... (CopyFile rejects that spelling) ---
if ($IsWin) {
# Isolated dir so we do not pick an earlier session's manifest
# (Get-ChildItem on the shared $ws first hit taken.txt).
$fwdWs = Join-Path $ws "fwd-prefix"
New-Item -ItemType Directory -Path $fwdWs | Out-Null
$fwdHit = Join-Path $fwdWs "fwd.txt"
Set-Content -LiteralPath $fwdHit -Value "old`n" -NoNewline
$fwd = "//?/" + ($fwdHit -replace '\\', '/')
Push-Location $fwdWs
try {
$r = Invoke-Pl --json replace old --new new --apply $fwd
} finally {
Pop-Location
}
$fwdBody = [System.IO.File]::ReadAllText($fwdHit)
$rel = $null
$manifests = Get-ChildItem -Path (Join-Path $fwdWs ".patchloom\backups") -Filter manifest.json -Recurse -ErrorAction SilentlyContinue
if ($manifests) {
$rel = (Get-Content -Raw $manifests[0].FullName | ConvertFrom-Json).entries[0].path
}
if ($r.ExitCode -eq 0 -and $fwdBody -eq "new`n" -and $rel -eq "fwd.txt") {
Pass "replace forward extended prefix"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "forward extended replace exit=$($r.ExitCode) body=$fwdBody rel=$rel out=$snip"
}
$cWs = Join-Path $ws "fwd-contain"
New-Item -ItemType Directory -Path $cWs | Out-Null
$cHit = Join-Path $cWs "con.txt"
Set-Content -LiteralPath $cHit -Value "in`n" -NoNewline
$cFwd = "//?/" + ($cHit -replace '\\', '/')
Push-Location $cWs
try {
$r = Invoke-Pl --json --contain replace in --new out --apply $cFwd
} finally {
Pop-Location
}
$cBody = [System.IO.File]::ReadAllText($cHit)
if ($r.ExitCode -eq 0 -and $cBody -eq "out`n") {
Pass "contain forward extended prefix in-workspace"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "contain //?/ in-ws exit=$($r.ExitCode) body=$cBody out=$snip"
}
$dWs = Join-Path $ws "dot-device"
New-Item -ItemType Directory -Path $dWs | Out-Null
$dHit = Join-Path $dWs "dev.txt"
Set-Content -LiteralPath $dHit -Value "old`n" -NoNewline
$dDev = "\\.\" + $dHit
Push-Location $dWs
try {
$r = Invoke-Pl --json replace old --new new --apply $dDev
} finally {
Pop-Location
}
$dBody = [System.IO.File]::ReadAllText($dHit)
if ($r.ExitCode -eq 0 -and $dBody -eq "new`n") {
Pass "replace dot-device drive dest"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "dot-device replace exit=$($r.ExitCode) body=$dBody out=$snip"
}
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$r = Invoke-Pl --json read '\\.\CON'
$sw.Stop()
if ($r.ExitCode -ne 0 -and $r.Output -match "not a file name|invalid_input" -and $sw.Elapsed.TotalSeconds -lt 15) {
Pass "read CON device dest refused"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "read CON exit=$($r.ExitCode) ms=$($sw.Elapsed.TotalMilliseconds) out=$snip"
}
$r = Invoke-Pl --json create '\\.\NUL' --content x --apply
if ($r.ExitCode -ne 0 -and $r.Output -match "not a file name|invalid_input") {
Pass "create NUL device dest refused"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "create NUL exit=$($r.ExitCode) out=$snip"
}
$r = Invoke-Pl --json create NUL --content x --apply
if ($r.ExitCode -ne 0 -and $r.Output -match "not a file name|invalid_input") {
Pass "create bare NUL dest refused"
} else {
$snip = if ($r.Output.Length -gt 240) { $r.Output.Substring(0, 240) } else { $r.Output }
Fail "create bare NUL exit=$($r.ExitCode) out=$snip"
}
}
# --- version ---
$r = Invoke-Pl --version
if ($r.ExitCode -eq 0 -and $r.Output -match "patchloom") {
Pass "version"
} else {
Fail "version: $($r.Output)"
}
} finally {
Pop-Location
Remove-Item -LiteralPath $ws -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Host "==== windows-smoke: passes=$script:Passes fails=$script:Fails ===="
if ($script:Fails -gt 0) {
exit 1
}
exit 0