mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
692 lines
35 KiB
YAML
692 lines
35 KiB
YAML
name: Test
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
push:
|
|
branches:
|
|
- main
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
with:
|
|
bun-version: 1.3.14
|
|
|
|
- name: Install dependencies
|
|
run: bun install --frozen-lockfile
|
|
|
|
- name: Check release version consistency
|
|
run: bun run check:release-version
|
|
|
|
- name: Generate Pi extension shared copies
|
|
run: bash apps/pi-extension/vendor.sh
|
|
|
|
- name: Type check
|
|
run: bun run typecheck
|
|
|
|
- name: Smoke-test published UI package
|
|
# This packs the real tarball and installs it with a pinned pnpm CLI in
|
|
# a temp consumer outside the monorepo. It prevents workspace protocols
|
|
# from leaking into the manifest published to external consumers.
|
|
run: bun run --cwd packages/ui smoke:package
|
|
|
|
# The extension has its own tsconfig (Node libs, no DOM) and is not part
|
|
# of the root typecheck, so nothing was checking it. A type error in the
|
|
# panel cookie seed shipped that way; this is the step that would have
|
|
# caught it.
|
|
- name: Type check VS Code extension
|
|
run: bun run --cwd apps/vscode-extension lint
|
|
|
|
- name: Run tests
|
|
run: bun test
|
|
|
|
# The file-browser watcher test is timing-sensitive (a 120ms SSE debounce
|
|
# asserted with real timers). Run it in its own process — matching the
|
|
# historical config — so it isn't destabilized by sharing a process with
|
|
# the rest of the ui suite (heavy load intermittently starves the debounce
|
|
# timer and the reconnect refetch is missed). See useFileBrowser.test.tsx.
|
|
- name: Run file-browser DOM test (isolated)
|
|
run: DOM_TESTS=1 bun test packages/ui/hooks/useFileBrowser.test.tsx
|
|
|
|
# These two render against the REAL @pierre/diffs, because the defects
|
|
# they cover (a stale render cache served across a cacheKey collision;
|
|
# an empty stub card) only exist inside its renderer. A sibling in the
|
|
# list below, AllFilesCodeView.lifecycle.test.tsx, calls
|
|
# `mock.module('@pierre/diffs', ...)` with a hunk-less getSingularPatch
|
|
# and `processFile: () => null` — and bun's module mocks are process
|
|
# global with no restore, so in one shared process the real renderer is
|
|
# not guaranteed to still be there by the time these load. That leak
|
|
# reproduced on the Linux runner and not on macOS. Their own process
|
|
# removes the coupling outright, same reasoning as the isolated step
|
|
# above.
|
|
- name: Run diff-renderer DOM tests (isolated, real @pierre/diffs)
|
|
run: >-
|
|
DOM_TESTS=1 bun test
|
|
packages/review-editor/components/DiffViewer.fullContentSwap.test.tsx
|
|
packages/review-editor/components/DiffViewer.oversizedStub.test.tsx
|
|
packages/review-editor/components/DiffViewer.binaryNotice.test.tsx
|
|
packages/review-editor/components/DiffViewer.headerControls.test.tsx
|
|
|
|
# Seam contracts + the remaining DOM-gated tests. Scoped to the DOM files
|
|
# (not the whole ui suite) to keep this process light. Files get isolated
|
|
# globals because theme, storage, and DOM test state must not leak between
|
|
# suites when Bun schedules them differently across platforms.
|
|
- name: Run UI seam-contract + DOM tests
|
|
run: >-
|
|
DOM_TESTS=1 bun test --isolate
|
|
packages/ui/markdownEditorFidelity.test.tsx
|
|
packages/ui/annotationDraftPersistence.test.tsx
|
|
packages/ui/codeAnnotationDraftPersistence.test.tsx
|
|
packages/ui/components/html-viewer/srcdoc.test.ts
|
|
packages/ui/components/html-viewer/htmlPinpointProtocol.test.tsx
|
|
packages/ui/components/html-viewer/htmlLiveProtocol.test.tsx
|
|
packages/ui/components/html-viewer/HtmlViewer.vimHud.test.tsx
|
|
packages/ui/components/html-viewer/HtmlViewer.bridgeAsset.test.tsx
|
|
packages/ui/components/Viewer.vimMode.integration.test.tsx
|
|
packages/ui/hooks/useVimSelection.test.tsx
|
|
packages/ui/utils/codeHighlight.test.ts
|
|
packages/ui/utils/htmlChrome.test.ts
|
|
packages/ui/utils/inputMethod.test.ts
|
|
packages/ui/utils/vimNavigation.test.ts
|
|
packages/ui/utils/clipboard.test.ts
|
|
apps/vscode-extension/src/vscode-theme.test.ts
|
|
packages/ui/utils/vimScroll.test.ts
|
|
packages/ui/components/InlineMarkdown.resolveLinkedDoc.test.tsx
|
|
packages/ui/components/MarkdownDiff.frozen.test.tsx
|
|
packages/ui/components/MarkdownEditor.extensions.test.tsx
|
|
packages/ui/components/ThemeProvider.favicon.test.tsx
|
|
packages/ui/components/CommentPopover.skillReferences.test.tsx
|
|
packages/ui/components/SkillReferenceMenu.placement.test.tsx
|
|
packages/ui/components/sidebar/FileBrowser.test.ts
|
|
packages/editor/editableDocumentsHook.test.tsx
|
|
packages/review-editor/components/ReviewSubmissionDialog.ui.test.tsx
|
|
packages/review-editor/components/AnnotationToolbar.placement.test.tsx
|
|
packages/review-editor/dock/panels/ReviewPROverviewPanel.ui.test.tsx
|
|
packages/review-editor/components/FileHeader.edit.test.tsx
|
|
packages/review-editor/edit/useEditSession.recovery.test.tsx
|
|
packages/review-editor/edit/discardRestoreRender.test.tsx
|
|
packages/review-editor/edit/selectionActionPopover.test.ts
|
|
packages/review-editor/components/AllFilesCodeView.lifecycle.test.tsx
|
|
packages/review-editor/components/AllFilesCodeView.generated.test.tsx
|
|
packages/review-editor/components/AllFilesCodeView.autoViewed.test.tsx
|
|
packages/review-editor/components/DiffViewer.compactTouchSelection.test.tsx
|
|
packages/guide-viewer/GuideSectionCard.test.tsx
|
|
packages/guide-viewer/GuideView.test.tsx
|
|
packages/guide-viewer/GuideViewportManager.test.tsx
|
|
packages/review-editor/components/guide/GuideExportButton.test.tsx
|
|
packages/review-editor/hooks/useReviewSearch.test.tsx
|
|
packages/review-editor/hooks/useTokenHover.test.tsx
|
|
packages/review-editor/components/TokenHoverCard.test.tsx
|
|
packages/review-editor/components/TokenHoverAnnouncementDialog.test.tsx
|
|
packages/review-editor/utils/stitchTokenIdentifier.test.ts
|
|
packages/review-editor/hooks/useAutoViewed.test.tsx
|
|
packages/review-editor/hooks/useCallFlowAnalysis.test.tsx
|
|
packages/review-editor/hooks/useCallFlowInstall.test.tsx
|
|
packages/review-editor/hooks/useCallFlowAutoInstall.test.tsx
|
|
packages/review-editor/dock/panels/ReviewCallFlowPanel.install.test.tsx
|
|
packages/review-editor/dock/panels/ReviewCallFlowPanel.raw.test.tsx
|
|
packages/review-editor/components/CallFlowFileBadge.interaction.test.tsx
|
|
packages/review-editor/components/CallFlowTreeView.interaction.test.tsx
|
|
packages/review-editor/components/PanelChrome.test.tsx
|
|
packages/review-editor/components/FileTree.search-placement.test.tsx
|
|
packages/review-editor/components/FileTreeNode.controls.test.tsx
|
|
packages/ui/components/Settings.analysis.test.tsx
|
|
packages/ui/components/AnnotationPanel.props.test.tsx
|
|
packages/ui/components/Viewer.consumer.test.tsx
|
|
packages/ui/components/Viewer.documentHeader.test.tsx
|
|
packages/ui/components/Viewer.codeBlockHighlightSwap.test.tsx
|
|
packages/ui/components/ImageAnnotator/ImageAnnotator.shortcuts.test.tsx
|
|
packages/ui/utils/undoHistory.dom.test.ts
|
|
packages/ui/utils/codeBlockMark.test.ts
|
|
packages/ui/components/InlineMarkdown.seam.test.tsx
|
|
packages/ui/components/ImageThumbnail.seam.test.tsx
|
|
packages/ui/hooks/useAnnotationHighlighter.test.tsx
|
|
packages/ui/hooks/annotationRestoreVerify.test.tsx
|
|
packages/ui/hooks/useAnnotationDraft.seam.test.tsx
|
|
packages/ui/hooks/useExternalAnnotations.seam.test.tsx
|
|
packages/ui/hooks/useAIChat.seam.test.tsx
|
|
packages/ui/hooks/useFileBrowser.seam.test.tsx
|
|
packages/ui/hooks/usePlanDiff.test.tsx
|
|
packages/ui/hooks/useLinkedDoc.test.tsx
|
|
packages/ui/components/DocBadges.test.tsx
|
|
packages/editor/planDiffAutoExit.test.tsx
|
|
packages/editor/App.archiveReadOnly.test.tsx
|
|
packages/editor/App.htmlChrome.test.tsx
|
|
packages/editor/App.decisionControl.test.tsx
|
|
packages/review-editor/App.decisionControl.test.tsx
|
|
packages/review-editor/components/ReviewSidebar.generalComment.test.tsx
|
|
packages/ui/components/HtmlSurfaceControls.test.tsx
|
|
packages/ui/components/AnnotationPanel.unanchored.test.tsx
|
|
packages/ui/hooks/useHtmlRefresh.test.tsx
|
|
packages/ui/hooks/useSharing.contentRevision.test.tsx
|
|
packages/ui/hooks/useSharing.shortUrlLifecycle.test.tsx
|
|
packages/ui/components/AnnotationToolbar.commentOnly.test.tsx
|
|
packages/ui/components/AnnotationToolstrip.test.tsx
|
|
packages/ui/components/StickyHeaderLane.test.tsx
|
|
packages/ui/components/CommentPopover.quickLookGood.test.tsx
|
|
packages/editor/actionsLabelMode.test.ts
|
|
packages/ui/webmcp/useToolset.seam.test.tsx
|
|
packages/ui/components/AnnotationPanel.inReplyTo.test.tsx
|
|
packages/editor/webmcp/useDocumentWebMcp.toggle.test.tsx
|
|
packages/editor/components/AppHeader.webmcpIndicator.test.tsx
|
|
packages/ui/components/MathBlock.firstPaint.test.tsx
|
|
packages/ui/components/DiagramBlock.lazyRetry.test.tsx
|
|
packages/ui/components/DecisionControl.test.tsx
|
|
packages/ui/components/ActionMenu.test.tsx
|
|
packages/ui/components/PlanHeaderMenu.mobile.test.tsx
|
|
packages/review-editor/components/ReviewHeaderMenu.mobile.test.tsx
|
|
|
|
opencode-v2:
|
|
name: OpenCode 2 installed package
|
|
runs-on: ubuntu-latest
|
|
# The smoke's own budgets (health 120s, plugin activation 300s) are the real assertion;
|
|
# this is only a backstop so a genuinely wedged runner cannot idle for hours.
|
|
timeout-minutes: 25
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
with:
|
|
bun-version: 1.3.14
|
|
|
|
- name: Install dependencies
|
|
run: bun install --frozen-lockfile
|
|
|
|
- name: Install OpenCode 2
|
|
# zizmor: ignore[adhoc-packages] This integration test requires the exact
|
|
# globally installed CLI. Owner: maintainers; reassess a locked fixture
|
|
# by 2027-08-11.
|
|
run: npm install --global @opencode-ai/cli@0.0.0-next-16775
|
|
|
|
- name: Build OpenCode plugin assets
|
|
run: bun run build:review && bun run build:hook && bun run build:opencode
|
|
|
|
# This is the only job with the single-file bundles on disk, so it is the
|
|
# only place the built-artifact assertions in entry-assets can actually
|
|
# run (they skip on an unbuilt checkout). Chief among them: no inlined
|
|
# WebAssembly survived the bundle.
|
|
- name: Assert built bundles ship no inlined WASM
|
|
run: bun test tests/entry-assets.test.ts
|
|
|
|
- name: Pack OpenCode plugin
|
|
working-directory: apps/opencode-plugin
|
|
run: npm pack --ignore-scripts --pack-destination "$RUNNER_TEMP"
|
|
|
|
- name: Run installed-package smoke
|
|
# Glob the tarball so this job survives version bumps; npm pack wrote
|
|
# exactly one plannotator-opencode-*.tgz into RUNNER_TEMP above.
|
|
run: >-
|
|
bun run --cwd apps/opencode-plugin smoke:v2 --
|
|
"$(command -v opencode2)"
|
|
"$RUNNER_TEMP"/plannotator-opencode-*.tgz
|
|
|
|
guides-show:
|
|
name: guides.show viewer (budgets + pinned manifest)
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
# The merge commit plus both parents, so the path gate below can diff
|
|
# the PR against its base without fetching the whole history.
|
|
fetch-depth: 2
|
|
|
|
# Path gate. The viewer build only reads apps/guides-show and the packages
|
|
# it bundles, so an unrelated PR skips the install and the build. Anything
|
|
# this cannot resolve (a push to main, a manual run, a checkout that is not
|
|
# the PR merge commit) runs the job: skipping is the risky direction.
|
|
- name: Does this change touch the viewer's inputs?
|
|
id: gate
|
|
run: |
|
|
set -eu
|
|
run=true
|
|
# HEAD is the PR merge commit, so HEAD^1 is the base tip and diffing
|
|
# the two is exactly the PR's changes. Requiring HEAD^2 confirms this
|
|
# really is the merge commit before trusting that.
|
|
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] \
|
|
&& base=$(git rev-parse --verify --quiet 'HEAD^1') \
|
|
&& git rev-parse --verify --quiet 'HEAD^2' >/dev/null; then
|
|
changed=$(git diff --name-only "$base" HEAD)
|
|
if grep -Eq '^(apps/guides-show/|packages/(guide-viewer|ui|review-editor|core)/)' <<< "$changed"; then
|
|
run=true
|
|
else
|
|
run=false
|
|
fi
|
|
fi
|
|
echo "run=$run" >> "$GITHUB_OUTPUT"
|
|
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
if: steps.gate.outputs.run == 'true'
|
|
with:
|
|
bun-version: 1.3.14
|
|
|
|
- name: Install dependencies
|
|
if: steps.gate.outputs.run == 'true'
|
|
run: bun install --frozen-lockfile
|
|
|
|
# The release workflow (guides-show-deploy.yml) refuses to publish when
|
|
# the viewer build blows its size budgets or the checked-in manifest no
|
|
# longer matches it, but that gate fires at the tag. The manifest pin is
|
|
# compiled into the release binaries independently of the viewer deploy,
|
|
# so a budget or manifest break has to surface on the PR that causes it.
|
|
# Regenerate with `bun run --cwd apps/guides-show build:viewer &&
|
|
# bun run --cwd apps/guides-show sync:manifest`.
|
|
- name: Build viewer
|
|
if: steps.gate.outputs.run == 'true'
|
|
run: bun run --cwd apps/guides-show build:viewer
|
|
|
|
- name: Size budgets
|
|
if: steps.gate.outputs.run == 'true'
|
|
run: bun run --cwd apps/guides-show check:budgets
|
|
|
|
# The viewer build is deterministic across platforms at the pinned Bun and
|
|
# lockfile (macOS and Linux produce identical hashes from a clean install),
|
|
# but a drifted local environment silently builds different bytes: a bun
|
|
# store with stale duplicate package versions reproduced exactly that. This
|
|
# Linux build is the authority the pin must match, so publish its manifest;
|
|
# when the check below fails and a fresh local regen still disagrees,
|
|
# download this artifact to apps/guides-show/dist/viewer/manifest.json, run
|
|
# `bun run --cwd apps/guides-show sync:manifest`, commit the result, and
|
|
# rebuild your environment (rm -rf node_modules && bun install --frozen-lockfile).
|
|
- name: Publish built viewer manifest for pin regeneration
|
|
if: always() && steps.gate.outputs.run == 'true'
|
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
|
with:
|
|
name: guide-viewer-manifest
|
|
path: apps/guides-show/dist/viewer/manifest.json
|
|
retention-days: 7
|
|
|
|
- name: Checked-in manifest matches this build
|
|
if: steps.gate.outputs.run == 'true'
|
|
run: bun run --cwd apps/guides-show check:manifest
|
|
|
|
uninstall-windows:
|
|
# Run the filesystem and path logic under real Windows path semantics.
|
|
# The tests inject temporary homes and process boundaries, so this job
|
|
# never touches the runner's actual agent installations or user data.
|
|
name: Uninstall lifecycle (Windows)
|
|
runs-on: windows-latest
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
with:
|
|
bun-version: 1.3.14
|
|
|
|
- name: Install dependencies
|
|
run: bun install --frozen-lockfile
|
|
|
|
- name: Run focused uninstall tests
|
|
run: bun test packages/server/uninstall.test.ts apps/hook/server/cli.test.ts
|
|
|
|
pi-extension-ai-runtime-windows:
|
|
# Exercises the Pi extension's Node/jiti server mirror on Windows with an
|
|
# npm-style `pi` shim pair. This catches regressions where `where pi`
|
|
# resolves the extensionless shim before pi.cmd and the Ask AI provider
|
|
# crashes before the plan review UI opens.
|
|
name: Pi extension AI runtime (Windows)
|
|
runs-on: windows-latest
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
with:
|
|
bun-version: 1.3.14
|
|
|
|
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
|
with:
|
|
node-version: 24
|
|
|
|
- name: Install dependencies
|
|
run: bun install --frozen-lockfile
|
|
|
|
- name: Generate Pi extension shared copies
|
|
shell: bash
|
|
run: bash apps/pi-extension/vendor.sh
|
|
|
|
- name: Run workspace symlink tests
|
|
run: bun test packages/server/review-workspace.test.ts --test-name-pattern discoverWorkspaceRepoPaths
|
|
|
|
- name: Build Pi AI runtime smoke
|
|
run: bun build scripts/smoke-pi-extension-ai-runtime.ts --target=node --outfile "$env:RUNNER_TEMP/pi-ai-runtime-smoke.mjs"
|
|
|
|
- name: Run Pi AI runtime smoke
|
|
run: node "$env:RUNNER_TEMP/pi-ai-runtime-smoke.mjs"
|
|
|
|
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- 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 # zizmor: ignore[misfeature]
|
|
# CMD is the subject of this regression test; replacing the shell would
|
|
# invalidate it. Owner: maintainers; reassess by 2027-08-11.
|
|
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: |
|
|
# Claude Code commands are deprecated in favor of skills: the
|
|
# installer no longer writes ~/.claude/commands files AT ALL (the
|
|
# core skills in ~/.claude/skills provide the slash commands). The
|
|
# old `echo ^!` escape regression class is structurally gone — no
|
|
# command content is generated by the installer anymore. Guard the
|
|
# NEW contract: a fresh install must not create plannotator command
|
|
# files.
|
|
$cmdDir = "$env:USERPROFILE\.claude\commands"
|
|
foreach ($file in @("plannotator-review.md", "plannotator-annotate.md", "plannotator-last.md")) {
|
|
$path = Join-Path $cmdDir $file
|
|
if (Test-Path $path) {
|
|
throw "Installer wrote a deprecated Claude command file: $path. Commands were replaced by skills; the installer must not generate them."
|
|
}
|
|
}
|
|
Write-Host "✓ No deprecated Claude Code command files were written (skills are the slash commands now)"
|
|
|
|
# Gemini slash commands — `.toml` files with `!{plannotator ...}` invocation
|
|
# syntax, now COPIED VERBATIM from the sparse checkout (apps/gemini/commands
|
|
# at the pinned tag) instead of being generated by echo escapes. The earlier
|
|
# integration step seeded `~/.gemini/settings.json`, so install.cmd's Gemini
|
|
# block fired. The `!{plannotator` content check verifies the verbatim copy
|
|
# actually delivered intact files (and would catch a regression back to
|
|
# generated content that mangles the `!`).
|
|
$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"
|
|
|
|
install-ps1-windows:
|
|
# End-to-end integration test for scripts/install.ps1 on real PowerShell.
|
|
# This specifically covers PLANNOTATOR_DATA_DIR tilde expansion because
|
|
# installer config lookup happens before any shared runtime code is loaded.
|
|
name: install.ps1 (Windows integration)
|
|
runs-on: windows-latest
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Run install.ps1 end-to-end
|
|
shell: pwsh
|
|
run: .\scripts\install.ps1 v0.17.1 -SkipAttestation
|
|
|
|
- name: Config opt-in resolves PLANNOTATOR_DATA_DIR tilde paths
|
|
shell: pwsh
|
|
run: |
|
|
$configDir = Join-Path $env:USERPROFILE "plannotator-ci-config"
|
|
New-Item -ItemType Directory -Force -Path $configDir | Out-Null
|
|
Set-Content -Path (Join-Path $configDir "config.json") -Value '{ "verifyAttestation": true }' -NoNewline
|
|
|
|
$installedBinary = "$env:LOCALAPPDATA\plannotator\plannotator.exe"
|
|
if (-not (Test-Path $installedBinary)) {
|
|
throw "Expected $installedBinary to exist from the earlier install.ps1 step, but it's missing."
|
|
}
|
|
$baselineHash = (Get-FileHash $installedBinary -Algorithm SHA256).Hash
|
|
$baselineWriteTime = (Get-Item $installedBinary).LastWriteTime
|
|
|
|
$env:PLANNOTATOR_DATA_DIR = '~\plannotator-ci-config'
|
|
$stderrFile = New-TemporaryFile
|
|
$p = Start-Process -Wait -PassThru -NoNewWindow pwsh `
|
|
-ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','.\scripts\install.ps1','v0.17.1' `
|
|
-RedirectStandardError $stderrFile.FullName
|
|
$stderr = Get-Content $stderrFile.FullName -Raw -ErrorAction SilentlyContinue
|
|
Remove-Item $stderrFile.FullName -ErrorAction SilentlyContinue
|
|
|
|
if ($p.ExitCode -eq 0) {
|
|
throw "install.ps1 should have found config.json via PLANNOTATOR_DATA_DIR=~\plannotator-ci-config and rejected v0.17.1, but exited 0."
|
|
}
|
|
if ($stderr -notmatch 'predates') {
|
|
throw "install.ps1 exited $($p.ExitCode), but not via the expected attestation pre-flight. Expected 'predates' in stderr, got: $stderr"
|
|
}
|
|
|
|
$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 config-driven verifyAttestation run."
|
|
}
|
|
if ($postWriteTime -ne $baselineWriteTime) {
|
|
throw "Binary at $installedBinary had its LastWriteTime modified during the rejected config-driven verifyAttestation run."
|
|
}
|
|
|
|
Write-Host "✓ PLANNOTATOR_DATA_DIR tilde expansion found config.json"
|
|
Write-Host "✓ Config-driven verifyAttestation rejected before download/install"
|