AI skills in use in my daily
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

524 lines
29 KiB

$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 }