Repository navigation
v1.1.0: senior-dev pass — backlog, clone generator, Phase 3 pilot (reset_cursor v0.3) #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ee107a5
886c5d3
874346f
e8b0e74
a091aa8
852a621
e988954
3252785
836af20
5907291
5082102
63696f7
5fbf4ba
f8c477e
d528d58
70c3386
108eb49
6425d0d
8c4b68b
4992bad
59938bd
fda3d39
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| # Harness: exercises GHOST.bat menu input handling on a TEST COPY. | ||
| # Runs locally and in CI (ci.yml windows job, "GHOST.bat menu harness"); | ||
| # derives the repo root from its own location so it works on any checkout. | ||
| # stdin redirection is unusable here (the launcher's ESC-capture powershell | ||
| # child drains redirected stdin, starving every set /p), so the copy reads | ||
| # answers from %TESTINPUT% line-by-line via a for /f counter instead. | ||
| # Answers: one per line, CRLF; no empty lines (for /f skips them; the | ||
| # interactive bare-Enter path is covered by the 'if not defined choice' | ||
| # guards and a space-only answer exercises the tokenless-input path). | ||
| # NOTE: some cmd builds reject skip=0 inside for /f options with a parse | ||
| # error ("delims= was unexpected"), so paging goes through `more +N` | ||
| # (which accepts +0) instead of `skip=N`. | ||
| $ErrorActionPreference = 'Stop' | ||
| $repo = $PSScriptRoot | ||
| $work = Join-Path $env:TEMP 'ghost-bat-harness' | ||
| if (Test-Path $work) { Remove-Item $work -Recurse -Force } | ||
| New-Item -ItemType Directory -Path $work | Out-Null | ||
|
|
||
| # --- build the test copy ------------------------------------------------------- | ||
| $src = [System.IO.File]::ReadAllText("$repo\GHOST.bat") | ||
| $count = ([regex]::Matches($src, [regex]::Escape('set /p choice=%PROMPT_TEXT%'))).Count | ||
| # Floor 5 = main + the four hand-written submenus; generated clone suites | ||
| # (tools\new_clone_suite.ps1) add one prompt per new submenu and the global | ||
| # rewrite below covers every occurrence, so the check guards the floor only. | ||
| if ($count -lt 5) { throw "expected at least 5 set /p sites in GHOST.bat (main + 4 submenus), found $count" } | ||
| $copy = $src.Replace('set /p choice=%PROMPT_TEXT%', 'call :TestNextInput choice') | ||
| $sub = @' | ||
|
|
||
| :TestNextInput | ||
| rem === HARNESS-ONLY: next answer comes from %TESTINPUT% (TESTIDX = next line, 0-based). | ||
| rem skip=0 is a parse error on this cmd build, so page with more +N and keep | ||
| rem the first line; the read cap stops an exhausted-input runaway loop. | ||
| if not defined TESTIDX set "TESTIDX=0" | ||
| set /a TESTREADS=TESTREADS+1 | ||
| if %TESTREADS% GTR 200 ( | ||
| echo HARNESS: read cap hit - input exhausted in a loop | ||
| exit 9 | ||
| ) | ||
| set "TESTANSWER=" | ||
| for /f "usebackq delims=" %%a in (`more +%TESTIDX% "%TESTINPUT%"`) do ( | ||
| if not defined TESTANSWER set "TESTANSWER=%%a" | ||
| ) | ||
| set /a TESTIDX=TESTIDX+1 | ||
| set "%~1=%TESTANSWER%" | ||
| goto :eof | ||
| '@ | ||
| $copy = $copy + ($sub -replace "`n", "`r`n") | ||
| $testCopy = Join-Path $work 'TestGhost.bat' | ||
| [System.IO.File]::WriteAllText($testCopy, $copy, [System.Text.Encoding]::ASCII) | ||
|
|
||
| function New-Input([string]$Name, [string[]]$Lines) { | ||
| $p = Join-Path $work "$Name.in.txt" | ||
| [System.IO.File]::WriteAllText($p, (($Lines | ForEach-Object { $_ + "`r`n" }) -join '')) | ||
| return $p | ||
| } | ||
|
|
||
| function Invoke-Scenario([string]$Name, [string[]]$Lines) { | ||
| $inFile = New-Input $Name $Lines | ||
| $outFile = Join-Path $work "$Name.out.txt" | ||
| $env:TESTINPUT = $inFile | ||
| $env:TESTIDX = '0' | ||
| $env:TESTREADS = '0' | ||
| cmd /c "`"$testCopy`" > `"$outFile`" 2>&1" | ||
| Remove-Item Env:TESTINPUT -ErrorAction SilentlyContinue | ||
| Remove-Item Env:TESTIDX -ErrorAction SilentlyContinue | ||
| Remove-Item Env:TESTREADS -ErrorAction SilentlyContinue | ||
| return (Get-Content $outFile -Raw) | ||
| } | ||
|
|
||
| $failures = @() | ||
|
|
||
| # A) plain exit at main menu -> Done, zero Unknown task, zero literal junk | ||
| $out = Invoke-Scenario 'A_plain_exit' @('0') | ||
| if ($out -match 'Unknown task') { $failures += 'A: Unknown task on plain exit' } | ||
| if ($out -match '%choice|%PENDING_CHAIN%') { $failures += 'A: literal variable text leaked' } | ||
| if ($out -notmatch 'Done\.') { $failures += 'A: never reached exit' } | ||
|
|
||
| # B) open ZCode submenu (PENDING_CHAIN undefined), back, exit -> no bogus line | ||
| $out = Invoke-Scenario 'B_submenu_render' @('2', '0', '0') | ||
| if ($out -match 'Unknown task') { $failures += 'B: Unknown task on plain submenu render' } | ||
| if ($out -match '%PENDING_CHAIN%') { $failures += 'B: literal %PENDING_CHAIN% leaked' } | ||
| if ($out -notmatch 'ZCode Multi-Instance') { $failures += 'B: ZCode submenu not shown' } | ||
|
|
||
| # C) tokenless input inside ZCode submenu -> silent re-render, then back, exit | ||
| $out = Invoke-Scenario 'C_space_submenu' @('2', ' ', '0', '0') | ||
| if ($out -match 'Unknown task') { $failures += 'C: Unknown task on tokenless submenu input' } | ||
| if ($out -notmatch 'ZCode Multi-Instance') { $failures += 'C: ZCode submenu not shown' } | ||
|
|
||
| # D) invalid main-menu id -> exactly one legit Unknown task "99" | ||
| $out = Invoke-Scenario 'D_invalid_main' @('99', '0') | ||
| $hits = ([regex]::Matches($out, 'Unknown task "99"')).Count | ||
| if ($hits -ne 1) { $failures += "D: expected 1 legit Unknown task, got $hits" } | ||
|
|
||
| # E) main sub-path "2 99" -> chain reaches ZCodeOne, exactly one legit | ||
| # Unknown task, and the PENDING_CHAIN literal never appears | ||
| $out = Invoke-Scenario 'E_subpath_chain' @('2 99', '0', '0') | ||
| $hits = ([regex]::Matches($out, 'Unknown task "99"')).Count | ||
| if ($hits -ne 1) { $failures += "E: expected 1 Unknown task from chain, got $hits" } | ||
| if ($out -match '%PENDING_CHAIN%') { $failures += 'E: literal %PENDING_CHAIN% leaked' } | ||
| if ($out -notmatch 'ZCode Multi-Instance') { $failures += 'E: ZCode submenu not reached via sub-path' } | ||
|
|
||
| # F) new ZCode submenu entry renders: [17] Freeze diagnostic is listed | ||
| $out = Invoke-Scenario 'F_new_entry_render' @('2', '0', '0') | ||
| if ($out -notmatch '\[17\].*Freeze diagnostic') { $failures += 'F: ZCode submenu [17] Freeze diagnostic not rendered' } | ||
|
|
||
| # G) invalid ZCode submenu id -> exactly one legit Unknown task "42" | ||
| $out = Invoke-Scenario 'G_invalid_zcode' @('2', '42', '0', '0') | ||
| $hits = ([regex]::Matches($out, 'Unknown task "42"')).Count | ||
| if ($hits -ne 1) { $failures += "G: expected 1 Unknown task from ZCode submenu, got $hits" } | ||
|
|
||
| # H) open AutoClaw submenu (PENDING_CHAIN undefined), back, exit -> clean render | ||
| $out = Invoke-Scenario 'H_autoclaw_render' @('4', '0', '0') | ||
| if ($out -match 'Unknown task') { $failures += 'H: Unknown task on AutoClaw submenu render' } | ||
| if ($out -match '%PENDING_CHAIN%') { $failures += 'H: literal %PENDING_CHAIN% leaked' } | ||
| if ($out -notmatch 'AutoClaw Multi-Instance') { $failures += 'H: AutoClaw submenu not shown' } | ||
|
|
||
| # I) invalid AutoClaw chain id "4 42" -> exactly one legit Unknown task "42" | ||
| $out = Invoke-Scenario 'I_invalid_autoclaw' @('4 42', '0', '0') | ||
| $hits = ([regex]::Matches($out, 'Unknown task "42"')).Count | ||
| if ($hits -ne 1) { $failures += "I: expected 1 Unknown task from AutoClaw chain, got $hits" } | ||
| if ($out -match '%PENDING_CHAIN%') { $failures += 'I: literal %PENDING_CHAIN% leaked' } | ||
|
|
||
| # J) hostile main-menu input - :ValidateChoice must reject (never execute) | ||
| # the injected command, show the hint, re-prompt, then exit cleanly | ||
| $out = Invoke-Scenario 'J_hostile_main' @('1 & calc', '0') | ||
| if ($out -match 'calc') { $failures += 'J: injection attempt leaked through main prompt' } | ||
| if ($out -notmatch 'Invalid input') { $failures += 'J: rejection hint missing' } | ||
|
|
||
| # K) hostile main-menu input with parens - PWNED must never execute | ||
| $out = Invoke-Scenario 'K_hostile_parens' @('2 ) & echo PWNED', '0') | ||
| if ($out -match 'PWNED') { $failures += 'K: paren-amp injection executed' } | ||
|
|
||
| # L) hostile ZCode submenu input - rejected at the submenu prompt | ||
| $out = Invoke-Scenario 'L_hostile_zcode' @('2', '1 & calc', '0', '0') | ||
| if ($out -match 'calc') { $failures += 'L: injection attempt leaked through ZCode prompt' } | ||
| if ($out -notmatch 'Invalid input') { $failures += 'L: rejection hint missing' } | ||
|
|
||
| if ($failures.Count -gt 0) { | ||
| Write-Output 'HARNESS FAIL:' | ||
| $failures | ForEach-Object { Write-Output " - $_" } | ||
| exit 1 | ||
| } | ||
| Write-Output 'HARNESS PASS: all 12 scenarios clean' | ||
| # Explicit exit: pwsh -File propagates the last native \$LASTEXITCODE, and the | ||
| # scenarios' cmd exit code inherits GHOST.bat's `timeout` calls, which fail on | ||
| # CI consoles (no interactive stdin) AFTER the PASS line was already printed. | ||
| exit 0 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| name: Release | ||
|
|
||
| # Publishes a GitHub Release with a ZIP asset when a vX.Y.Z tag is pushed. | ||
| # The ZIP is a plain `git archive` (tracked files only) so its content matches | ||
| # exactly what update_ghost.ps1's download-ZIP path fetches and verifies. | ||
| # Prerequisites (enforced, never silent): the tag must have a matching | ||
| # "## [X.Y.Z]" section in CHANGELOG.md - its content becomes the release notes. | ||
|
|
||
| on: | ||
| push: | ||
| tags: ['v*'] | ||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
||
| jobs: | ||
| # The full CI suite runs first: a tag never publishes an untested tree. | ||
| tests: | ||
| name: CI gates (called) | ||
| uses: ./.github/workflows/ci.yml | ||
|
|
||
| release: | ||
| name: Build ZIP + publish GitHub Release | ||
| needs: tests | ||
| runs-on: windows-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Guard - tag must match a CHANGELOG section | ||
| shell: pwsh | ||
| run: | | ||
| $tag = $env:GITHUB_REF_NAME | ||
| if ($tag -notmatch '^v\d+\.\d+\.\d+$') { | ||
| Write-Error "Unexpected tag format '$tag' - expected vX.Y.Z" | ||
| exit 1 | ||
| } | ||
| $ver = $tag.Substring(1) | ||
| $section = Select-String -Path CHANGELOG.md -Pattern ('^## \[' + [regex]::Escape($ver) + '\]') | ||
| if (-not $section) { | ||
| Write-Error "CHANGELOG.md has no '## [$ver]' section - write the release notes before tagging $tag" | ||
| exit 1 | ||
| } | ||
| Write-Host "Tag $tag -> CHANGELOG line $($section.LineNumber): $($section.Line)" | ||
|
|
||
| - name: Build ZIP (git archive = tracked files, mirrors update_ghost.ps1's ZIP copy) | ||
| shell: pwsh | ||
| run: | | ||
| $tag = $env:GITHUB_REF_NAME | ||
| $zip = "ghost-$tag.zip" | ||
| git archive --format=zip --prefix="ghost-$tag/" --output="$zip" HEAD | ||
| $item = Get-Item -LiteralPath $zip | ||
| if ($item.Length -lt 100KB) { | ||
| Write-Error "ZIP suspiciously small: $($item.Length) bytes" | ||
| exit 1 | ||
| } | ||
| Write-Host "Built $zip ($([math]::Round($item.Length / 1MB, 2)) MB)" | ||
|
|
||
| - name: Extract release notes from the CHANGELOG section | ||
| shell: pwsh | ||
| run: | | ||
| $tag = $env:GITHUB_REF_NAME | ||
| $ver = $tag.Substring(1) | ||
| $lines = Get-Content -LiteralPath CHANGELOG.md | ||
| $start = -1 | ||
| for ($i = 0; $i -lt $lines.Count; $i++) { | ||
| if ($lines[$i] -match ('^## \[' + [regex]::Escape($ver) + '\]')) { $start = $i; break } | ||
| } | ||
| if ($start -lt 0) { Write-Error "Section '## [$ver]' vanished between guard and extract"; exit 1 } | ||
| $end = $lines.Count | ||
| for ($j = $start + 1; $j -lt $lines.Count; $j++) { | ||
| if ($lines[$j] -match '^## \[') { $end = $j; break } | ||
| } | ||
| $notes = $lines[$start..($end - 1)] -join "`r`n" | ||
| Set-Content -LiteralPath release-notes.md -Value $notes -Encoding utf8 | ||
| Write-Host "Extracted $($end - $start) note lines for [$ver]" | ||
|
|
||
| - name: Create the GitHub Release with the ZIP asset | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| shell: pwsh | ||
| run: | | ||
| $tag = $env:GITHUB_REF_NAME | ||
| gh release create "$tag" "ghost-$tag.zip" --title "GHOST $tag" --notes-file release-notes.md --verify-tag | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Check that release creation succeeded. If Based on learnings: “After each external command invocation, explicitly check $LASTEXITCODE.” 🤖 Prompt for AI AgentsSource: Learnings |
||
|
|
||
| - name: Verify-after - the asset is on the release (never silent-success) | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| shell: pwsh | ||
| run: | | ||
| $tag = $env:GITHUB_REF_NAME | ||
| $assets = @(gh release view "$tag" --json assets --jq '.assets[].name') | ||
| if ($assets -notcontains "ghost-$tag.zip") { | ||
| Write-Error "ZIP asset missing from release $tag (assets: $($assets -join ', '))" | ||
| exit 1 | ||
| } | ||
| Write-Host "Verified: release $tag carries ghost-$tag.zip" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Fail a scenario when
cmdexits with an error.Invoke-Scenariodiscards the batch process exit code. For example, the hostile-input scenarios can printInvalid inputand then abort; their output checks can still report a pass. Check$LASTEXITCODEimmediately aftercmd, before another native command can replace it. PowerShell does not make a failed native command throw solely because$ErrorActionPreferenceisStop. (learn.microsoft.com)Based on learnings: “After each external command invocation, explicitly check $LASTEXITCODE.”
🤖 Prompt for AI Agents
Source: Learnings