$ErrorActionPreference = 'Continue'
# Self-contained regression suite for the lights-out skill.
# pwsh -NoProfile -File <skill>\scripts\test\e2e.ps1
# Uses a stubbed nightshift engine and a sandboxed state dir + vault, so it never touches the
# real registry at ~\.scout\lights-out or the Obsidian vault.
$sandRoot = Join-Path $env:TEMP 'lights-out-e2e'
$sand = Join-Path $sandRoot 'active'
$stubSrc = Join-Path $PSScriptRoot 'stub-nightshift.ps1'
$stub = Join-Path $sand 'bin'
$env:LIGHTS_OUT_ROOT = " $ sand\state "
$env:LIGHTS_OUT_VAULT_DIR = " $ sand\vault "
$scriptsDir = Split-Path -Parent $PSScriptRoot
$skillRoot = Split-Path -Parent $scriptsDir
$S = Join-Path $scriptsDir 'start-run.ps1'
$C = Join-Path $scriptsDir 'check-run.ps1'
function Wait-SandboxWorkerExit {
param ( [string] $StatePath )
if ( -not ( Test-Path -LiteralPath $StatePath ) ) { return }
try {
$runs = @ ( Get-Content -LiteralPath $StatePath -Raw | ConvertFrom-Json )
}
catch {
return
}
foreach ( $run in $runs ) {
if ( -not $run . workerPid ) { continue }
for ( $i = 0 ; $i -lt 30 ; $i + + ) {
if ( -not ( Get-Process -Id $run . workerPid -ErrorAction SilentlyContinue ) ) { break }
Start-Sleep -Seconds 1
}
}
}
function Reset-Sandbox {
param ( [string] $Path )
Wait-SandboxWorkerExit -StatePath ( Join-Path $Path 'state\runs.json' )
for ( $i = 0 ; $i -lt 30 ; $i + + ) {
Remove-Item -Recurse -Force $Path -ErrorAction SilentlyContinue
if ( -not ( Test-Path -LiteralPath $Path ) ) { return }
Start-Sleep -Seconds 1
}
throw " Could not reset sandbox at $ Path. "
}
New-Item -ItemType Directory -Force -Path $sandRoot | Out-Null
Reset-Sandbox -Path $sand
New-Item -ItemType Directory -Force -Path " $ sand\repo " , $stub | Out-Null
Copy-Item $stubSrc ( Join-Path $stub 'stub-nightshift.ps1' ) -Force
@"
@echo off
pwsh -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " $ stub\stub-nightshift.ps1 " % *
exit / b % ERRORLEVEL %
" @ | Set-Content -LiteralPath (Join-Path $ stub 'nightshift.cmd') -Encoding ASCII
@ '
@echo off
exit / b 0
'@ | Set-Content -LiteralPath (Join-Path $stub ' cursor-agent . cmd ' ) -Encoding ASCII
Push-Location " $ sand\repo "
git init -q -b main
git config user . email 't@t.local'
git config user . name 'T'
'# scratch' | Set-Content README . md
'export const widget = (x) => { if (true) { return x + 1; } };' | Set-Content index . js
git add -A
git commit -q -m 'init'
Pop-Location
function Show ( $n ) { Write-Host " `n ### $ n " -ForegroundColor Yellow }
$script:pass = 0
$script:fail = 0
function Check ( $label , $cond ) {
if ( $cond ) { Write-Host " PASS $ label " -ForegroundColor Green ; $script:pass + + }
else { Write-Host " FAIL $ label " -ForegroundColor Red ; $script:fail + + }
}
function Has-Property ( $Object , $Name ) {
$null -ne $Object -and $Object . PSObject . Properties . Name -contains $Name
}
. ( Join-Path $scriptsDir 'lib.ps1' )
function Resolve-NightshiftCommandForTest {
if ( Get-Command Resolve-NightshiftCommand -ErrorAction SilentlyContinue ) {
Resolve-NightshiftCommand
}
else {
$null
}
}
$skillDoc = Get-Content ( Join-Path $skillRoot 'SKILL.md' ) -Raw
$cliDoc = Get-Content ( Join-Path $skillRoot 'references\cli.md' ) -Raw
$promptDoc = Get-Content ( Join-Path $skillRoot 'references\prompts.md' ) -Raw
$startText = Get-Content $S -Raw
$checkText = Get-Content $C -Raw
$libText = Get-Content ( Join-Path $scriptsDir 'lib.ps1' ) -Raw
$agentFn = [regex] :: Match (
$startText ,
'function Get-AgentCommandName\s*\{.*?^\}' ,
[System.Text.RegularExpressions.RegexOptions] :: Singleline -bor
[System.Text.RegularExpressions.RegexOptions] :: Multiline
)
if ( $agentFn . Success ) { Invoke-Expression $agentFn . Value }
Show 'TEST 0: rename surface and public API'
Check 'skill mentions nightshift trigger' ( $skillDoc -match '(?i)nightshift' )
Check 'skill documents schema break generically' ( $skillDoc -match '(?i)legacy prefix' )
Check 'cli doc uses .nightshift run path' ( $cliDoc -match '\\\.nightshift\\runs\\<runId>\\' )
Check 'cli doc names the config file' ( $cliDoc -match '~\\\.nightshift\\config\.yml' )
Check 'prompt templates still mention lights out' ( $promptDoc -match '(?i)lights out' )
Check 'start-run exposes engine output field' ( $startText -match '\bengine\s*=' )
Check 'check-run exposes engine exit code' ( $checkText -match '\bengineExitCode\b' )
Check 'lib resolves nightshift command' ( $libText -match 'Resolve-NightshiftCommand' )
Check 'start-run keeps Agent parameter' ( $startText -match '\[string\]\$Agent' )
Check 'start-run keeps Prompt parameter' ( $startText -match '\[string\]\$Prompt' )
Check 'start-run keeps StopWhen parameter' ( $startText -match '\[string\]\$StopWhen' )
Check 'start-run keeps VerifyCommand parameter' ( $startText -match '\[string\]\$VerifyCommand' )
Check 'worker handoff uses registryId' ( $startText -match " '-RunStateId',\s*\ `$ run\.registryId " )
Check 'worker owns workerPid persistence' (
[regex] :: IsMatch ( $startText , 'if \(\$Phase -eq ''Worker''\).*workerPid.*\$PID.*Save-LightsOutRun' , [System.Text.RegularExpressions.RegexOptions] :: Singleline ) -and
-not [regex] :: IsMatch ( $startText , 'NotePropertyName workerPid -NotePropertyValue \$process\.Id -Force\s*\r?\nSave-LightsOutRun' )
)
Check 'launch state starts as launching' ( $startText -match " status\s+=\s+'launching' " )
Check 'console log uses registryId' ( $startText -match 'consoleLogStem.*registryId' )
Check 'vault folder includes registry id' ( $libText -match 'registryId' -and $libText -match 'Get-LightsOutVaultFolder' )
Check 'wrapper parses engine run dir from console log' ( $libText -match 'Resolve-LightsOutRunDirFromConsoleLog' )
Check 'worktree fallback filters old run dirs' ( $libText -match 'LastWriteTimeUtc' -and $libText -match 'startedAt' )
Check 'worktree resolution has no branch-only fallback' ( $libText -notmatch 'if \(\$branch -eq \$Run\.branch\)' )
Check 'worktree verify skips unresolved checkout' ( $startText -match 'skipped-unresolved-checkout' )
Check 'Get-AgentCommandName is still present' ( $agentFn . Success )
if ( $agentFn . Success ) {
Check 'Get-AgentCommandName keeps cursor mapping' ( ( Get-AgentCommandName -AgentName 'cursor' ) -eq 'cursor-agent' )
Check 'Get-AgentCommandName keeps acp bypass' ( $null -eq ( Get-AgentCommandName -AgentName 'acp:loopback' ) )
}
Show 'TEST 1: vendored nightshift is self-contained'
$vendored = Join-Path $skillRoot 'vendor\node_modules\nightshift\dist\cli.mjs'
Check 'vendored nightshift present' ( Test-Path $vendored )
if ( Test-Path $vendored ) {
$bundle = Get-Content -LiteralPath $vendored -Raw
Check 'vendored bundle has no analytics client or configuration' ( $bundle -notmatch 'a\.kunchenguid\.com|NIGHTSHIFT_TELEMETRY|NIGHTSHIFT_UMAMI_HOST|NIGHTSHIFT_UMAMI_WEBSITE_ID|/api/send|initDefaultTelemetry|getDefaultTelemetry|createTelemetryClient' )
}
$env:LIGHTS_OUT_NIGHTSHIFT = ''
$resolved = Resolve-NightshiftCommandForTest
Check 'resolves to vendored by default' ( $resolved -and $resolved . Kind -eq 'vendored' )
Check 'vendored version reported' ( $resolved -and $resolved . Version )
if ( $resolved ) { Write-Host " -> $( $resolved . Kind ) v $( $resolved . Version ) : $( $resolved . Source ) " }
Show 'TEST 2: empty prompt rejected'
try { & $S -RepoPath " $ sand\repo " -Prompt ' ' | Out-Null ; Check 'empty prompt rejected' $false }
catch { Check 'empty prompt rejected' $true }
Show 'TEST 3: dirty tree + missing verify tool'
'dirty' | Set-Content " $ sand\repo\scratch.txt "
$r3 = ( & $S -RepoPath " $ sand\repo " -Prompt 'tidy things up' -VerifyCommand 'definitely-missing-lightsout-tool run test' ) | ConvertFrom-Json
Check 'preflight failed' ( -not $r3 . ok )
Check 'dirty tree reported' ( [bool] ( $r3 . problems -match 'dirty' ) )
Check 'missing verify tool reported' ( [bool] ( $r3 . problems -match 'definitely-missing-lightsout-tool' ) )
Remove-Item " $ sand\repo\scratch.txt " -Force
Show 'TEST 4: already on a nightshift branch (non-TTY overwrite trap)'
Push-Location " $ sand\repo " ; git checkout -q -b 'nightshift/fake' ; Pop-Location
$r4 = ( & $S -RepoPath " $ sand\repo " -Prompt 'tidy things up' ) | ConvertFrom-Json
Check 'nightshift-branch launch blocked' ( -not $r4 . ok )
Push-Location " $ sand\repo " ; git checkout -q main ; git branch -q -D 'nightshift/fake' ; Pop-Location
Show 'TEST 5: unknown agent CLI blocked'
$r5 = ( & $S -RepoPath " $ sand\repo " -Prompt 'tidy things up' -Agent 'notarealagent' ) | ConvertFrom-Json
Check 'missing agent CLI blocked' ( -not $r5 . ok )
Show 'TEST 6: copied skill happy path with stubbed nightshift'
$copiedSkill = Join-Path $sand 'skill copy with spaces'
Copy-Item -LiteralPath $skillRoot -Destination $copiedSkill -Recurse
$copiedStart = Join-Path $copiedSkill 'scripts\start-run.ps1'
$copiedCli = Join-Path $copiedSkill 'vendor\node_modules\nightshift\dist\cli.mjs'
$copiedVersion = & node $copiedCli - -version
Check 'copied engine runs without installation' ( $LASTEXITCODE -eq 0 -and $copiedVersion -eq '0.1.51-lights-out' )
$env:LIGHTS_OUT_NIGHTSHIFT = ( Join-Path $stub 'nightshift.cmd' )
$resolvedOverride = Resolve-NightshiftCommandForTest
Check 'stub nightshift resolves via override' ( $resolvedOverride -and $resolvedOverride . Kind -eq 'override' )
$out = & $copiedStart -RepoPath " $ sand\repo " -Prompt 'reduce complexity in the widget helper' -Title 'Reduce Complexity' -StopWhen 'git --version exits 0 and no unrelated files changed' -VerifyCommand 'git --version' -MaxIterations 5 -Mode companion -Agent 'copilot'
$res = $out | ConvertFrom-Json
Check 'launch ok' ( $res . ok -eq $true )
Check 'launch output keeps engine field' ( $res . PSObject . Properties . Name -contains 'engine' )
Check 'launch output keeps verifyCommand field' ( $res . verifyCommand -eq 'git --version' )
Check 'launch output uses nightshift branch' ( $res . branch -like 'nightshift/*' )
Write-Host " run id: $( $res . id ) pid: $( $res . workerPid ) "
Show 'Waiting for detached worker...'
$final = $null
for ( $i = 0 ; $i -lt 45 ; $i + + ) {
Start-Sleep -Seconds 2
$j = & $C -Id $res . id -Json
if ( $j ) { $final = ( $j | ConvertFrom-Json ) [ 0 ] }
if ( $final -and $final . status -in @ ( 'completed' , 'failed' ) ) { break }
}
if ( $final -and $final . status -notin @ ( 'completed' , 'failed' ) ) {
Start-Sleep -Seconds 2
$j = & $C -Id $res . id -Json
if ( $j ) { $final = ( $j | ConvertFrom-Json ) [ 0 ] }
}
Check 'worker finished' ( $null -ne $final )
if ( $final ) {
Check 'status completed' ( $final . status -eq 'completed' )
Check 'two commits captured' ( $final . commitCount -eq 2 )
Check 'end-state parsed' ( ( Has-Property $final 'endState' ) -and ( Has-Property $final . endState 'status' ) -and ( $final . endState . status -eq 'completed' ) )
Check 'verify ran before and after' ( ( $final . verifyExitCodeBefore -eq 0 ) -and ( $final . verifyExitCodeAfter -eq 0 ) )
Check 'run dir resolved' ( $null -ne $final . runDir )
Check 'run dir uses .nightshift path' ( $final . runDir -match '\\\.nightshift\\runs\\' )
Check 'notes tail captured' ( [bool] $final . notesTail )
Check 'engine provenance recorded' ( ( Has-Property $final 'engineKind' ) -and ( $final . engineKind -eq 'override' ) )
Check 'engine exit code recorded' ( ( Has-Property $final 'engineExitCode' ) -and ( $final . engineExitCode -eq 0 ) )
}
Show 'State file uses engine-prefixed fields'
$statePath = Join-Path $env:LIGHTS_OUT_ROOT 'runs.json'
$savedRuns = Get-Content $statePath -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json
$savedRun = if ( $savedRuns ) { @ ( $savedRuns | Where-Object { $_ . id -eq $res . id } ) [ 0 ] } else { $null }
Check 'state file written' ( $null -ne $savedRun )
if ( $savedRun ) {
Check 'state records engineKind' ( $savedRun . PSObject . Properties . Name -contains 'engineKind' )
Check 'state records engineSource' ( $savedRun . PSObject . Properties . Name -contains 'engineSource' )
Check 'state records engineVersion' ( $savedRun . PSObject . Properties . Name -contains 'engineVersion' )
Check 'state records engineExitCode' ( $savedRun . PSObject . Properties . Name -contains 'engineExitCode' )
}
Show 'TEST 6b: worktree runs finalize against generated worktree'
$verifyWorktreeScript = Join-Path $sand 'verify-worktree.ps1'
'if ((Get-Content index.js -Raw) -match ''=> x \+ 1'') { exit 0 } else { exit 1 }' | Set-Content -LiteralPath $verifyWorktreeScript -Encoding UTF8
$verifyWorktreeCommand = " pwsh -NoProfile -File `" $ verifyWorktreeScript `" "
$unsuffixedRepo = Join-Path $sand 'unsuffixed-worktree-repo'
New-Item -ItemType Directory -Force -Path $unsuffixedRepo | Out-Null
Push-Location $unsuffixedRepo
git init -q -b main
git config user . email 't@t.local'
git config user . name 'T'
'# unsuffixed worktree scratch' | Set-Content README . md
'export const widget = (x) => { if (true) { return x + 1; } };' | Set-Content index . js
git add -A
git commit -q -m 'init'
Pop-Location
$unsuffixedRunId = Get-LightsOutRunId -Prompt 'simplify widget in an unsuffixed worktree'
$outUnsuffixed = & $copiedStart -RepoPath $unsuffixedRepo -Prompt 'simplify widget in an unsuffixed worktree' -Title 'Unsuffixed Worktree' -StopWhen 'worktree-local verification exits 0' -VerifyCommand $verifyWorktreeCommand -MaxIterations 5 -Mode companion -Agent 'copilot' -Worktree
$resUnsuffixed = $outUnsuffixed | ConvertFrom-Json
Check 'unsuffixed worktree launch ok' ( $resUnsuffixed . ok -eq $true )
$finalUnsuffixed = $null
if ( $resUnsuffixed . PSObject . Properties . Name -contains 'id' ) {
for ( $i = 0 ; $i -lt 45 ; $i + + ) {
Start-Sleep -Seconds 2
$j = & $C -Id $resUnsuffixed . id -Json
if ( $j ) { $finalUnsuffixed = ( $j | ConvertFrom-Json ) [ 0 ] }
if ( $finalUnsuffixed -and $finalUnsuffixed . status -in @ ( 'completed' , 'failed' ) ) { break }
}
}
Check 'unsuffixed worktree completed on unsuffixed id' ( $finalUnsuffixed -and $finalUnsuffixed . id -eq $unsuffixedRunId )
$preWorkerUnsuffixed = [pscustomobject] @ {
registryId = [guid] :: NewGuid ( ) . ToString ( 'n' )
id = $unsuffixedRunId
launchId = $unsuffixedRunId
schemaVersion = 1
status = 'launching'
repoPath = $unsuffixedRepo
repoName = 'unsuffixed-worktree-repo'
branch = " nightshift/ $ unsuffixedRunId "
baseCommit = ( git -C $unsuffixedRepo rev-parse HEAD )
prompt = 'simplify widget in an unsuffixed worktree'
title = 'Unsuffixed Pre Worker'
agent = 'copilot'
model = ''
mode = 'companion'
stopWhen = 'worktree-local verification exits 0'
maxIterations = 5
maxTokens = 0
worktree = $true
currentBranch = $false
push = $false
mock = $false
verifyCommand = $verifyWorktreeCommand
verifyExitCodeBefore = 1
engineKind = 'override'
engineSource = $env:LIGHTS_OUT_NIGHTSHIFT
engineVersion = $null
startedAt = ( Get-Date ) . ToString ( 'o' )
launchedBy = 'scout:lights-out'
workerPid = $null
}
Save-LightsOutRun -Run $preWorkerUnsuffixed
$preWorkerUnsuffixedPoll = ( & $C -Id $preWorkerUnsuffixed . registryId -Json | ConvertFrom-Json ) [ 0 ]
Check 'unsuffixed pre-worker launch is not orphaned immediately' ( $preWorkerUnsuffixedPoll . status -eq 'launching' )
Check 'unsuffixed pre-worker polling has no borrowed checkout' ( -not $preWorkerUnsuffixedPoll . checkoutPath )
Check 'unsuffixed pre-worker polling has no borrowed run dir' ( -not $preWorkerUnsuffixedPoll . runDir )
Check 'unsuffixed pre-worker polling has no borrowed notes' ( -not $preWorkerUnsuffixedPoll . notesTail )
Check 'unsuffixed pre-worker polling has no borrowed branch result' ( -not $preWorkerUnsuffixedPoll . branchExists )
Check 'unsuffixed pre-worker polling has no borrowed commits' ( $preWorkerUnsuffixedPoll . commitCount -eq 0 )
Check 'unsuffixed pre-worker polling has no borrowed diff' ( -not $preWorkerUnsuffixedPoll . diffStat )
$worktreeRepo = Join-Path $sand 'worktree-repo'
New-Item -ItemType Directory -Force -Path $worktreeRepo | Out-Null
Push-Location $worktreeRepo
git init -q -b main
git config user . email 't@t.local'
git config user . name 'T'
'# worktree scratch' | Set-Content README . md
'export const widget = (x) => { if (true) { return x + 1; } };' | Set-Content index . js
git add -A
git commit -q -m 'init'
Pop-Location
$worktreeRunId = Get-LightsOutRunId -Prompt 'simplify widget in a worktree'
Push-Location $worktreeRepo
git branch " nightshift/ $ worktreeRunId "
Pop-Location
$outWorktree = & $copiedStart -RepoPath $worktreeRepo -Prompt 'simplify widget in a worktree' -Title 'Worktree Finalize' -StopWhen 'worktree-local verification exits 0' -VerifyCommand $verifyWorktreeCommand -MaxIterations 5 -Mode companion -Agent 'copilot' -Worktree
$resWorktree = $outWorktree | ConvertFrom-Json
Check 'worktree launch ok' ( $resWorktree . ok -eq $true )
if ( $resWorktree . ok -ne $true ) {
Write-Host ( ' problems: ' + ( $resWorktree . problems -join '; ' ) ) -ForegroundColor Red
}
Check 'worktree launch output uses nightshift branch' ( ( $resWorktree . PSObject . Properties . Name -contains 'branch' ) -and ( $resWorktree . branch -like 'nightshift/*' ) )
$finalWorktree = $null
if ( $resWorktree . PSObject . Properties . Name -contains 'id' ) {
for ( $i = 0 ; $i -lt 45 ; $i + + ) {
Start-Sleep -Seconds 2
$j = & $C -Id $resWorktree . id -Json
if ( $j ) { $finalWorktree = ( $j | ConvertFrom-Json ) [ 0 ] }
if ( $finalWorktree -and $finalWorktree . status -in @ ( 'completed' , 'failed' ) ) { break }
}
}
Check 'worktree worker finished' ( $null -ne $finalWorktree )
if ( $finalWorktree ) {
Check 'worktree collision recorded suffixed id' ( $finalWorktree . id -eq " $ worktreeRunId-1 " )
Check 'worktree registry id exposed' ( [bool] $finalWorktree . registryId )
Check 'worktree launch id preserved' ( $finalWorktree . launchId -eq $worktreeRunId )
Check 'worktree branch recorded suffixed branch' ( $finalWorktree . branch -eq " nightshift/ $ worktreeRunId-1 " )
Check 'worktree status completed' ( $finalWorktree . status -eq 'completed' )
Check 'worktree verify ran after in worktree' ( $finalWorktree . verifyExitCodeAfter -eq 0 )
Check 'worktree run dir resolved' ( [bool] $finalWorktree . runDir )
Check 'worktree run dir uses generated worktree' ( $finalWorktree . runDir -match " \\worktree-repo-nightshift-worktrees\\ $ worktreeRunId-1\\ " )
Check 'worktree notes tail captured' ( [bool] $finalWorktree . notesTail )
Check 'worktree vault note captured' ( [bool] $finalWorktree . vaultNote )
if ( $finalWorktree . vaultNote ) {
$worktreeBody = Get-Content $finalWorktree . vaultNote -Raw
Check 'worktree vault records checkout row' ( $worktreeBody -match '\| Checkout \|' )
Check 'worktree vault records suffixed branch' ( $worktreeBody -match [regex] :: Escape ( " | Branch | `` nightshift/ $ worktreeRunId-1 `` | " ) )
Check 'worktree vault saved diff patch' ( Test-Path ( Join-Path ( Split-Path -Parent $finalWorktree . vaultNote ) 'diff.patch' ) )
Check 'worktree vault path includes registry id' ( ( Split-Path -Leaf ( Split-Path -Parent $finalWorktree . vaultNote ) ) -match [regex] :: Escape ( $finalWorktree . registryId . Substring ( 0 , 8 ) ) )
}
}
Show 'TEST 6c: same-prompt pre-worker polling does not borrow old artifacts'
$preWorker = [pscustomobject] @ {
registryId = [guid] :: NewGuid ( ) . ToString ( 'n' )
id = $worktreeRunId
launchId = $worktreeRunId
schemaVersion = 1
status = 'launching'
repoPath = $worktreeRepo
repoName = 'worktree-repo'
branch = " nightshift/ $ worktreeRunId "
baseCommit = ( git -C $worktreeRepo rev-parse HEAD )
prompt = 'simplify widget in a worktree'
title = 'Worktree Pre Worker'
agent = 'copilot'
model = ''
mode = 'companion'
stopWhen = 'worktree-local verification exits 0'
maxIterations = 5
maxTokens = 0
worktree = $true
currentBranch = $false
push = $false
mock = $false
verifyCommand = $verifyWorktreeCommand
verifyExitCodeBefore = 1
engineKind = 'override'
engineSource = $env:LIGHTS_OUT_NIGHTSHIFT
engineVersion = $null
startedAt = ( Get-Date ) . ToString ( 'o' )
launchedBy = 'scout:lights-out'
workerPid = $null
}
Save-LightsOutRun -Run $preWorker
$preWorkerPoll = ( & $C -Id $preWorker . registryId -Json | ConvertFrom-Json ) [ 0 ]
Check 'pre-worker launch is not orphaned immediately' ( $preWorkerPoll . status -eq 'launching' )
Check 'pre-worker polling has no borrowed checkout' ( -not $preWorkerPoll . checkoutPath )
Check 'pre-worker polling has no borrowed run dir' ( -not $preWorkerPoll . runDir )
Check 'pre-worker polling has no borrowed notes' ( -not $preWorkerPoll . notesTail )
Check 'pre-worker polling has no borrowed branch result' ( -not $preWorkerPoll . branchExists )
Check 'pre-worker polling has no borrowed commits' ( $preWorkerPoll . commitCount -eq 0 )
Check 'pre-worker polling has no borrowed diff' ( -not $preWorkerPoll . diffStat )
Show 'TEST 6d: same-prompt worktree relaunch preserves registry history'
$outWorktreeAgain = & $copiedStart -RepoPath $worktreeRepo -Prompt 'simplify widget in a worktree' -Title 'Worktree Finalize Again' -StopWhen 'worktree-local verification exits 0' -VerifyCommand $verifyWorktreeCommand -MaxIterations 5 -Mode companion -Agent 'copilot' -Worktree
$resWorktreeAgain = $outWorktreeAgain | ConvertFrom-Json
Check 'same-prompt relaunch ok' ( $resWorktreeAgain . ok -eq $true )
$finalWorktreeAgain = $null
if ( $resWorktreeAgain . PSObject . Properties . Name -contains 'id' ) {
for ( $i = 0 ; $i -lt 45 ; $i + + ) {
Start-Sleep -Seconds 2
$j = & $C -Id $resWorktreeAgain . id -Json
if ( $j ) { $finalWorktreeAgain = ( $j | ConvertFrom-Json ) [ 0 ] }
if ( $finalWorktreeAgain -and $finalWorktreeAgain . status -in @ ( 'completed' , 'failed' ) ) { break }
}
}
Check 'same-prompt latest worker finished' ( $null -ne $finalWorktreeAgain )
if ( $finalWorktreeAgain ) {
Check 'same-prompt latest uses newer suffixed id' ( $finalWorktreeAgain . id -match " ^ $( [regex] :: Escape ( $worktreeRunId ) ) -\d+ $ " -and $finalWorktreeAgain . id -ne $finalWorktree . id )
Check 'same-prompt latest has distinct registry id' ( $finalWorktreeAgain . registryId -and $finalWorktree . registryId -and $finalWorktreeAgain . registryId -ne $finalWorktree . registryId )
$firstCheckout = if ( $finalWorktree . runDir ) { Split-Path -Parent ( Split-Path -Parent ( Split-Path -Parent $finalWorktree . runDir ) ) } else { $null }
$secondCheckout = if ( $finalWorktreeAgain . runDir ) { Split-Path -Parent ( Split-Path -Parent ( Split-Path -Parent $finalWorktreeAgain . runDir ) ) } else { $null }
Check 'same-prompt first preserved worktree still exists' ( $firstCheckout -and ( Test-Path -LiteralPath $firstCheckout ) )
Check 'same-prompt second preserved worktree exists' ( $secondCheckout -and ( Test-Path -LiteralPath $secondCheckout ) )
Check 'same-prompt preserved worktrees are distinct' ( $firstCheckout -and $secondCheckout -and $firstCheckout -ne $secondCheckout )
$history = @ ( Get-Content $statePath -Raw | ConvertFrom-Json | Where-Object { $_ . launchId -eq $worktreeRunId -and $_ . status -eq 'completed' } )
Check 'same-prompt registry preserves both runs' ( $history . Count -eq 2 )
Check 'same-prompt vault preserves both notes' ( @ ( Get-ChildItem -Recurse " $ sand\vault " -Filter 'summary.md' | Where-Object { $_ . FullName -like '*Worktree Finalize*' } ) . Count -eq 2 )
$latestByLaunchId = ( & $C -Id $worktreeRunId -Json | ConvertFrom-Json ) [ 0 ]
Check 'same-prompt launch id resolves latest run' ( $latestByLaunchId . id -eq $finalWorktreeAgain . id )
if ( $finalWorktree . registryId ) {
$priorByRegistryId = ( & $C -Id $finalWorktree . registryId -Json | ConvertFrom-Json ) [ 0 ]
Check 'same-prompt prior run remains addressable by registry id' ( $priorByRegistryId . id -eq " $ worktreeRunId-1 " )
}
}
Show 'Stub received correct nightshift flags'
$stubArgs = Get-Content " $ sand\state\stub-args.json " -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json
if ( $stubArgs ) {
Write-Host ( ' ' + ( $stubArgs -join ' ' ) )
Check 'passed --stop-when' ( [bool] ( $stubArgs -contains '--stop-when' ) )
Check 'passed --prevent-sleep' ( [bool] ( $stubArgs -contains '--prevent-sleep' ) )
Check 'passed --max-iterations' ( [bool] ( $stubArgs -contains '--max-iterations' ) )
Check 'prompt is last arg' ( $stubArgs [ - 1 ] -eq 'simplify widget in a worktree' )
}
else { Check 'stub was invoked' $false }
Show 'Legacy engine field fallback'
if ( $savedRun ) {
$savedRun | Add-Member -NotePropertyName archivalKind -NotePropertyValue $ ( if ( ( Has-Property $savedRun 'engineKind' ) ) { $savedRun . engineKind } else { $null } ) -Force
$savedRun | Add-Member -NotePropertyName archivalSource -NotePropertyValue $ ( if ( ( Has-Property $savedRun 'engineSource' ) ) { $savedRun . engineSource } else { $null } ) -Force
$savedRun | Add-Member -NotePropertyName archivalVersion -NotePropertyValue $ ( if ( ( Has-Property $savedRun 'engineVersion' ) ) { $savedRun . engineVersion } else { $null } ) -Force
$savedRun | Add-Member -NotePropertyName archivalExitCode -NotePropertyValue $ ( if ( ( Has-Property $savedRun 'engineExitCode' ) ) { $savedRun . engineExitCode } else { $null } ) -Force
if ( ( Has-Property $savedRun 'engineKind' ) ) { $savedRun . PSObject . Properties . Remove ( 'engineKind' ) }
if ( ( Has-Property $savedRun 'engineSource' ) ) { $savedRun . PSObject . Properties . Remove ( 'engineSource' ) }
if ( ( Has-Property $savedRun 'engineVersion' ) ) { $savedRun . PSObject . Properties . Remove ( 'engineVersion' ) }
if ( ( Has-Property $savedRun 'engineExitCode' ) ) { $savedRun . PSObject . Properties . Remove ( 'engineExitCode' ) }
@ ( $savedRuns ) | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $statePath -Encoding UTF8
$legacy = ( & $C -Id $res . id -Json | ConvertFrom-Json ) [ 0 ]
Check 'legacy kind still projects as engineKind' ( ( Has-Property $legacy 'engineKind' ) -and ( $legacy . engineKind -eq 'override' ) )
Check 'legacy source still projects as engineSource' ( ( Has-Property $legacy 'engineSource' ) -and [bool] $legacy . engineSource )
Check 'legacy version still projects as engineVersion' (
( Has-Property $legacy 'engineVersion' ) -or
( $null -eq $resolvedOverride . Version )
)
Check 'legacy exit still projects as engineExitCode' ( ( Has-Property $legacy 'engineExitCode' ) -and ( $legacy . engineExitCode -eq 0 ) )
}
Show 'Legacy engine descriptor inference'
$legacyOrdered = [pscustomobject][ordered] @ {
schemaVersion = 1
previousKind = 'path'
previousSource = 'C:\legacy\nightshift.cmd'
previousVersion = '0.1.49'
previousExitCode = 0
}
$legacyDescriptor = Get-RunEngineDescriptor -Run $legacyOrdered
Check 'schemaVersion does not override legacy version' ( $legacyDescriptor . Version -eq '0.1.49' )
Check 'legacy prefix projects exit code' ( $legacyDescriptor . ExitCode -eq 0 )
$preferredDescriptor = Get-RunEngineDescriptor -Run ( [pscustomobject][ordered] @ {
engineKind = 'vendored'
engineSource = 'C:\actual\cli.mjs'
previousKind = 'path'
previousSource = 'C:\legacy\nightshift.cmd'
previousVersion = '0.1.49'
} )
Check 'engine descriptor precedence keeps actual kind' ( $preferredDescriptor . Kind -eq 'vendored' )
Check 'engine descriptor precedence keeps actual source' ( $preferredDescriptor . Source -eq 'C:\actual\cli.mjs' )
Check 'engine descriptor precedence does not backfill version from legacy' ( $null -eq $preferredDescriptor . Version )
$spuriousDescriptor = Get-RunEngineDescriptor -Run ( [pscustomobject][ordered] @ {
schemaVersion = 1
randomVersion = '9.9.9'
verifyExitCodeBefore = 0
} )
Check 'no spurious version selected from unrelated fields' ( $null -eq $spuriousDescriptor . Version )
Check 'no spurious exit code selected from unrelated fields' ( $null -eq $spuriousDescriptor . ExitCode )
$ambiguousMessage = $null
try {
$null = Get-RunEngineDescriptor -Run ( [pscustomobject][ordered] @ {
previousKind = 'path'
previousSource = 'C:\legacy\nightshift.cmd'
archivalKind = 'override'
archivalSource = 'C:\alt\nightshift.cmd'
} )
}
catch {
$ambiguousMessage = $_ . Exception . Message
}
Check 'ambiguous legacy descriptors rejected explicitly' ( $ambiguousMessage -match 'Ambiguous' )
Show 'Vault output'
Get-ChildItem -Recurse " $ sand\vault " -File -ErrorAction SilentlyContinue | ForEach-Object { ' ' + $_ . FullName . Replace ( " $ sand\vault\ " , '' ) }
$note = Get-ChildItem -Recurse -Filter 'summary.md' " $ sand\vault " -ErrorAction SilentlyContinue | Select-Object -First 1
Check 'summary.md written' ( $null -ne $note )
if ( $note ) {
$body = Get-Content $note . FullName -Raw
Check 'has lights-out-run type' ( $body -match 'type: lights-out-run' )
Check 'has assessment stub' ( $body -match '## Assessment' )
Check 'records commits' ( $body -match 'refactor: simplify widget helper' )
Check 'records nightshift branch' ( $body -match '\| Branch \| `nightshift/' )
Check 'records engine row' ( $body -match '\| Engine \| override' )
Check 'diff.patch saved' ( Test-Path ( Join-Path $note . DirectoryName 'diff.patch' ) )
Check 'notes.md copied' ( Test-Path ( Join-Path $note . DirectoryName 'notes.md' ) )
$yamlOk = ( python -c " import yaml,io,sys; t=io.open(sys.argv[1],encoding='utf-8').read(); yaml.safe_load(t.split('---')[1]); print('OK') " $note . FullName 2 > & 1 ) -join ''
Check 'frontmatter is valid YAML' ( $yamlOk -match 'OK' )
if ( $yamlOk -notmatch 'OK' ) { Write-Host " $ yamlOk " -ForegroundColor Red }
Check 'ISO started timestamp' ( $body -match '\| Started \| \d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2} \|' )
}
Show " RESULT: $( $script:pass ) passed, $( $script:fail ) failed "
if ( $note ) { Write-Host " `n --- summary.md --- `n " ; Get-Content $note . FullName }
$env:LIGHTS_OUT_NIGHTSHIFT = ''
Remove-Item -Recurse -Force $sand -ErrorAction SilentlyContinue
if ( $script:fail -gt 0 ) { exit 1 }