Skip to content

Latest commit

 

History

History
467 lines (369 loc) · 29.5 KB

testing.md

File metadata and controls

467 lines (369 loc) · 29.5 KB
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
pnpm test
pnpm verify:quick

Testing Guide

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.

Verification tiers

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.

End-to-end: the real daemon process

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.

Swift Codex live acceptance on a Mac

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-fixtures

This 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.

Signed Swift/Node lifecycle acceptance

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:

  1. Copy the complete app to /private/tmp/agentdeck-lifecycle-acceptance/AgentDeck Lifecycle Audit.app. Give this copy bundle ID bound.serendipity.agent.deck.lifecycle-audit and a distinct display name. Keep all frameworks, resources and, for Debug builds, AgentDeck.debug.dylib: the Debug executable alone is only a trampoline.
  2. 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.
  3. Compile lifecycle-preferences.swift as the executable of a temporary Foundation-only helper bundle with that same alternate identifier and sandbox signature. Run the helper with argument 19450 before launching the app. It sets typed own-app preferences for loopback-only/no-device mode and prints its private container dataDirectory. Launch-argument strings are not a replacement for typed port preferences. It writes only synthetic APME/provider settings inside that test container.
  4. 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/run

Add --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.

macOS device runs: preflight before you deploy

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.

Quick Start

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)

Test Structure

Vitest (TypeScript — bridge, plugin, shared, hooks)

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 (JUnit + Robolectric)

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 testDebugUnitTest

Apple (XCTest)

apple/AgentDeckTests/
  ProtocolTests.swift          # BridgeEventParser parsing
  TimelineTests.swift          # Timeline entry decoding + grouping + store

ESP32 (Robot Framework)

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

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 Scope

Coverage is generated only for the Vitest-managed TypeScript packages:

  • bridge/src/**/*.ts
  • shared/src/**/*.ts
  • plugin/src/**/*.ts
  • hooks/src/**/*.ts

Excluded from the Vitest coverage job:

  • **/__tests__/**
  • **/node_modules/**
  • **/dist/**

Generate a report:

pnpm vitest run --coverage       # Terminal summary + lcov + json-summary

Well-Tested Areas

  • 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 in 8c3a4278), 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

Known Gaps

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

Unified Test Report

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, plus summary.json and run-metadata.json
  • Vitest and E2E JSON at coverage/test-report/vitest.json and e2e.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.

CI Pipeline

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 separate windows-latest job covers the Node 22/24/26 native runtime, and a macos-26 job (macos-native-parity) runs the Vitest cases gated on process.platform === 'darwin': generated-Swift parity vectors, launchd probes, the Foundation Models helper build. Those cases are skipped on ubuntu and apple-test.yml runs XCTest, not Vitest, so before this job they ran on no CI runner. verification-catalog.test.ts fails 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: --coverage executes the same suite as pnpm 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.

Writing Tests

Conventions

  • 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 .js extension (ESM)
  • Android tests in android/app/src/test/kotlin/dev/agentdeck/ mirroring source structure

Mocking Patterns

// 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();

Test Helpers

  • bridge/src/__tests__/helpers/temp-data-dir.ts — Isolated temp filesystem for daemon/registry tests
  • bridge/src/__tests__/helpers/ws-test-client.ts — WebSocket client simulation

Priority for New Tests

When adding tests, prioritize by impact:

  1. Shared types/utils — contract between packages, highest ROI
  2. State machine transitions — core correctness
  3. Parser logic — data transformation accuracy
  4. Protocol handling — client-server contract
  5. Android state/network — cross-platform parity
  6. Renderers — snapshot tests if visual regressions matter

Apple Dot build isolation

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.