$ErrorActionPreference = 'Continue' # Self-contained regression suite for the lights-out skill. # pwsh -NoProfile -File \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\\\\') 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 }