Testing¶
Expectations¶
Always add tests for behaviour changes.
- Pure logic in
coreshould get unit tests. - Serialisation, DTO mapping, and remote-call behaviour in
servershould get contract tests. - Recording code should separate testable pure logic from OS-specific integration and test the pure parts directly.
- UI-facing spike work in
sample-desktopmay require manual validation, but any reusable logic extracted from it should still receive automated tests. - Run targeted tests while iterating, then finish with the broader relevant verification pass.
- Before pushing, ensure the CI-shaped path is green locally when practical:
./gradlew check.
TDD Red-Green Cycle¶
Follow this strictly:
- Write the test first.
- Run it and confirm it fails for the missing behaviour.
- Write the minimum implementation.
- Re-run the targeted test to green.
- Run the broader relevant suite.
Do not treat “I wrote the test after the code and it passes” as evidence. If a test was never red, recreate the failure before considering the work done.
Test Completeness Check¶
Before marking testing work complete:
- review every planned scenario and edge case
- confirm each one has a corresponding test or a deliberate manual-validation note
- fill the gaps before moving on
Package-channel (Homebrew / Scoop) contracts¶
CLI package manifests are not optional docs — they are install paths. They are split so
a clean Linux host without Ruby can still run ./gradlew check (issue #400):
| Gradle task | Host tools | On check when |
|---|---|---|
verifyCliPackageManifests |
bash, python3 |
Every Unix host |
verifyHomebrewFormulaInstallSemantics |
bash, python3, Ruby |
Unix and (Ruby on PATH or CI is set) |
verifyCliPackageManifestHostDeps |
bash, python3 |
Every Unix host (regression: no undeclared Ruby on the structural path; missing Ruby on the semantics path fails with a preflight message) |
Scripts:
- structural/generator:
.github/scripts/test-generate-cli-package-manifests.sh - install semantics (Ruby):
.github/scripts/test-homebrew-formula-install-semantics.sh→.rb - host-dep regression:
.github/scripts/test-cli-package-manifest-host-deps.sh
Structural checks generate fixture manifests, assert Scoop JSON + formula text contracts
(wrapper bin entry, dual-layout Spectre.app discovery snippets, install-body alignment
between generator output and committed Formula/spectre.rb). They must not invoke Ruby.
Install-semantics evaluate the formula's real app = Dir[...] expression against nested
and Homebrew-stripped layouts and require a wrapper (not a Roast bin.install_symlink).
They need Ruby. Locally, if Ruby is absent the task is skipped (structural + host-dep
guards still run). Under CI (CI env set), the task always runs and fails closed with
an actionable preflight if Ruby is missing:
error: 'ruby' is required for verifyHomebrewFormulaInstallSemantics
Install: apt install ruby | brew install ruby | …
GitHub ubuntu-latest typically ships Ruby; if that ever changes, install Ruby in
.github/workflows/ci.yml before ./gradlew check (do not re-merge semantics into the
structural script). For a full local package-channel gate on Linux: apt install ruby
(or equivalent), then ./gradlew verifyHomebrewFormulaInstallSemantics.
Cross-Boundary Contract Tests¶
When a feature spans a boundary, add at least one test that exercises the real boundary shape.
Examples for Spectre:
- HTTP request/response payloads in
server - compound node identity formatting and parsing
- coordinate conversion behaviour across Compose/AWT/Robot units
- native helper invocation contracts for recording
- coordinator frames plus real forked-JVM endpoint/process behavior
- JUnit extension/rule lease ownership across factory, artifacts, video, and teardown
Desktop coordination tests use a fake clock/state machine for FIFO, fencing, expiry, cancellation, and recovery. At least one forked boundary test must use the real local socket and process. Live desktop validation records lease ordering and an observable UI result; a skipped assumption is not a pass. Platform release smoke covers macOS, Windows, and Linux Xorg/Xvfb independently, while Wayland remains synthetic-input-only.
These tests should use the real payload or coordinate format rather than a hand-crafted idealized version. Unit tests for internal math are necessary, but boundary tests catch drift between layers.
Running ./gradlew check Locally¶
./gradlew check is the pre-push gate, so it must stay runnable on a machine you are also using.
Two test paths need the whole desktop to themselves when they opt into real
java.awt.Robot key events at a spawned Compose window, which only works while that window
owns OS keyboard focus. A terminal, an editor, or a notification taking focus mid-run used
to fail them. The default automator path is synthetic AWT input and does not need OS focus;
these gated paths exist for the remaining real-keyboard contract cells.
| Path | Where |
|---|---|
typeText into the fixture text field |
AgentAttachIntegrationTest (:agent) |
press-key-tab-after-focus corpus scenario |
AutomatorContractCorpus, run by AgentContractCorpusTest (:agent) |
One gate governs both: RealKeyboardGate in :testing. By default the paths run on CI (CI=true
in the environment) and are skipped on developer machines, where each path prints to stderr
exactly what it skipped. Everything else runs on every host — attach, windows(),
findByTestTag, click(), doubleClick(), swipe(), scrollWheel(), window identity, and
screenshots.
When the gate is off, the corpus records press-key-tab-after-focus as passed with the detail
skipped:real-keyboard-gate-off and never touches the driver. Raising and clicking the fixture
window are themselves focus-stealing, so a gated-off run has to do nothing at all rather than try
and tolerate the failure.
Run the keyboard paths yourself on an idle desktop:
# PowerShell: quote -P… so the shell does not split on the property name
./gradlew check "-Pspectre.agent.realKeyboard=true"
Pass -Pspectre.agent.realKeyboard=false to turn them off on CI. The :agent, :server, and
:testing test tasks all forward the property to their workers, and both the property and the CI
environment variable are task inputs, so switching modes never reuses a cached result from the
other mode.
When the paths do run their assertions are unchanged: hosted macOS CI still tolerates a lost OS focus handoff, Linux Xvfb stays fail-closed, and a local opt-in run fails loudly so a real keyboard regression is visible.
Manual Spike Validation¶
Some concerns still need live manual verification even with good automated tests:
- Retina/HiDPI coordinate accuracy
- popup discovery across different layer modes
- Robot focus behaviour and click targeting
- recording permission and capture behaviour on macOS
- AWT/Compose Desktop painting and
Robotcapture when the test JVM runs under a macOSsandbox-execprofile; see Running on CI
Use sample-desktop to make those checks reproducible. If a manual validation step is required
for a change, note it explicitly in the final report.
Coroutine Testing¶
- Prefer
runTestfor coroutine-based logic that is not Spectre UI automation. - Prefer
runSpectreTest(from:testing) for Spectre UI tests: real wall-clockdelayfor longClick/swipe/paste settle, plus unfinished-child leak detection. Do not userunTestfor Spectre interaction tests (virtual time collapses internal delays). PlainrunBlockingremains a valid fallback. - Avoid real sleeps when a deterministic scheduler or fake clock will do.
- Cancel/close long-lived scopes created in tests.
- If asynchronous behaviour cannot be made deterministic, isolate the nondeterminism behind a small interface and test the decision logic separately.