Release smoke¶
Hosted CI is necessary but not sufficient for a Spectre release. Several high-value surfaces are shipped on all three desktop OSes while full interactive e2e is Linux/macOS-only (or unit-only) on GitHub-hosted runners. A scoped pre-tag smoke on real desktops closes that gap before anyone tags.
This page is part of the release process. Do not tag a release without a written smoke plan for that version and a completed results table.
Related: Publishing, Capability matrix, Stability policy, Testing policy.
When it runs¶
| Phase | What |
|---|---|
| Before tag | Author the scoped plan (below), run hard cells, paste results into the release PR / issue / chat. Hard red → fix or drop the claim before tagging. |
| Tag / CI | release.yml (runtime matrix, helpers, notarization, verifyMavenLocalPublication, Central upload, draft GH release). |
| After promote | Central checker + undraft + package channels (see Publishing). Optional quick re-smoke of CLI from the public release archive. |
Smoke does not replace green main CI or a green release workflow. It
covers paths CI cannot or does not execute fail-closed.
How to scope a release (every time)¶
Produce a short plan for this version before running anything. Keep it in
the release notes draft, a PR description, or .plans/<version>-smoke.md
(gitignored). Minimum contents:
- Version and base tag (e.g.
v0.3.0→v0.4.0). - Delta inventory — features and packaging changes since the last tag: Map each user-visible theme to at least one smoke cell (or mark “CI-only / unit-covered”).
- Baseline regression set (always include — see Baseline hard cells).
- Delta hard cells — new or changed paths that lack fail-closed multi-OS
evidence (especially anything gated
@EnabledOnOs(LINUX, MAC)while docs claim Windows). - Soft cells — optional; failure does not block the tag if the capability matrix already says Experimental / Not yet CI-executed.
- Results table — OS × cell, pass/fail/N/A + one-line note.
Signals that something must be a hard cell¶
Add a hard physical/desktop cell when any of these hold:
- Hosted tests use
@EnabledOnOs(OS.LINUX, OS.MAC)(or skip Windows) for a surface docs claim on Windows. CapabilityMatrixmarks the cell Not yet CI-executed or Experimental while release notes would still market it as “works.”- New native helper packaging, notarization shape, or CLI distribution layout.
- New default-on test behaviour (e.g. failure artifacts) that consumers hit without opt-in.
- New attach/bootstrap path (inject, launch-and-attach, protocol version bump).
- Only unit/mocked tests exist for a CLI/daemon feature that users run live.
Use testing/.../contract/CapabilityMatrix.kt and
docs/guide/capability-matrix.md as the machine-checkable inventory of claimed
vs unproven cells.
Permanent CI gaps (always consider for hard smoke)¶
These are structural, not one-release accidents:
| Surface | Hosted CI | Physical smoke |
|---|---|---|
| CLI package-channel (Homebrew/Scoop) contracts | Structural on every Unix check (python3); install-semantics when Ruby present or under CI (issue #400) |
Optional: install Ruby on a clean Linux box and run ./gradlew verifyHomebrewFormulaInstallSemantics if claiming formula behaviour beyond CI |
| Agent attach + contract corpus (live UI) | Linux Xvfb + macOS desktop; Windows transport/ACL unit tests | Windows headed desktop (not SSH-only for capture) |
| Input coordinator contention/recovery | Deterministic + forked process on every OS | Automated pre-tag as the input-coord-* cells (both entrypoints, fail-closed JUnit XML). Those all pass headless, so headed real-input contention is a separate hard cell, input-coord-headed-robot — automated too (Linux Xvfb, macOS and Windows desktops), with an operator signature only where no host can run it |
| Agent Windows UI e2e | Opt-in only: -Pspectre.agent.attachE2e.allowWindows=true |
Run that property on Mattone-class boxes |
Agent real-keyboard typeText and pressKey |
Runs on CI (CI=true); skipped on developer machines |
Add -Pspectre.agent.realKeyboard=true on an idle desktop |
| Agent inject attach | Linux + macOS e2e | Windows inject fixture (no preinstalled core) |
| Launch-and-attach e2e | Linux + macOS | Windows direct java (and Gradle if claimed) |
| CLI daemon + live fixture | Linux + macOS | Windows release-shaped CLI binary |
| Daemon-owned window recording | macOS e2e when TCC allows; heavy unit mocks elsewhere | macOS live record; Windows WGC only from an interactive console (SSH sessions fail WGC / black pixels) |
macOS SpectreCaptureHelper.app + TCC row |
Helper build on PR; notary on tag | Extract from jar; permissions check; one SCK capture; notarize remains CI |
| Windows multi-file WGC helper | Packaging contract in Gradle + portal checker | Extract from jar; live capture from interactive console |
| Stock IntelliJ inject (no spectre-core in IDE) | Recipe + instrumented ide-uitest only | Manual recipe when that path is in release notes |
Popup compose.layers.type=WINDOW on Windows |
Skipped (upstream skiko) | Do not claim; optional recheck after runtime bumps |
Windows session note: A box can be “interactive” for console login yet still fail Screen
Capture / WGC under SSH (e.g. 0x80070424, black Skiko pixels). Treat attach (semantics)
as SSH-safe when proven; treat pixel capture / WGC as requiring a local interactive
desktop session (RDP console or physical).
Experimental input coordination release gate¶
Shipping the experimental coordinator is a release claim even though its API may change. Treat the following as delta hard cells on a headed macOS, Windows, and Linux Xorg/Xvfb desktop:
- two independent JVMs acquire real-input work in FIFO order without interleaving;
- a queued waiter is cancelled without stranding the next waiter;
- holder session loss stays fenced until cleanup acknowledgement or exact-ID forced recovery, without granting stale ownership;
- exact-ID normal revoke cannot affect a newer lease;
- explicit forced recovery reports
unsafeTakeover=trueand allows the queue to progress; and - parallel JUnit invocations using
InputIsolationConfig.perTest()serialise factory, body, evidence, and teardown.
The Experimental label does not turn these into soft cells. If an OS cannot be exercised, narrow
the release notes/platform claim rather than calling the coordinator cross-platform. Making
Auto the no-argument driver default is a separate stability decision and requires the full matrix
again.
These six bullets are automated as the input-coord-* scenario IDs on both entrypoints
(scripts/release-smoke.py and scripts/windows-release-smoke.ps1), each fail-closed on the
coordinator's own JUnit XML:
| Gate bullet | Scenario ID | Proof driven |
|---|---|---|
| Two independent JVMs, FIFO, no interleave | input-coord-contention |
TwoClientJvmContentionTest — two forked client JVMs whose held intervals must be disjoint — + LocalCoordinatorServerTest FIFO across two client sessions + CoordinatorProcessLauncherTest real forked-coordinator JVM |
| Cancelled waiter, next not stranded | input-coord-cancellation |
LocalCoordinatorServerTest cancelled-acquire-preserves-FIFO |
| Holder loss fenced until ack / exact force | input-coord-quarantine |
LocalCoordinatorServerTest crashed-holder quarantine → exact-ID unsafe recovery |
| Exact-ID revoke cannot hit a newer lease | input-coord-revoke |
LocalCoordinatorServerTest stale-observation revoke fences the actual holder |
Forced recovery reports unsafeTakeover=true |
input-coord-forced-recovery |
LocalCoordinatorServerTest explicit force advances FIFO + unsafe takeover |
Parallel JUnit perTest() serialises lifecycle |
input-coord-junit-pertest |
InputIsolationLifecycleTest (JUnit 4 + 5 whole-test lease across factory/body/evidence/teardown) + ParallelPerTestInputIsolationTest (concurrent invocations against a real coordinator must not overlap) |
These proofs are deterministic and display-independent, so the cells are hard on all three OSes (macOS, Windows including SSH, and Linux Xorg/Xvfb) and are not Robot cells — they run with the plain scenario env, never the Xvfb/Robot wrapper.
Two of the bullets need proof that a single sequential JVM cannot give, so they are backed by dedicated tests rather than by the coordinator's in-process suite alone:
TwoClientJvmContentionTestforks two independent client JVMs against one coordinator and asserts their held wall-clock intervals are disjoint. Give the two processes separate desktop keys and it fails, so the assertion is load-bearing rather than vacuous.ParallelPerTestInputIsolationTestruns concurrentperTest()extension lifecycles against a real coordinator on a hermetic endpoint and asserts no two leases overlap. Remove the mutual exclusion and all invocations overlap and it fails.
Non-overlap in both is an invariant, not a timing race: the lease is mutually exclusive, so a waiter cannot be granted until the holder releases. A failure means genuine interleaving, never a slow machine.
The headed half of the first bullet is a seventh hard cell, input-coord-headed-robot, and
not a residual. Every cell in the table above passes headless and under SSH, because none of them
constructs a RobotDriver: together they prove the lease is mutually exclusive, never that two
processes' real OS input does not interleave. Do not treat green input-coord-* cells as
satisfying it.
Since #491 that cell is automated too. Both
entrypoints run :sample-desktop:headedRobotContentionTest, which forks two JVMs, each with its
own RobotDriver(InputLeasePolicy.Required), releases them from one barrier, and has both type a
distinguishable block into the same focused text field of one fixture window. An ababab field is
the failure being ruled out. The e2e is genuinely contended by construction, and says so: each
probe pays for coordinator launch and window focus before the barrier, and the test then measures
that the second probe asked for the keyboard while the first was still typing — otherwise it fails
as a broken barrier rather than reporting a vacuous pass.
| Platform | How the cell is satisfied |
|---|---|
| Linux Xorg/Xvfb | Automated. release-smoke.py runs the task under the same xvfb-run wrapper and forced-X11 env as junit-live — Robot cells must leave the compositor seat (#432). Hard fail if xvfb-run is absent, unless an operator signs. |
| macOS desktop | Automated, if java.awt.Robot has Accessibility (TCC) permission. Without it the probes' input is dropped and the cell fails; grant it, or sign for a run on a Mac that has it. |
| Windows interactive / RDP | Automated. |
| Windows SSH | Not automated — an SSH session has no interactive desktop for Robot to type into, so the cell needs an operator signature there. |
The operator flag survives as that escape hatch: --headed-robot-evidence "<note>" on Unix,
-HeadedRobotEvidence "<note>" on Windows. It cannot rescue a red automated run — an observed
interleave outranks an unverifiable note — and it does not substitute for a deleted proof source.
Absent both a run and a signature, the cell is a hard fail.
To add or change one of these cells, follow
Adding a scenario ID; do not leave
one-off ./gradlew commands only in chat.
Environments¶
| Host | Role |
|---|---|
| macOS (dev machine) | Primary: library, agent, CLI, SCK helper, permissions |
Windows (logged-in desktop; optional ssh for non-WGC) |
Prefer one-liner Windows smoke script on the console user session |
| Linux (Hyper-V VM from Windows host, or native) | Agent attach under Xvfb or real display; package smoke; Wayland only if session is real |
Headless windows-latest is not a substitute for B9–B13.
Prefer release-shaped artifacts:
./gradlew check
./gradlew publishToMavenLocal -PVERSION_NAME=<version>-rc.smoke
# helpers: real platform builds where possible; stub only what the host cannot build
./gradlew verifyMavenLocalPublication # flags per docs/PUBLISHING.md
CLI: package the host OS binary (:cli:package*) or use the draft release archive
after tag. Prefer the same helper layout consumers extract from Maven jars.
Baseline hard cells¶
Run these for every release unless a cell is explicitly N/A with reason (e.g. no Linux display for this cut and no Linux claim change).
| ID | OS | Cell | Pass criteria |
|---|---|---|---|
| B1 | macOS | ./gradlew check |
Green on release SHA |
| B2 | macOS | Maven Local core + testing consume | Compile/run a one-liner or sample against -PVERSION_NAME=… |
| B3 | macOS | Failure artifacts | Failing Spectre JUnit test writes build/reports/spectre/** (capture.json + PNG) |
| B4 | macOS | Atomic capture | In-process or CLI; schemaVersion matches shipped skill/docs |
| B5 | macOS | Agent attach (preinstalled core) | Non-empty windows + known tag; clean detach |
| B6 | macOS | Agent inject | Compose-only target; inject bootstrap; non-empty tree; detach |
| B7 | macOS | CLI daemon fixture | spectre attach (or package binary) → tree/capture against live fixture |
| B8 | macOS | Capture helper + TCC | Helper is app-bundle identity; spectre permissions check; one screenshot/record |
| B16 | macOS | CLI app seal (packaged) | Release-shaped or Homebrew Spectre.app: codesign --verify --deep --strict exit 0; xcrun stapler validate when notarized; launcher --help prints Usage (not silent exit 0 / Gatekeeper “damaged”). Prefer brew install of the formula after undraft when claiming Homebrew. See #390. |
| B9 | Windows | Agent attach (core) | Same as B5; prefer opt-in e2e (recipe below). SSH OK if semantics-only. |
| B10 | Windows | Agent inject | Same as B6 — does not ship “three OS agent” without this |
| B11 | Windows | Launch-and-attach | Direct java launch + attach (Gradle optional) |
| B12 | Windows | CLI package + attach | Packaged spectre (or packageWindowsX64) attaches to fixture |
| B13 | Windows | WGC helper | Multi-file extract (SSH OK) and live capture from interactive console (not SSH) |
| B14 | Linux | Agent attach | Xvfb or real display; non-empty tree |
| B15 | any | Publication shape | verifyMavenLocalPublication (and portal checker once deployment exists) |
Soft (every release if time; never block solely on focus flakes):
| ID | Cell |
|---|---|
| S1 | Agent typeText / pressKey (known OS-focus flakes; matrix Experimental where noted) |
| S2 | Hot Reload settle e2e (needs HR-enabled target) |
| S3 | Stock IntelliJ inject recipe (manual) |
| S4 | JUnit 4 failure-artifact path (JUnit 5 is the hard path) |
| S5 | Full kill-target mid-record finalize |
Results table (required)¶
Copy into the release record:
Version: vX.Y.Z Base: vA.B.C SHA: <full>
Operator: <name> Date: <ISO>
| ID | OS | Cell | Result | Note |
|-----|---------|------------------------------|--------|------|
| B1 | macOS | check | | |
| B5 | macOS | attach (core) | | |
| B6 | macOS | inject | | |
| B9 | Windows | attach (core) | | |
| B10 | Windows | inject | | |
| … | | | | |
| D1 | … | <delta cell> | | |
Hard failures: none | <list>
Soft notes: …
Result values: pass | fail | n/a (with reason).
Empty hard cells or n/a for “no display” on a claimed platform block the tag.
Baseline automation¶
Run the committed baseline runner from the repository root on macOS and Linux:
Optional flags:
| Flag | Effect |
|---|---|
--base v0.4.1 |
Record the previous release tag (default: latest git tag) |
--skip-check |
Skip ./gradlew check (records hard n/a with reason) |
--out-dir PATH |
Report/log directory (default build/smoke) |
--overall-timeout SECS |
Wall-clock budget for the whole run (default 7200) |
--skip-maven-local |
Skip Maven Local publish + consumer (hard n/a with reason) |
--skip-recording |
Skip host native recording smoke (hard n/a with reason) |
--preflight-only |
Run only preflight; remaining required IDs are hard n/a with reason preflight-only mode; scenario not executed. Validates report schema + matrix wiring without a multi-hour GO. Not a release smoke GO. |
On Linux it supplies xvfb-run -a when DISPLAY is unset and records environment.displayMode
as xvfb-auto or real-display:$DISPLAY. On Windows use the interactive PowerShell runner in the
next section (shared stable scenario IDs and schemaVersion report shape).
Xvfb ≠ Wayland: auto-Xvfb proves the X11 path only. Real portal consent/cancel requires a real Wayland session (manual cell below).
Report artifacts¶
Every run writes under build/smoke/ (or --out-dir):
| Path | Contents |
|---|---|
release-smoke.json |
Versioned machine-readable report (schemaVersion, full SHA, dirty flag, env, scenario rows) |
release-smoke.md |
Markdown results table for the release record |
<scenario-id>-<timestamp>.log |
Per-step stdout/stderr |
Report fields of note: schemaVersion (currently 1), version, base, sha (full), dirty,
environment.displayMode, and scenarios[] with stable id, result (pass | fail | n/a),
optional reason (required for hard n/a), timings, and log path.
Hard skips are fail-closed: a hard scenario result of n/a without a non-empty reason is
treated as fail. Soft cells may use hard: false. Missing a required scenario ID is also fail
(Unix: validate_report(..., required_ids=REQUIRED_SCENARIO_IDS); Windows: RequiredScenarioIds
completeness check before exit).
schemaVersion bump policy¶
schemaVersion is defined once in scripts/smoke_lib.py as SCHEMA_VERSION. The Windows
entrypoint (scripts/windows-release-smoke.ps1) reads that constant at report time via
Get-SmokeSchemaVersion — do not hardcode a parallel integer in the PowerShell script.
Bump only when report field names or semantics change incompatibly (rename/remove a field,
change meaning of an existing value). Additive optional fields and new scenario IDs do not
require a bump — keep existing field names stable so older report consumers still parse. When you
bump:
- Update
SCHEMA_VERSIONinsmoke_lib.pyonly (Windows picks it up automatically). - Extend
validate_report/ contract tests for the new shape. - Note the bump in the release record so operators do not compare v1 and v2 rows as identical.
Adding a scenario ID (per-release delta or permanent baseline)¶
Do not leave a one-release command only in chat. To add a reusable cell:
- Append a stable kebab-case ID to
REQUIRED_SCENARIO_IDSinscripts/smoke_lib.py(or document it as a soft / delta-only cell if it is not required on every cut — soft cells usehard: falseand need not be inREQUIRED_SCENARIO_IDS). - Register the same ID on both entrypoints:
- Unix:
scripts/release-smoke.py(run_scenario/run_callable_scenario/ explicit hardn/awith reason). - Windows:
scripts/windows-release-smoke.ps1(RequiredScenarioIdsarray + step that emits the row; environment-impossible → hardn/awith reason, never silent omit). - Extend contract tests:
.github/scripts/test-release-smoke-scripts.py(test_required_scenario_ids_are_stable, wiring asserts, any new fail-closed rule)..github/scripts/test-windows-release-smoke-script.sh(ID presence + any new policy needles).- Document the cell in the stable scenario table above and in the automated-vs-manual matrix.
- Prefer forcing live UI with
gradle_ui_force_args()/--rerun-tasks --no-build-cachewhen the cell is UI-backed so cache-only cannot fake PASS.
Per-release delta cells that will not stay permanent may live in
.plans/<version>-smoke.md as manual recipes; promote them into the runner when they repeat.
Stable scenario IDs¶
Shared across macOS / Linux / Windows entrypoints (scripts/smoke_lib.py → REQUIRED_SCENARIO_IDS):
| ID | Cell |
|---|---|
preflight |
Environment / SHA / clean-tree preflight |
macos-tcc |
macOS Screen Recording + Accessibility TCC preflight. Screen Recording uses MacOsScreenCaptureAccess.preflight on the runtime ~/Library/Application Support/spectre/helpers/spectre-screencapture/SpectreCaptureHelper.app, plus the MacOsTccGuard wrapping-app Robot probe (screencapture 32×32 origin, all-black = denied) and ioreg IOConsoleLocked (locked console is fail-closed even if the helper reports granted). Accessibility uses the MacOsTccGuard osascript (wrapping app). Fail-closed on Denied / Locked / Unknown, on helper exit/JSON mismatch, and on a missing, relative, or invalid SPECTRE_SCREENCAPTURE_HELPER. Hard n/a on Linux/Windows. Runs before ./gradlew check. |
check |
./gradlew check |
junit-live |
Live JUnit failure artifacts/video and atomic capture |
agent-attach-core |
Agent attach with preinstalled core |
agent-contract-corpus |
Agent contract corpus |
agent-inject |
Injected attach without preinstalled core |
agent-launch-and-attach |
Launch-and-attach |
cli-packaged |
Release-shaped host CLI packaging |
cli-native-helper-layout |
Native-helper layout in package |
cli-user-flow |
Packaged CLI user flow (ps/attach/tree/input/screenshots/detach) |
mcp-sdk-flow |
Packaged MCP attach/op/detach lifecycle + strict stdio |
host-native-recording |
Host native recording smoke |
maven-local-consumer |
Maven Local publication + fresh consumer |
portal-token-warmup |
Linux Wayland: one interactive RemoteDesktop + monitor grant at run start, then reuse the stored restore token. Hard n/a on macOS/Windows/Xvfb. |
pointer-move |
In-process moveTo / moveBy hover without click (:sample-desktop:validationTest --tests '*PointerMoveLive*'). Hard n/a only if the verbs disappear from ComposeAutomator. On Windows, a post-green MessageIOException / Could not write '/127.0.0.1:…' after exitApplication teardown (#500 / #72) is a pass when the PointerMoveLive JUnit XML is green. |
input-coord-contention |
Two independent JVMs take one desktop lease in FIFO order (:input-coordinator-server:test FIFO client contention + forked-coordinator process). Hard on macOS/Windows/Linux Xorg/Xvfb — no display needed, so hard on Windows SSH too. |
input-coord-cancellation |
A cancelled queued waiter is removed without stranding the next waiter. |
input-coord-quarantine |
A crashed holder stays fenced/quarantined until exact-ID forced recovery, without granting stale ownership. |
input-coord-revoke |
Exact-ID normal revoke fences only its own lease and cannot affect a newer one. |
input-coord-forced-recovery |
Explicit forced recovery reports unsafeTakeover=true and advances the FIFO queue. |
input-coord-junit-pertest |
InputIsolationConfig.perTest() serialises factory, body, evidence, and teardown (:testing:test InputIsolationLifecycleTest). |
input-coord-headed-robot |
Two RobotDriver(InputLeasePolicy.Required) JVMs dispatching real OS input on a headed desktop must not interleave. Driven by :sample-desktop:headedRobotContentionTest (fail-closed on its JUnit XML) wherever the host can run it: Linux under xvfb-run, a macOS desktop with Accessibility granted, Windows interactive/RDP. Where it cannot — Windows SSH, a Linux box without xvfb-run — the operator passes --headed-robot-evidence / -HeadedRobotEvidence. With neither it is a hard fail and the smoke exits non-zero; a reasoned n/a would be ignored by the fail-closed check and report success, so absence is deliberately a failure. Required on every OS whose release notes claim headed coordination. |
The input-coord-* cells (#459) are the automated form of the
Experimental input coordination release gate. Each drives the
coordinator's own deterministic + forked-process + JUnit-isolation tests with --rerun-tasks --no-build-cache, and
fails closed on the JUnit XML (the exact testcase must have executed, not assumption-skipped) so a cache-only or
empty-filter run cannot fake pass. They need no display, so they stay hard on all three OSes. A missing proof source is a hard
fail, not a skip: deleting or renaming one of these tests must not turn six hard cells green by
omission. Dropping the feature legitimately means re-scoping affirmatively — remove the
input-coord-* IDs from REQUIRED_SCENARIO_IDS and update this page. Fully-headed
two-RobotDriver real-input contention is a separate hard cell, input-coord-headed-robot, which
runs its own Robot e2e where the host allows and is operator-recorded where it does not (below).
The Unix runner registers every required ID end-to-end (fail-closed). Environment-impossible
cells must be explicit hard n/a with reason — never a silent omit or fake pass.
The cross-platform runner covers the stable baseline that should not be reinvented per release:
- environment/SHA/dirty-tree preflight recorded in the report
- macOS
macos-tccfail-closed Screen Recording / Accessibility probe before./gradlew check(issue #502); Linux/Windows hardn/a - the full
checkgate - live JUnit validation (failure artifacts/video and capture/wait validation), forced with
--rerun-tasks --no-build-cacheso cache-only passes cannot skip UI work - agent attach with preinstalled core, contract corpus, inject, and launch-and-attach
- release-shaped packaged CLI construction + host native-helper layout checks
- packaged CLI fixture user flow (ps/attach/find/input/fail-closed window screenshot/fullscreen/detach)
- packaged MCP via official SDK e2e (attach → op → detach → session-gone required for hard
pass) + strict stdio (version / tools/list including
detach/ unknown-session detachisError) - host native recording smoke (macOS SCK region / Linux X11; Windows WGC via interactive PS script)
- Maven Local
verifyMavenLocalPublication+ fresh consumer jar resolve - Linux Wayland
portal-token-warmup: one:recording:runWaylandPortalSmokewith a pinnedSPECTRE_WAYLAND_HELPER+SPECTRE_WAYLAND_RESTORE_TOKEN_DIRunderbuild/smoke/wayland-restore-tokens/. Approve Share + Remember / Allow remote interaction for the whole screen; the warmup writeswayland-rd-restore-token-rd-monitor-embedded. Later helper monitor cells (host-native-recording) reuse that grant via the long-lived session. Robot-backed cells stay underxvfb-run. Window-source prompts are per-window and may still appear. Xvfb / macOS / Windows record hardn/a. pointer-move(#433 / #435): live hover viamoveTo(node)/moveBy/moveTo(x, y)against the samplescenario.hoverfixture, with JUnit XML fail-closed so assumption-skips cannot fake PASS. IfComposeAutomatorever loses both verbs the cell is hardn/awith reason. This is the in-process gap; CLI / MCP / agent verbs are a follow-up. Windows:SampleAppFixturekeepsexitProcessOnExit = falseso the validation worker can close the Gradle test-event socket afterexitApplication. If a residual#72/#500MessageIOExceptionstill kills the worker, the smoke cell still passes when PointerMoveLive XML is green.
Each release still needs delta cells based on git log <previous-tag>..HEAD. Add reusable delta
coverage to the runner rather than leaving a one-release command only in chat.
Automated vs manual (0.5.0 harness)¶
| Surface | Automated entrypoint | Manual recipe only |
|---|---|---|
| Preflight / check / agent attach·inject·launch / CLI package / MCP SDK / Maven Local | release-smoke.py (macOS/Linux); Windows PS shares IDs |
— |
| Live JUnit failure artifacts/video + atomic capture | release-smoke.py → junit-live |
Windows: run on macOS/Linux baseline (hard n/a on Windows entrypoint with reason) |
| Pointer-move hover (#433 / #435) | Unix + Windows pointer-move → *PointerMoveLive* |
Fail-closed on all three OSes; hard n/a only if the verbs disappear |
| Input coordination gate (#459: contention / cancellation / quarantine / revoke / forced recovery / JUnit PerTest) | Unix + Windows input-coord-* (coordinator protocol + forked process + JUnit isolation, fail-closed XML) |
— |
Headed two-RobotDriver real-input contention (#491) |
Unix + Windows input-coord-headed-robot → :sample-desktop:headedRobotContentionTest (Linux Xvfb, macOS desktop with TCC, Windows interactive/RDP) |
Hard on hosts that cannot run it (Windows SSH; no xvfb-run): record via --headed-robot-evidence / -HeadedRobotEvidence |
| Host native recording | macOS SCK + Linux X11 in release-smoke.py; WGC in Windows PS when interactive |
SSH WGC is N/A (not PASS) |
| macOS Screen Recording / Accessibility TCC | Unix macos-tcc fail-closed preflight (no SecurityAgent prompt; no TCC.db reads) |
Unlock the console if IOConsoleLocked; grant Accessibility and Screen Recording to the wrapping app (Robot) and Screen Recording to Spectre Capture Helper; quit/relaunch + ./gradlew --stop, then rerun |
| Notarization / app seal | — | macOS recipes below |
| Real Wayland portal | — | Real Wayland session (Xvfb ≠ Wayland) |
| Public Homebrew / Scoop / archive installs | — | After draft release undraft |
| Focus / lock keys / multi-monitor / HiDPI | Soft / env-dependent | Operator notes |
| Stock IntelliJ inject | Soft recipe | Manual when claimed in notes |
On-demand pre-tag macOS notarization¶
The tag workflow notarizes the macOS helper and CLI bundles, but B8/B16 need release-signed
artifacts before the tag. Run the artifact-only workflow against the intended release SHA after
that SHA has landed on main:
sha="$(git rev-parse HEAD)"
run_url="$(gh workflow run notarize-macos.yml \
--ref main \
-f ref="$sha" \
-f version=0.5.0-rc.smoke)"
run_id="${run_url##*/}"
echo "$run_url"
The workflow rejects commits that are not reachable from the repository's default branch. It calls
the same reusable signing jobs as release.yml, but it never publishes to Central or creates a
GitHub release. After the run finishes, download its two artifacts:
gh run watch "$run_id"
gh run download "$run_id" -n mac-helper -D build/notarized-smoke/mac-helper-artifact
gh run download "$run_id" -n mac-cli-bundles -D build/notarized-smoke/mac-cli-bundles
mkdir -p build/notarized-smoke/mac-helper
tar -xzf build/notarized-smoke/mac-helper-artifact/SpectreCaptureHelper.app.tar.gz \
-C build/notarized-smoke/mac-helper
test -x \
build/notarized-smoke/mac-helper/SpectreCaptureHelper.app/Contents/MacOS/spectre-screencapture
The helper is intentionally archived before artifact upload because GitHub normalizes raw artifact
file modes. Use build/notarized-smoke/mac-helper/SpectreCaptureHelper.app for the B8 TCC/live-SCK
check. For B16, run the committed verifier on the host-architecture archive (replace macosArm64 with
macosX64 on Intel):
bash .github/scripts/verify-macos-cli-bundle.sh \
build/notarized-smoke/mac-cli-bundles/spectre-macosArm64.zip \
0.5.0-rc.smoke
Record the workflow run URL and exact resolved SHA in the release smoke results. The workflow proves Developer ID signing, notarization, stapling, and archive seal; the local live capture is still required because GitHub-hosted macOS runners cannot grant Screen Recording TCC.
Manual cells that remain¶
These cannot currently be made portable and fail-closed by the baseline runner:
- Windows WGC: run
windows-release-smoke.ps1in the logged-in user's native console terminal. SSH and evenPsExec -ican use a service/elevated token that WGC rejects with0x80070424orUnauthorizedAccessException. The Windows harness records hardn/awith reason whendisplayModeiswindows-ssh— do not treat SSH runs as visual PASS evidence. - macOS TCC grant + release seal:
macos-tccalready fail-closes when Screen Recording or Accessibility is Denied / Locked / Unknown. Accessibility names the wrapping app; Screen Recording names Spectre Capture Helper (SpectreCaptureHelper.app), which the harness installs to~/Library/Application Support/spectre/helpers/spectre-screencapture/(same path later capture cells extract) after:recording:assembleScreenCaptureKitHelper(always invoked so the staged tree matches the reviewed SHA). An inconclusive probe reinstalls that runtime bundle so a stale cached helper cannot pin the gate on Unknown. A granted cached helper is also replaced when the freshly assembled tree fingerprints differently, so TCC identity cannot change after macos-tcc already passed.SPECTRE_SCREENCAPTURE_HELPERmust be an absolute path. After a grant, quit/relaunch the wrapping app, run./gradlew --stop, and rerun smoke. Live SCK still/record plus signed-appcodesign --verify --deep --strict,spctl, andxcrun stapler validateremain manual. A local ad-hoc app is not notarization evidence. - Wayland portal: Xvfb proves X11 only. On a real Wayland desktop the Unix harness runs
portal-token-warmupfirst (:recording:runWaylandPortalSmoke) and pinsSPECTRE_WAYLAND_HELPER+SPECTRE_WAYLAND_RESTORE_TOKEN_DIR. Approve Share + Remember / Allow remote interaction for the whole screen once. The warmup storeswayland-rd-restore-token-rd-monitor-embedded. Helper monitor capture/input on the seat reuse that grant (host-native-recording).check, attach, corpus, inject, CLI, and MCP stay underxvfb-runeven whenDISPLAY=:0so Robot-heavy cells do not fight the seated session. Missingxvfb-runis a hard fail for those cells, not a fallback onto the seat. - Homebrew/Scoop/public archives: after the draft artifacts exist, install through the real package manager and rerun launcher/MCP smoke. Local packaging does not prove channel metadata.
- Input focus/lock keys: real Robot input uses global desktop state. Record focus failures and Caps Lock state; restore any modified lock state. Do not silently rerun a case mismatch.
- Multi-monitor / HiDPI / stock IntelliJ: environment-dependent delta cells; keep as recipes when the release delta claims them.
Copy build/smoke/release-smoke.json (+ .md) and Windows' windows-release-smoke.json (+ .md)
into the release record. A report from a different SHA or user session is not evidence for the
release SHA.
Residual gaps for the 0.5.0 cut¶
- Harness baseline (#398): committed one-command entrypoints + shared
REQUIRED_SCENARIO_IDS - fail-closed reports are on main. Operator multi-OS proof for a given SHA should attach
build/smoke/release-smoke.json/.md(macOS + Linux) andwindows-release-smoke.json/.md(Windows) — not chat-only PASS. Use--preflight-only/-PreflightOnlyonly to validate wiring; it is not a release GO. - WGC / host-native-recording on Windows: hard pass only from an interactive desktop
console. SSH runs must record hard
n/awith reason (displayMode: windows-ssh) — never treat SSH as visual PASS evidence. agent-attach-coreon Windows SSH:AgentAttachIntegratione2e includes WGC node screenshots (#362). Underwindows-sshthe harness records hardn/awith reason (same class as WGC recording). Re-run from an interactive console for hard PASS of attach screenshot parity. Inject / launch-and-attach remain SSH-runnable semantics cells.- MCP lifecycle (#399 / #414) is a hard cell on all three entrypoints when packaging is
claimed: Unix
release-smoke.pyand Windowswindows-release-smoke.ps1both require attach → cheap op → detach → session-gone (DaemonFixture MCP e2e) plus strictmcp-stdio-smoke.py(tools/list includesdetach; unknown detach isisError). Packaging and the MCP Gradle leg bake-PVERSION_NAME=<smoke --version>soserverInfo.versionmatches--expected-version. Windows fixture e2e is opt-in with-Pspectre.agent.attachE2e.allowWindows=true(hostedwindows-lateststays skip-safe). Environment-impossible cases (no display, missing Python for the stdio leg, packaging skipped) remain hardn/aorfailwith an explicit reason — never a fake PASS. - Optional
mcp-stdio-smoke.py --attach-pid <pid>proves the same lifecycle over raw stdio when a live fixture PID is available; without a PID the script still fails closed on tools + unknown detach. Release hard pass always rests on the fixture e2e leg for session-gone, not tools/list alone. - #386 (Windows packaged
launch --once+ GradleJVM_ATTACHABLE): product default for Gradle-ish launches expands JVM_ATTACHABLE to 120s (matching agent e2e). The Windows one-linercli-user-flowmay still use Gradle with--app-name ComposeFixtureMain; prefer a healthy UP-TO-DATE fixture build so cold daemon start alone fits the budget. Prod-like launch remains the troubleshooting recommendation when Gradle is slow or flaky. - #433 / #435 pointer-move: verbs shipped on
main(433dd5b). Unix and Windows run:sample-desktop:validationTest --tests '*PointerMoveLive*'with--rerun-tasks --no-build-cacheand reject assumption-skips via JUnit XML. A hardn/anow means the source probe could not seemoveTo/moveBy— treat that as a regression, not a skip. CLI / MCP / agent verbs are still a follow-up. - #459 experimental input coordination: the six delta hard cells are automated as the
input-coord-*scenario IDs on both entrypoints. Each forces--rerun-tasks --no-build-cacheover the coordinator's own tests (LocalCoordinatorServerTestFIFO/cancellation/quarantine/ revoke/forced-recovery,CoordinatorProcessLauncherTestforked coordinator JVM,TwoClientJvmContentionTesttwo forked client JVMs, andInputIsolationLifecycleTest+ParallelPerTestInputIsolationTestfor JUnit PerTest) and fails closed on the JUnit XML. They need no display, so they are hard on macOS, Windows (including SSH), and Linux Xorg/Xvfb. A missing proof source is a hard fail rather than a reasonedn/a(whichhard_failures()ignores), so a rename cannot pass six cells by omission; re-scope the release affirmatively if the feature is genuinely dropped. The one thing these cells do not cover is a fully-headed two-RobotDriverreal-input run: they all pass headless, soinput-coord-headed-robotcarries that claim as a seventh hard cell. - #491 headed two-JVM Robot contention:
input-coord-headed-robotis now driven by:sample-desktop:headedRobotContentionTeston both entrypoints — two forked JVMs, each withRobotDriver(InputLeasePolicy.Required), released from one barrier onto a single focused text field, asserting the two typed blocks never interleave and that the second probe genuinely contended for the keyboard. It runs on Linux underxvfb-run, on a macOS desktop that has granted Robot Accessibility (TCC), and on Windows interactive/RDP; a Windows SSH session has no interactive desktop and still needs-HeadedRobotEvidence. Absent both a run and a signature the cell reports fail (not a reasonedn/a, whichhard_failures()ignores), so the smoke cannot report success without it — and a signature never rescues a red automated run, because an observed interleave is a measurement and a note is not. - Linux helper
cargoon Robot cells: Unix harness prepends$CARGO_HOME/binor~/.cargo/binto PATH. Non-login SSH +xvfb-runotherwise cannot start:recording:buildWaylandHelperwhen--rerun-tasksrebuilds the helper. Do not move JBR Robot cells onto the seated Wayland display to dodge this — helper ScreenCast restore tokens do not cover OpenJDK/JBR Robot (new unparented Share / Remote Desktop dialogs per JVM; see #432). - Manual residual (not auto-green): first-time TCC grant + quit/relaunch (the
macos-tccprobe is automated and fail-closed); notarization / app seal; first Wayland ScreenCast consent duringportal-token-warmup(later cells reuse the restore token); public Homebrew/Scoop/archive after undraft; focus/lock keys; multi-monitor / HiDPI; stock IntelliJ inject. Xvfb still does not prove Wayland portal behaviour.
Windows one-liner script¶
When you have a Windows desktop for a few minutes, run one command from the repo
root (interactive logon session preferred). Prefer PowerShell 7+ (pwsh) when
installed; both hosts need process-scoped Bypass under common Restricted policy.
Preferred (pwsh):
Windows PowerShell 5.1 / stock powershell.exe:
Bypass applies only to that process; it does not weaken machine policy. A bare
.\scripts\windows-release-smoke.ps1 often fails under the common default
LocalMachine Restricted policy, and UTF-8 multi-byte punctuation in the
script historically broke WinPS 5.1 parse (the script is kept ASCII-only so 5.1
can load it without a BOM).
From an absolute path (either host; adjust the repo path):
pwsh -NoProfile -ExecutionPolicy Bypass -File C:\src\spectre\scripts\windows-release-smoke.ps1
# or:
powershell -NoProfile -ExecutionPolicy Bypass -File C:\src\spectre\scripts\windows-release-smoke.ps1
What it does (no second terminal), using the same stable scenario IDs as
scripts/release-smoke.py / scripts/smoke_lib.py:
preflight+ optionalcheckpointer-move— livemoveTo/moveByhover (*PointerMoveLive*)- Agent UI e2e:
agent-attach-core,agent-inject,agent-launch-and-attach(-Pspectre.agent.attachE2e.allowWindows=true, properly quoted for PowerShell;agent-attach-corealso passes-Pspectre.agent.realKeyboard=true) host-native-recording— WGC region smoke only whendisplayModeis interactive (SSH → hardn/awith reason, never fake PASS)cli-packaged/cli-native-helper-layout/ packagedspectre launch --onceascli-user-flow- optional
maven-local-consumerviaverifyMavenLocalPublication
Writes versioned build/smoke/windows-release-smoke.json + .md (schemaVersion from
smoke_lib.SCHEMA_VERSION, full SHA,
dirty flag, environment.displayMode) and exits non-zero on any hard fail.
Flags:
| Flag | Effect |
|---|---|
-Version 0.5.0 |
Release version recorded in the report (default 0.5.0) |
-Base v0.4.1 |
Previous tag recorded in the report |
-SkipCheck |
Skip ./gradlew check (hard n/a with reason) |
-SkipAgentE2e |
Skip Gradle attach/inject/launch tests |
-SkipWgc |
Skip region recording smoke |
-SkipCli |
Skip package + spectre launch |
-SkipPackageCli |
Reuse existing spectre.exe (still runs launch) |
-SkipMavenLocal |
Skip Maven Local publication smoke |
-PreflightOnly |
Preflight + full required-ID matrix as hard n/a with reason (schema self-check). Not a release GO. |
| This is manual, operator-driven automation — not hosted CI. Use it so release smoke is | |
| not multi-terminal faff. Prefer the logged-in interactive console for any WGC cell. |
Recipes (common hard cells)¶
Agent attach on Windows (product + opt-in e2e)¶
Product path (all OSes including Windows 10 1803+):
Optional full UI e2e on a physical Windows desktop (not hosted CI default):
# bash / zsh / cmd
./gradlew :agent:test \
-Pspectre.agent.attachE2e.allowWindows=true \
-Pspectre.agent.realKeyboard=true \
--tests '*AgentAttachIntegration*'
# PowerShell: quote the -P arguments (otherwise PS splits on dots after -Pspectre)
./gradlew :agent:test `
"-Pspectre.agent.attachE2e.allowWindows=true" `
"-Pspectre.agent.realKeyboard=true" `
--tests '*AgentAttachIntegration*'
-Pspectre.agent.realKeyboard=true keeps the Robot typeText subpath, which is opt-in off CI so
./gradlew check stays runnable on a machine in use. Leave the smoke desktop idle while it runs.
This Windows recipe does not cover press-key-tab-after-focus: that scenario lives in
AgentContractCorpusTest, which the command above does not select and which is
@EnabledOnOs(LINUX, MAC) anyway. On Linux and macOS the same property gates both paths — see the
agent-contract-corpus scenario in scripts/release-smoke.py.
See Agent attach. Do not enable this property on headless
windows-latest as a fail-closed gate without a headed runner story.
Agent inject (Linux / macOS / Windows)¶
Same intent as AgentInjectAttachIntegrationTest:
- Build
spectre-agent-runtimewith nestedMETA-INF/spectre/inject-runtime.jar. - Start the inject fixture without
spectre-coreon the child classpath (InjectComposeFixtureMain/ classpath strip helpers in:agenttests). - Target JVM:
-XX:+EnableDynamicAgentLoading, non-headless where UI is required. - Attacher:
AgentAttach.attach(pid, AttachOptions(agentJarPath = …)). - Assert fixture window, non-empty nodes / test tag, clean detach.
- Prefer stderr line that core was injected (not found preinstalled).
On Windows, attach, inject, and launch UI e2e share
-Pspectre.agent.attachE2e.allowWindows=true (or the one-liner script).
Launch-and-attach¶
LaunchAndAttach / spectre launch with a short-lived Compose fixture or
java -jar. Assert readiness stages complete and attach returns a usable
automator. Separate failures: process death vs attach vs empty tree. Covered by the
Windows smoke script’s agent e2e + packaged spectre launch --once step.
macOS Capture Helper¶
- Consume
spectre-recording-macosfrom Maven Local or the release jar. - Confirm tree is
native/macos/SpectreCaptureHelper.app/..., not a bare Mach-O. spectre permissions check/requestas needed; Settings row should name Spectre Capture Helper.- One still or short window/region capture via library or CLI.
Windows WGC helper¶
- Open
spectre-recording-windowsjar; confirm dual-arch multi-file layout (see packaging contract inbuildSrc/ Publishing). Portal checker and Gradle both assert multi-file basenames. - Packaging may be verified over SSH (extract + list files).
- Live capture: prefer the Windows smoke script or
:recording:runWindowsGraphicsCaptureRegionSmokeon a logged-in desktop. SSH can work but sometimes yields WGC0x80070424or black frames.
After a successful smoke¶
- Tag only the smoked SHA (
vX.Y.Z). - Watch
release.yml(matrix, notary, publish). - Promote Central + undraft per Publishing.
- Optionally re-run B7/B12 against public CLI archives once package channels land.
- If smoke discovered a permanent evidence gap, update
CapabilityMatrix/ the guide rather than leaving docs overselling CI.
What smoke is not¶
- Not a full substitute for runtime-matrix or validation workflows.
- Not permission to enable flaky Windows e2e on headless GH runners.
- Not required for every PR — only for release tags (and RC tags if you cut them).
- Not a place to re-litigate Experimental APIs; soft-smoke them or keep them out of headline release claims.