$ErrorActionPreference = 'Continue' # Self-contained regression suite for the lights-out skill. # pwsh -NoProfile -File \scripts\test\e2e.ps1 # Uses a stubbed gnhf and a sandboxed state dir + vault, so it never touches the real # registry at ~\.scout\lights-out or the Obsidian vault. $sand = Join-Path $env:TEMP 'lights-out-e2e' $stubSrc = Join-Path $PSScriptRoot 'stub-gnhf.ps1' $stub = Join-Path $sand 'bin' $env:LIGHTS_OUT_ROOT = "$sand\state" $env:LIGHTS_OUT_VAULT_DIR = "$sand\vault" $scriptsDir = Split-Path -Parent $PSScriptRoot $S = Join-Path $scriptsDir 'start-run.ps1' $C = Join-Path $scriptsDir 'check-run.ps1' Remove-Item -Recurse -Force $sand -ErrorAction SilentlyContinue New-Item -ItemType Directory -Force -Path "$sand\repo", $stub | Out-Null # Get-Command resolves .ps1 from PATH, so the stub must be named exactly gnhf.ps1. Copy-Item $stubSrc (Join-Path $stub 'gnhf.ps1') -Force 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++ } } # Load the shared lib so the suite can assert on resolution directly. . (Join-Path $scriptsDir 'lib.ps1') function Resolve-GnhfCommandForTest { Resolve-GnhfCommand } Show 'TEST 0: vendored gnhf is self-contained' $vendored = Join-Path (Split-Path -Parent $scriptsDir) 'vendor\node_modules\gnhf\dist\cli.mjs' Check 'vendored gnhf present' (Test-Path $vendored) $env:LIGHTS_OUT_GNHF = '' $resolved = Resolve-GnhfCommand 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 1: empty prompt rejected' try { & $S -RepoPath "$sand\repo" -Prompt ' ' | Out-Null; Check 'empty prompt rejected' $false } catch { Check 'empty prompt rejected' $true } Show 'TEST 2: dirty tree + missing verify tool (pnpm trap)' 'dirty' | Set-Content "$sand\repo\scratch.txt" $r2 = (& $S -RepoPath "$sand\repo" -Prompt 'tidy things up' -VerifyCommand 'pnpm run test') | ConvertFrom-Json Check 'preflight failed' (-not $r2.ok) Check 'dirty tree reported' ([bool]($r2.problems -match 'dirty')) Check 'missing pnpm reported' ([bool]($r2.problems -match 'pnpm')) Remove-Item "$sand\repo\scratch.txt" -Force Show 'TEST 3: already on a gnhf/* branch (non-TTY overwrite trap)' Push-Location "$sand\repo"; git checkout -q -b 'gnhf/fake'; Pop-Location $r3 = (& $S -RepoPath "$sand\repo" -Prompt 'tidy things up') | ConvertFrom-Json Check 'gnhf-branch launch blocked' (-not $r3.ok) Push-Location "$sand\repo"; git checkout -q main; git branch -q -D 'gnhf/fake'; Pop-Location Show 'TEST 4: unknown agent CLI blocked' $r4 = (& $S -RepoPath "$sand\repo" -Prompt 'tidy things up' -Agent 'notarealagent') | ConvertFrom-Json Check 'missing agent CLI blocked' (-not $r4.ok) Show 'TEST 5: happy path with stubbed gnhf' $env:LIGHTS_OUT_GNHF = (Join-Path $stub 'gnhf.ps1') Check 'stub gnhf resolves via override' ((Resolve-GnhfCommandForTest).Kind -eq 'override') $out = & $S -RepoPath "$sand\repo" -Prompt 'reduce complexity in the widget helper' -Title 'Reduce Complexity' -StopWhen 'npm test passes 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) 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', 'orphaned')) { break } } 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' ($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 'notes tail captured' ([bool]$final.notesTail) Check 'gnhf provenance recorded' ($final.gnhfKind -eq 'override') } Show 'Stub received correct gnhf 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 'reduce complexity in the widget helper') } else { Check 'stub was invoked' $false } 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 '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_GNHF = '' Remove-Item -Recurse -Force $sand -ErrorAction SilentlyContinue if ($script:fail -gt 0) { exit 1 }