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