| id | validation.testing | ||
|---|---|---|---|
| title | Testing Guide | ||
| description | Meaningful verification by runtime, platform, and evidence level. | ||
| category | Engineering | ||
| locale | en | ||
| canonical | true | ||
| status | stable | ||
| owner | Quality maintainers | ||
| reviewed | 2026-10-31 | ||
| revision | 2026-10-31 | ||
| source_of_truth | docs/testing.md | ||
| validators |
|
AgentDeck currently uses 4 test frameworks across the monorepo:
- TypeScript packages (
bridge,plugin,shared,hooks) use Vitest - Android uses JUnit + Robolectric
- Apple uses XCTest
- ESP32 validation uses Robot Framework
The root pnpm test command runs only the Vitest suite configured in the repository root. Platform-specific suites are executed separately or through scripts/test-report.sh.
What each gate proves — and does not prove — is catalogued in scripts/verification-catalog.json and published on the GitHub Pages report's "What we verify" tab. The catalog also assigns every test file to a domain (ordered glob patterns, first match wins). scripts/__tests__/verification-catalog.test.ts fails when a workflow is missing from it, a path it names is gone, or a test file matches no domain, so the public page cannot silently drift from the repository. The per-file lists below are illustrative; the catalog is the complete map.
Three local tiers run the same steps at three depths. The steps, their tiers, the paths that select them and the toolchain each needs are defined once, in the tiers and steps of scripts/verification-catalog.json. scripts/verify.mjs runs them, and the Pages report renders the same list as its "Verification tiers" matrix.
| Tier | Command | When | What it runs |
|---|---|---|---|
| Changed-area | pnpm verify:changed |
While iterating; before pushing a focused fix | Steps whose paths match a file changed since the merge base with origin/master (committed, staged, unstaged, untracked). Vitest runs vitest related on changed sources plus tests that name a changed non-code file. A change to a package.json, the lockfile, a tsconfig or a Vitest config runs the whole suite. Android, Apple and ESP32 suites run when their trees change and the toolchain is present. |
| Quick | pnpm verify:quick |
Before every push or PR, about 2–3 minutes | The ubuntu PR gates: version sync, build, type check, ESLint and design-lint ratchets, the whole Vitest suite, daemon E2E, protocol and preview-mirror drift, tokens, docs, catalog, devlog and Pages parity. |
| Pre-release | pnpm verify:full |
Before any release tag | Quick plus the coverage floor, every native suite this host can run (Android, macOS XCTest, iOS build, App Store bundle, ESP32 host tests, simulator and TTGO builds), the clean npm package acceptance, and the lab gates (ESP32 Robot, device deploy, Swift Codex live acceptance), which a person attests. |
Every run prints a summary and writes a receipt to coverage/verify/<tier>.json. A step is pass, fail, skip with its reason (missing toolchain, wrong platform, an opt-in such as the macOS E2E), or manual for an unattested lab gate. A skip never counts as a pass. Useful flags:
node scripts/verify.mjs --list # every tier and step
pnpm verify:changed -- --dry-run # what would run for this diff
pnpm verify:changed -- --base origin/release-x # compare against another ref
pnpm verify:quick -- --only vitest,e2e --allow-keychain # run the macOS-opt-in E2E too
pnpm verify:full -- --attest esp32-robot=pass:"box_86, ips_35" --record--record (pre-release tier only, clean tree) also writes verification/receipts/<date>-<sha>.json. Commit that file: the published Test Report shows the newest one as "Last recorded pre-release check", including which steps were skipped on that host and which lab gates were attested or left unattested. --report (on by default for verify:full) renders the Build Health page locally to coverage/test-report/index.html from only this run's results.
The tiers do not replace CI. The hosted jobs still run on every PR, including the two no Mac or Linux host can reproduce locally: windows-native-runtime (Node 22/24/26) and macos-native-parity.
tests/e2e/daemon-hub.e2e.test.ts (pnpm test:e2e, own config vitest.e2e.config.ts) starts bridge/dist/cli.js daemon start --foreground --local --loopback as a child process in a fresh temp HOME, on a free port inside a private --port-window, and drives it only from outside: /health, POST /hooks/*, a WebSocket dashboard client, the exact hook shell snippet the installer writes, SIGTERM and a restart. It needs pnpm build first and takes a few seconds.
It is safe beside a live daemon (outside 9120–9139, so the singleton guard never asks the real one to stand down) and raises no OS prompts on Linux: loopback bind, every device module off, no LAN emission. It also leaves the developer's own agent tools alone: AGENTDECK_DAEMON_NO_GATEWAY=1 keeps the daemon off a local OpenClaw Gateway (no probe, no adapter, no openclaw doctor), and AGENTDECK_CODEX_LIVE_USAGE=0 keeps the Codex usage probe from spawning codex. On macOS it is opt-in (AGENTDECK_E2E_ALLOW_DARWIN=1) because the darwin usage poller reads the login Keychain. CI runs it on every PR (ci.yml) and the Pages report re-runs it on master.
After building and launching the signed, sandboxed macOS app, run:
node scripts/verify-swift-codex-live.mjs --port PORT --pid PID --app /path/AgentDeck.app --allow-visible-fixturesThis replay creates three visible agentdeck-live-* sessions and leaves labeled
timeline entries. Use a dedicated test app/data store whenever possible. The
explicit --allow-visible-fixtures flag is required before any network request;
do not run it silently against the user’s daily dashboard.
Use the registry-resolved serving port and the PID of that exact app. The runner
refuses Node daemons, PID changes, a different app path, and unsigned or
unsandboxed builds. It drives the real HTTP routes and checks both /status
and the WebSocket roster: idle start, prompt, repeated start, hook ownership,
late OTel, OTel-only/notify fallback, old-turn rejection, actual terminal expiry,
post-expiry suppression, and hook re-engagement. Allow about two minutes.
The runner does not start agents, call a model, edit configuration or trust,
or switch daemon ownership. If Node owns the port, use the supported daemon
lifecycle commands to stop it for the test and restore it afterwards. A JSON
receipt under diagnostics/swift-codex-live/ records the app binary hashes,
PID, sandbox check, assertions and cleanup; --output selects another path.
Only this run's synthetic roster rows are removed. Labeled synthetic timeline
entries remain as evidence; real session content and pairing tokens are omitted
from the receipt. A failed cleanup fails the run.
This is live sandboxed transport evidence using synthetic events. It does not prove actual CLI emission, first-run trust, Kiro folder consent/revocation, OpenCode SSE reconnect, App Store distribution, or physical display rendering.
verify-daemon-lifecycle.mjs is an
opt-in real-process lab gate. It never targets the installed app or ports
9120–9139. Build the current macOS app using the
Apple workflow, then prepare:
- Copy the complete app to
/private/tmp/agentdeck-lifecycle-acceptance/AgentDeck Lifecycle Audit.app. Give this copy bundle IDbound.serendipity.agent.deck.lifecycle-auditand a distinct display name. Keep all frameworks, resources and, for Debug builds,AgentDeck.debug.dylib: the Debug executable alone is only a trampoline. - Sign that copy with the local Developer ID identity and App Sandbox/network
entitlements from the production app. An alternate identifier may require
removing its copied provisioning profile and WeatherKit entitlement; this is
a local signed derivative, not an installed Release or Store acceptance.
Verify it with
codesign --verify --deep --strict. - Compile
lifecycle-preferences.swiftas the executable of a temporary Foundation-only helper bundle with that same alternate identifier and sandbox signature. Run the helper with argument19450before launching the app. It sets typed own-app preferences for loopback-only/no-device mode and prints its private containerdataDirectory. Launch-argument strings are not a replacement for typed port preferences. It writes only synthetic APME/provider settings inside that test container. - Keep 19450–19459 free. Run the command below with the printed directory. Do not launch the copy manually: the runner supplies the private data/window, Gateway opt-out and UI discovery-disable settings on every launch.
node scripts/verify-daemon-lifecycle.mjs --allow-visible-fixtures \
--app '/private/tmp/agentdeck-lifecycle-acceptance/AgentDeck Lifecycle Audit.app' \
--swift-data 'TEST_CONTAINER_DATA_DIRECTORY' \
--out diagnostics/lifecycle-integration/runAdd --display-only to measure actual display-only sleep/wake in Swift-only,
Node-only and Node-owner/Swift-client mode. Leave the keyboard, mouse and trackpad
idle during the run. An independent 25ms CoreGraphics probe separates macOS
transition timing from WebSocket edge delivery; each direction must arrive within
one second of that observation. The runner prevents system idle sleep temporarily,
wakes the display even on failure, and keeps the same private owner PID. It uses
isolated protocol clients, not physical-device rendering or installed-app delivery
acceptance. Unlike --sleep-mode, it does not suspend the shared host.
The runner checks the sandbox signature and alternate identity before launching;
uses isolated Node HOME/data and a Keychain-denying shim; starts the CLI with
--local --loopback --no-build; and exercises both solo modes, both startup
orders, concurrent startup, client app termination/relaunch, Node CLI restart,
owner crash and incoming-owner crash after Swift has yielded. It verifies real
HTTP/WS history, token continuity without recording token values, one prompt
receipt per marker, no client TCP listener and no shared-window sockets. Cleanup
signals only processes whose executable and private data scope were verified.
results.json records scenario timings; on failure failure.json identifies the
step. Logs and synthetic histories remain in private diagnostic/test stores.
Allow several minutes: failed incoming takeover deliberately waits out the
production yield lease, rather than shortening it for acceptance. The
2026-10-31 signed derivative run is recorded in the
feature boundary table.
Add --configuration --fresh for session-pin and Pixoo settings acceptance:
Swift-only and Node-only edits/restart, both startup orders, native-client
relaunch/promotion, repeated handover, stale-revision and unsupported-replica
refusal, explicit removals and real CLI editing against the Swift owner. All
addresses are synthetic loopback devices and hardware modules remain disabled;
this does not certify physical output or cold recovery without a receiving peer.
For opt-in cold recovery, bootstrap the alternate signed fixture's preferences
helper with its --recovery argument, then run the same isolated runner with
--cold-recovery --fresh. Its bookmark targets only RecoveryFixture in the
disposable container. The scenarios stop one runtime before starting the other,
check settings/removals/opaque receipt replay and disconnect, and reject an
ungranted native path. This proves process/storage integration; external-folder
Powerbox selection and real post-relaunch bookmark access remain separate UI gates.
For that UI gate, clear only the alternate fixture's recovery preference keys with
the signed helper's --clear-recovery option. Keep its isolated data root and
19450–19459 window; select a disposable external local parent through the
actual app's system folder picker. Do not seed a bookmark with the helper. Verify
the child store can be written, quit and relaunch the exact signed fixture with
the same environment, and verify access without reselecting. Include cancellation
and disconnect while checking that pre-existing sentinel files and stored history
remain unchanged. The Mac must be unlocked for native UI automation.
Conflict regression tests exercise both explicit choices, rejected stale previews, private-file preservation, v1 migration and refusal to replay an acknowledged approval. UI acceptance additionally requires a published Node/Swift conflict, the corresponding Review Conflicts counts and an actual choice followed by producer adoption. Passing storage tests alone does not certify these UI steps. The 2026-11-01 signed Release run completed those external-picker/relaunch, Node saved-profile choice, Swift recovery-profile choice, stale-preview rejection and disconnect/relaunch steps; the acceptance table is in the feature matrix.
Add --inflight --fresh for the synthetic approval, adaptive Feed, checkpoint
replica, paused-owner and OTA push/pull scenarios. --fresh resets only the
explicit isolated synthetic stores after the private window is proven vacant.
The pull test explicitly stages identical images on both owners; it does not
implement stage/configuration distribution. Source tests cover wall-clock expiry
before sleeping timers run and retired IOKit registrations.
Actual machine sleep uses a dedicated Mac, never the shared hardware host.
Prepare the same signed derivative and private runtime there, then use
--sleep-mode swift, node and both separately with fresh synthetic data scopes.
--sleep-now --dedicated-sleep-host requires the shared daemon window to be
provably free and invokes macOS sleep from this developer fixture immediately after
approval and OTA waits are live. Arrange physical wake after at least 45 seconds;
do not change power settings. The controller must verify actual OS sleep and
full wake records before writing wake-verified.json in that run's output folder.
Elapsed time, SIGSTOP or an unavailable SSH connection alone are not sleep proof.
--signal-cleanup avoids Apple Events for cleanup on an SSH-only fixture host;
it signals only the verified alternate app process. Keep baseline and partial
attempts separate from completed acceptance receipts.
This does not certify installed delivery, real provider/source grants, physical writers, remote network partitions, absent-replica recovery or configuration migration. X4 physical acceptance remains deferred to companion-app integration.
Agent-driven build → deploy → check runs on a Mac used to stall on privacy dialogs (Automation, Accessibility, Screen Recording, firewall). bash scripts/macos-preflight.sh --automation --accessibility --screen-recording [--firewall <app>] --json checks each grant with a bounded, non-interactive probe and reports granted / denied / unknown per check (exit 2 = something denied, with the exact System Settings path; exit 3 = could not tell). The agentdeck-deploy skill and the screenshot/recording scripts run it first.
pnpm test # Run root Vitest suite
pnpm test -- --watch # Watch mode
pnpm vitest run --coverage # Coverage report + threshold check
pnpm build && pnpm test:e2e # Real daemon process end-to-end (tests/e2e/)
pnpm test:report # Unified report across all configured frameworks
pnpm test:android # Android suite via unified report script
bash scripts/test-report.sh --report # Report from existing results (no execution)bridge/src/__tests__/
adapter.test.ts # Adapter factory, MonitorAdapter, ClaudeCode lifecycle, OpenClaw protocol
bridge-core.test.ts # BridgeCore orchestration, state building, usage broadcast
codex-output-parser.test.ts # Codex CLI parser coverage
cursor-sync.test.ts # OutputParser + StateMachine cursor tracking
daemon-lifecycle.test.ts # Daemon singleton guard, session registry, PID validation
esp32-serial-node.test.ts # Serial bridge protocol, event filtering, JSON messages
output-parser.test.ts # ANSI parsing, mode detection, spinner, markdown (~95KB)
pixoo-sprites.test.ts # Pixoo sprite generation invariants
server-integration.test.ts # HookServer + WsServer + StateMachine integration
session-registry.test.ts # daemon.json paths, process alive checks
session-timeline-relay.test.ts # Daemon relay for sibling session timeline events
state-machine.test.ts # State transitions, timeouts, billing, permission modes
tier3-integration.test.ts # mDNS crash recovery, display sync, voice transcription
timeline-integration.test.ts # Timeline store dedup, enrichment pipeline
tui-dashboard.test.ts # TUI dashboard layout/render behavior
tui-renderer-snapshots.test.ts # TUI renderer snapshots
tui-terrarium-snapshots.test.ts # Braille terrarium snapshots
usage-relay.test.ts # 3-tier usage relay (HTTP/WS/direct)
plugin/src/__tests__/
connection-integration.test.ts # Real WS servers, Bridge/Gateway priority
connection-manager.test.ts # ConnectionManager with mocked BridgeClient
option-scenario.test.ts # 6-option SELECT, button layout
renderer-snapshots.test.ts # Stream Deck renderer snapshot coverage
text-utils-and-labels.test.ts # CJK width, text wrapping, label abbreviation
shared/src/__tests__/
protocol-contract.test.ts # BridgeEvent JSON shape (5 client platforms)
timeline.test.ts # cleanDetailText, dedup, parseLogLine, keyword similarity
hooks/src/__tests__/
install.test.ts # Hook installation, v2.1+ matcher-group format
android/app/src/test/kotlin/dev/agentdeck/
net/ProtocolTest.kt # parseBridgeMessage (all event types), PluginCommands, edge cases
state/TimelineStoreTest.kt # addEntry dedup, upsert, groupConsecutive, merge, MAX_ENTRIES
state/SessionMetricsTest.kt # connect/disconnect lifecycle, reconnect counting
util/TimeFormatUtilsTest.kt # formatCount, gaugeBar, formatBytes, formatDurationCompact
Requires JDK 17. Run with:
cd android && JAVA_HOME=$(brew --prefix openjdk@17)/libexec/openjdk.jdk/Contents/Home ./gradlew testDebugUnitTestapple/AgentDeckTests/
ProtocolTests.swift # BridgeEventParser parsing
TimelineTests.swift # Timeline entry decoding + grouping + store
esp32/robot/tests/
01_build.robot # Local pre-flash smoke: build outputs + binary size (no hardware)
02_flash_and_boot.robot # Device flash, boot messages, heap/PSRAM (hardware required)
03_serial_protocol.robot # JSON protocol, state_update, error recovery (hardware required)
04_performance.robot # Boot, latency, throughput, and heap metrics (hardware required)
Run with bash esp32/robot/run.sh build (no hardware) or bash esp32/robot/run.sh all (full).
The no-hw build suite is intentionally local-only validation before flashing. It is not run in GitHub CI or Pages because it only wraps the same PlatformIO compilation and artifact checks, adding substantial duplicate build time without exercising firmware behavior. The meaningful Robot suites are the hw, protocol, and perf runs against connected boards.
Coverage thresholds are configured in vitest.config.ts and enforced in CI. They sit about three points under measured coverage (2026-09-28: lines 59.6%, functions 60.0%, branches 55.3%, statements 58.6%), so a change that deletes tests or lands a large untested module fails. The previous floor (17/15/14/16) was ~40 points below reality and could not catch a regression. Raise them as coverage improves; do not lower them to land a change. The Pages report reads the numbers from the config rather than quoting them.
Coverage is generated only for the Vitest-managed TypeScript packages:
bridge/src/**/*.tsshared/src/**/*.tsplugin/src/**/*.tshooks/src/**/*.ts
Excluded from the Vitest coverage job:
**/__tests__/****/node_modules/****/dist/**
Generate a report:
pnpm vitest run --coverage # Terminal summary + lcov + json-summary- State Machine — transitions, timeouts, permission/option/diff flows, billing detection
- Output Parser — ANSI parsing, mode detection, spinner events, cursor sync
- Adapter Hierarchy — factory, ClaudeCode/OpenClaw/Monitor capabilities, Gateway protocol, lifecycle events
- Timeline —
parseLogLine()(structured patterns only — heuristic word-match retired in8c3a4278),cleanDetailText(), semantic dedup, keyword similarity, groupConsecutive - Connection Manager — Bridge/Gateway priority, failover, event forwarding
- Hook Installation — v2.1+ matcher-group format, migration, idempotency
- Android Protocol — all BridgeEvent types parsed, PluginCommands JSON generation
- Android State — TimelineStore dedup/upsert/merge, SessionMetrics lifecycle
| Area | Files | Reason |
|---|---|---|
| Plugin actions | 9 action handlers | Heavy SD SDK dependency |
| SVG renderers | 10 renderer files | Visual output — snapshot testing TBD |
| TUI dashboard | 6 files | Terminal rendering — visual inspection |
| Device modules | adb, serial, mdns, pixoo | Hardware-dependent |
| Voice system | voice, speech helper, TTS | Audio hardware + external process |
| Daemon server | daemon-server.ts | Requires full process lifecycle |
| Android UI | 31 Compose files | Compose UI testing framework TBD |
| Android terrarium | 20 creature/env files | Canvas rendering — screenshot testing TBD |
| Apple app | 41 Swift files | Most modules untested |
pnpm test:report is the pre-release tier with the local report: node scripts/verify.mjs --tier full --report. scripts/test-report.sh remains as an alias that maps its old flags (--report, --vitest, --android, --apple) onto verify.mjs. Its previous implementation needed bash 4 associative arrays and failed on its first line under macOS's /bin/bash 3.2.
Output includes:
- Terminal table with pass/fail/skip/manual per step, and the receipt at
coverage/verify/full.json - The Build Health page at
coverage/test-report/index.html, plussummary.jsonandrun-metadata.json - Vitest and E2E JSON at
coverage/test-report/vitest.jsonande2e.json
The GitHub Pages report also renders a scenario coverage mapping. That view is based on explicit file + assertion/case pattern matches from scripts/scenario-matrix.json, not raw code coverage percentages.
GitHub Actions currently runs on every push and PR to master:
# .github/workflows/ci.yml
- pnpm install --frozen-lockfile
- pnpm build
- pnpm typecheck
- npx vitest run --coverage # the whole Vitest suite once, plus the coverage floor
- pnpm test:e2e # real daemon process (tests/e2e/)
- node scripts/check-eslint-baseline.mjs # ESLint error ratchet (scripts/eslint-baseline.json)Current CI details:
- Runner:
ubuntu-latest, Node 22. A separatewindows-latestjob covers the Node 22/24/26 native runtime, and amacos-26job (macos-native-parity) runs the Vitest cases gated onprocess.platform === 'darwin': generated-Swift parity vectors, launchd probes, the Foundation Models helper build. Those cases are skipped on ubuntu andapple-test.ymlruns XCTest, not Vitest, so before this job they ran on no CI runner.verification-catalog.test.tsfails when a platform-gated test file is missing from its job. - ESLint runs as a ratchet: the error count may not grow past
scripts/eslint-baseline.json. It was configured but ran in no workflow, and had grown to 181 errors, 166 of them config noise (the vendored Ulanzi browser SDK linted with Node globals). - Included: version sync, build, typecheck, Vitest with coverage floor, daemon E2E, preview-mirror and protocol drift
- Vitest runs once:
--coverageexecutes the same suite aspnpm test, so running both only doubled the time - Not included: Apple XCTest (own workflow) and physical-hardware ESP32 Robot Framework (lab only)
Android compilation and its JUnit + Robolectric suite are a separate, path-scoped check — .github/workflows/android-test.yml runs ./gradlew :app:testDebugUnitTest on pushes and PRs that touch android/**, and uploads the HTML/XML reports as an artifact. It exists because ci.yml never reads the Android sources: before it, a PR changing only android/** could go all-green without anything having compiled its Kotlin, since the Android run inside test-report.yml fires only on push to master. The debug variant needs no release signing secrets, and the GitHub-hosted runners ship the Android SDK, so the job is just JDK 17 + Gradle.
ESP32 C++ has the same path-scoped shape (#243) — .github/workflows/esp32-sim.yml runs pio run over every esp32/sim env (the native host build of the real esp32/src render trees, no Xtensa toolchain) on PRs touching esp32/**. Before it, the only job compiling firmware sources was the sim build inside test-report.yml, which fires on push to master — so a firmware compile error surfaced after merge, or at an esp32-v* tag. It shares test-report.yml's pio-sim-* cache key so master pushes keep the cache PR runs restore from. The same job runs esp32/sim/run-tests.sh, the host-compiled assertions over the firmware's policy headers, which used to be local-only. Its ttgo job links the real TTGO image on the release Xtensa toolchain, the one hosted check of static DRAM. The workflow also runs on each master push that brings an esp32/** change, because a PR is compiled against the master it was tested with: #565 and #566 merged 8 seconds apart on 2026-10-18, each green, and master's TTGO then overflowed static DRAM by 8 bytes without anything reporting it. Behaviour on boards stays with the lab Robot suites, as below.
The Android run inside test-report.yml remains a non-blocking diagnostic feeding the Build Health page. Apple XCTest is not yet in the Linux CI job. Robot Framework's meaningful behavioral coverage requires physical hardware and remains a lab/local workflow rather than a GitHub-hosted check.
Release workflows (Android, Apple) are tag-triggered and do not run tests.
- Place tests in
{package}/src/__tests__/{module}.test.ts - Use
vi.mock()for external dependencies (node-pty, ws, fs, child_process) - Use
vi.useFakeTimers()for timeout/interval testing - Import from source with
.jsextension (ESM) - Android tests in
android/app/src/test/kotlin/dev/agentdeck/mirroring source structure
// Module mock (node-pty, express, http, ws)
vi.mock('node-pty', () => ({ spawn: vi.fn() }));
// HTTP server mock (include closeAllConnections for shutdown)
vi.mock('http', async () => {
const actual = await vi.importActual<typeof import('http')>('http');
return {
...actual,
createServer: vi.fn(() => ({
listen: vi.fn((_p, _h, cb) => cb()),
close: vi.fn((cb) => cb()),
closeAllConnections: vi.fn(),
on: vi.fn(),
})),
};
});
// Function spy
const handler = vi.fn();
emitter.on('event', handler);
expect(handler).toHaveBeenCalledWith(expected);
// Fake timers
vi.useFakeTimers();
vi.advanceTimersByTime(5000);
vi.useRealTimers();bridge/src/__tests__/helpers/temp-data-dir.ts— Isolated temp filesystem for daemon/registry testsbridge/src/__tests__/helpers/ws-test-client.ts— WebSocket client simulation
When adding tests, prioritize by impact:
- Shared types/utils — contract between packages, highest ROI
- State machine transitions — core correctness
- Parser logic — data transformation accuracy
- Protocol handling — client-server contract
- Android state/network — cross-platform parity
- Renderers — snapshot tests if visual regressions matter
The same Apple targets have Store (Debug/Release) and explicit DotPreview
configurations. pnpm verify:changed checks macOS XCTest in both variants, optimized
Store builds on macOS/iOS, and an optimized preview on both platforms. The native runner is
apple/scripts/verify-build.sh; its default derived data is checkout-local.
AGENTDECK_APPLE_DERIVED_DATA can select another isolated build cache.
AppleBuildBoundaryTests verifies compiled/resource identity, incoming external Dot
snapshot isolation and unchanged matrix output in Store builds. Dot's own hosting tests
remain active in DotPreview. scripts/__tests__/apple-dot-build.test.ts exercises the
artifact verifier's rejection paths with macOS/iOS fixtures, including Debug dylibs,
leaked resources and mislabeled previews. verify-appstore-archive.sh runs the same
check with excluded against the actual submitted app. These checks do not prove
Cloudflare deployment, real Dot interoperability, UI/device acceptance or App Review.
See the configurations and release procedure.