feat(annotate): support HTML files and URL annotation #703
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Test | |
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| push: | |
| branches: | |
| - main | |
| jobs: | |
| test: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Generate Pi extension shared copies | |
| run: bash apps/pi-extension/vendor.sh | |
| - name: Type check | |
| run: bun run typecheck | |
| - name: Run tests | |
| run: bun test | |
| install-cmd-windows: | |
| # End-to-end integration test for scripts/install.cmd on real cmd.exe. | |
| # The unit tests in scripts/install.test.ts are file-content string checks | |
| # that run on Linux and never exercise cmd's delayed-expansion parser or | |
| # the embedded `node -e` Gemini merge — exactly where issue #506 lived. | |
| # This job runs install.cmd end-to-end on Windows with a seeded ~/.gemini | |
| # settings.json fixture so the Gemini merge path actually executes and | |
| # any regression of #506 (or similar cmd-parser bugs) fails CI. | |
| name: install.cmd (Windows integration) | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Seed fake ~/.gemini/settings.json with pre-existing hook | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini" | Out-Null | |
| # Fixture mirrors the shape of a real Gemini settings.json (top-level | |
| # `hooks.BeforeTool` array plus unrelated sibling keys) but uses only | |
| # obviously-fake values. Must NOT contain the literal string | |
| # "plannotator" anywhere — install.cmd's Gemini block is gated on | |
| # `findstr /c:"plannotator"` returning non-zero and would otherwise | |
| # skip the merge entirely. | |
| $fixture = @' | |
| { | |
| "theme": "ci-fixture-theme", | |
| "hooks": { | |
| "BeforeTool": [ | |
| { | |
| "matcher": "ci-fixture-existing-matcher", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "ci-fixture-existing-command", | |
| "timeout": 1000 | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| "general": { | |
| "ciFixtureSentinel": true | |
| } | |
| } | |
| '@ | |
| Set-Content -Path "$env:USERPROFILE\.gemini\settings.json" -Value $fixture -NoNewline | |
| - name: Run install.cmd end-to-end | |
| shell: cmd | |
| # v0.17.1 is pinned intentionally. This test needs a real binary | |
| # to download so it can exercise install.cmd end-to-end — SHA256 | |
| # verification, skills sparse-checkout, and (critically) the | |
| # embedded `node -e` Gemini merge path that was the site of | |
| # issue #506. Using `latest` would couple the Windows regression | |
| # test to whatever version is currently released, so a bad | |
| # release would retroactively break CI on every branch. | |
| # | |
| # v0.17.1 was the current release when the test was added and is | |
| # locked in place by GitHub Immutable Releases. If you ever need | |
| # to bump it (e.g. this version becomes too old to represent the | |
| # install flow we care about), verify the replacement has: | |
| # - plannotator-win32-x64.exe attached | |
| # - plannotator-win32-x64.exe.sha256 attached | |
| # - the install.cmd and packages/shared/ layout your branch | |
| # under test expects to find in apps/skills/ | |
| # A missing or mismatched release asset surfaces as a curl 404 | |
| # in this step with no obvious connection to the test's purpose. | |
| run: scripts\install.cmd v0.17.1 --skip-attestation | |
| - name: Verify Gemini settings.json was merged correctly | |
| shell: pwsh | |
| run: | | |
| $path = "$env:USERPROFILE\.gemini\settings.json" | |
| if (-not (Test-Path $path)) { throw "settings.json missing after install" } | |
| # Must still parse as JSON after the merge (regression for #506, | |
| # where cmd's delayed expansion corrupted the embedded node script | |
| # and left settings.json in a broken state). | |
| $s = Get-Content $path -Raw | ConvertFrom-Json | |
| # The plannotator hook must have been added. | |
| $plannotatorEntries = $s.hooks.BeforeTool | Where-Object { | |
| $_.matcher -eq 'exit_plan_mode' | |
| } | |
| if (-not $plannotatorEntries) { | |
| throw "plannotator hook was not added to BeforeTool" | |
| } | |
| $planCmd = $plannotatorEntries[0].hooks | Where-Object { | |
| $_.command -eq 'plannotator' | |
| } | |
| if (-not $planCmd) { | |
| throw "plannotator command entry missing inside the new hook" | |
| } | |
| # The pre-existing (fixture) hook must have survived the merge. | |
| # The original buggy JS was `if(!s.hooks.BeforeTool)s.hooks.BeforeTool=[]` | |
| # which — after cmd ate the `!` — wiped existing arrays. The fix | |
| # (`s.hooks.BeforeTool = s.hooks.BeforeTool || []`) must preserve them. | |
| $fixtureHook = $s.hooks.BeforeTool | Where-Object { | |
| $_.matcher -eq 'ci-fixture-existing-matcher' | |
| } | |
| if (-not $fixtureHook) { | |
| throw "pre-existing hook was wiped — merge clobbered existing data" | |
| } | |
| # Unrelated top-level keys must survive the merge. | |
| if ($s.theme -ne 'ci-fixture-theme') { | |
| throw "unrelated top-level field 'theme' was mangled" | |
| } | |
| if ($s.general.ciFixtureSentinel -ne $true) { | |
| throw "unrelated top-level field 'general' was mangled" | |
| } | |
| Write-Host "✓ Gemini settings.json merge verified (issue #506 regression guard)" | |
| - name: Attestation pre-flight rejects v0.17.1 on real cmd.exe | |
| shell: pwsh | |
| run: | | |
| # Regression guard: the main feature of this PR (three-layer | |
| # verification opt-in + MIN_ATTESTED_VERSION pre-flight + | |
| # injection-safe $env:-based PowerShell version comparison) had | |
| # no runtime coverage on Windows because the previous | |
| # integration step passes --skip-attestation. | |
| # | |
| # We can't test the SUCCESS path (valid attested release) | |
| # because v0.17.1 is the current latest and it predates the | |
| # release.yml attestation step. Until the first post-merge | |
| # release exists, the only realistic end-to-end test is the | |
| # REJECTION path: invoke install.cmd with --verify-attestation | |
| # against v0.17.1 and assert the pre-flight rejects with | |
| # exit != 0 and stderr containing "predates". | |
| # | |
| # This exercises on a real cmd.exe: | |
| # - setlocal enabledelayedexpansion parser under the guard | |
| # - three-layer resolution reaching the CLI flag layer | |
| # - the :~1 substring (instead of :v= global substitution) | |
| # - the pre-release tag detection (negative — v0.17.1 is stable) | |
| # - the PowerShell shell-out with $env:TAG_NUM / $env:MIN_NUM | |
| # (injection-safe — the previous interpolation would have | |
| # allowed arbitrary PS execution via --version) | |
| # - the `[version] -ge` comparison returning false | |
| # - the "predates" error message block | |
| $installedBinary = "$env:USERPROFILE\.local\bin\plannotator.exe" | |
| # Capture the currently-installed binary's hash BEFORE running | |
| # the rejection test. The earlier Gemini-merge integration step | |
| # installed v0.17.1 at this path; we use the captured hash as | |
| # a baseline so we can prove the rejected invocation left it | |
| # untouched (no wasted download, no overwrite). | |
| if (-not (Test-Path $installedBinary)) { | |
| throw "Expected $installedBinary to exist from the earlier install.cmd step, but it's missing. Cannot baseline the preservation check." | |
| } | |
| $baselineHash = (Get-FileHash $installedBinary -Algorithm SHA256).Hash | |
| $baselineWriteTime = (Get-Item $installedBinary).LastWriteTime | |
| $stderrFile = New-TemporaryFile | |
| $p = Start-Process -Wait -PassThru -NoNewWindow cmd ` | |
| -ArgumentList '/c','scripts\install.cmd v0.17.1 --verify-attestation' ` | |
| -RedirectStandardError $stderrFile.FullName | |
| $stderr = Get-Content $stderrFile.FullName -Raw -ErrorAction SilentlyContinue | |
| Remove-Item $stderrFile.FullName -ErrorAction SilentlyContinue | |
| if ($p.ExitCode -eq 0) { | |
| throw "install.cmd v0.17.1 --verify-attestation should have been rejected by the MIN_ATTESTED_VERSION pre-flight, but exited 0. stderr: $stderr" | |
| } | |
| if ($stderr -notmatch 'predates') { | |
| throw "install.cmd rejected with exit $($p.ExitCode), but not via the pre-flight guard. Expected 'predates' in stderr, got: $stderr" | |
| } | |
| # Assert the pre-flight ran BEFORE any download / install step. | |
| # If the binary's hash or mtime changed, something downloaded | |
| # and moved a new file into place — meaning the pre-flight | |
| # rejection happened late (after the download step) instead | |
| # of early (before it). Catches future regressions that re- | |
| # introduce the post-download pre-flight pattern. | |
| $postHash = (Get-FileHash $installedBinary -Algorithm SHA256).Hash | |
| $postWriteTime = (Get-Item $installedBinary).LastWriteTime | |
| if ($postHash -ne $baselineHash) { | |
| throw "Binary at $installedBinary was overwritten during the rejected --verify-attestation run. Baseline SHA256 $baselineHash, post SHA256 $postHash. The pre-flight must run before any download." | |
| } | |
| if ($postWriteTime -ne $baselineWriteTime) { | |
| throw "Binary at $installedBinary had its LastWriteTime modified during the rejected --verify-attestation run. Baseline $baselineWriteTime, post $postWriteTime." | |
| } | |
| Write-Host "✓ MIN_ATTESTED_VERSION pre-flight rejected v0.17.1 via the expected code path" | |
| Write-Host "✓ Installed binary was preserved (SHA256 and LastWriteTime both unchanged)" | |
| - name: Verify Claude Code slash command files contain the shell-invocation prefix | |
| shell: pwsh | |
| run: | | |
| # Regression guard: install.cmd previously wrote `echo !`plannotator | |
| # review $ARGUMENTS`` (note the unescaped `!`) under | |
| # setlocal enabledelayedexpansion, which silently stripped the `!` | |
| # from the written .md file. Without the `!` prefix, Claude Code | |
| # renders the backtick block as inline markdown code and the slash | |
| # command is a silent no-op when invoked — the install appears | |
| # successful but the command does nothing. | |
| # | |
| # install.sh and install.ps1 already write the `!` correctly via | |
| # single-quoted heredocs / here-strings. This step checks that | |
| # install.cmd now matches (via `echo ^!`) and catches future | |
| # regressions of the same class. | |
| # Claude Code slash commands — `.md` files with `!`\`plannotator ...\`` invocation syntax | |
| $cmdDir = "$env:USERPROFILE\.claude\commands" | |
| foreach ($file in @("plannotator-review.md", "plannotator-annotate.md", "plannotator-last.md")) { | |
| $path = Join-Path $cmdDir $file | |
| if (-not (Test-Path $path)) { | |
| throw "Expected slash command file missing: $path" | |
| } | |
| $content = Get-Content $path -Raw | |
| if ($content -notmatch '!`plannotator') { | |
| throw "Slash command file $file is missing the '!' shell-invocation prefix. Content: $content" | |
| } | |
| } | |
| Write-Host "✓ All three Claude Code slash command files contain the '!' prefix" | |
| # Gemini slash commands — `.toml` files with `!{plannotator ...}` invocation syntax. | |
| # Same `^^!` cmd-escape class as the Claude Code files. The earlier integration | |
| # step seeded `~/.gemini/settings.json`, so install.cmd's Gemini block fired and | |
| # wrote these two files alongside the Claude Code ones. They use a different | |
| # invocation form (`!{...}` instead of `!\`...\``) but the regression risk is | |
| # identical — a future revision that drops a `^` from the echo would silently | |
| # produce broken Gemini commands. | |
| $geminiDir = "$env:USERPROFILE\.gemini\commands" | |
| foreach ($file in @("plannotator-review.toml", "plannotator-annotate.toml")) { | |
| $path = Join-Path $geminiDir $file | |
| if (-not (Test-Path $path)) { | |
| throw "Expected Gemini command file missing: $path" | |
| } | |
| $content = Get-Content $path -Raw | |
| if ($content -notmatch '!\{plannotator') { | |
| throw "Gemini command file $file is missing the '!' shell-invocation prefix. Content: $content" | |
| } | |
| } | |
| Write-Host "✓ Both Gemini slash command files contain the '!' prefix" | |
| - name: Unknown flag is rejected with non-zero exit | |
| shell: pwsh | |
| run: | | |
| # Regression guard for the review finding that install.cmd silently | |
| # reinterpreted typoed flags as version strings. A leading-dash token | |
| # that doesn't match a known flag must now produce a non-zero exit | |
| # AND emit "Unknown option:" on stderr — the latter is the real | |
| # discriminator between the guard triggering and some other failure | |
| # mode (network, gh auth, pre-PR release without an attestation) | |
| # that also happens to exit non-zero. | |
| # | |
| # `--verify-attesttion` below is INTENTIONALLY MISSPELLED. Do not | |
| # "correct" it during a typo sweep — the valid spelling is a real | |
| # flag and would bypass this guard. The stderr assertion below | |
| # would catch the drift, but the comment is the first line of | |
| # defense for future maintainers. | |
| $stderrFile = New-TemporaryFile | |
| $p = Start-Process -Wait -PassThru -NoNewWindow cmd ` | |
| -ArgumentList '/c','scripts\install.cmd --verify-attesttion' ` | |
| -RedirectStandardError $stderrFile.FullName | |
| $stderr = Get-Content $stderrFile.FullName -Raw -ErrorAction SilentlyContinue | |
| Remove-Item $stderrFile.FullName -ErrorAction SilentlyContinue | |
| if ($p.ExitCode -eq 0) { | |
| throw "install.cmd should have rejected --verify-attesttion but exited 0. stderr: $stderr" | |
| } | |
| if ($stderr -notmatch 'Unknown option:') { | |
| throw "install.cmd exited $($p.ExitCode) but not via the unknown-flag guard. Expected 'Unknown option:' in stderr but got: $stderr" | |
| } | |
| Write-Host "✓ Unknown flag rejected with exit code $($p.ExitCode) via the unknown-flag guard" |