Skip to content

test: integration test suite (fake-satellite protocol + dashboard) on CI - #139

Merged
emir-hasanbegovic merged 6 commits into
mainfrom
feat/integration-tests
Jul 9, 2026
Merged

test: integration test suite (fake-satellite protocol + dashboard) on CI#139
emir-hasanbegovic merged 6 commits into
mainfrom
feat/integration-tests

Conversation

@emir-hasanbegovic

@emir-hasanbegovic emir-hasanbegovic commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

What this adds

The repo had ~1475 unit tests and 159 native tests but nothing exercising the integrated app on CI. This adds a real integration layer.

Fake-satellite protocol suite

An in-process FakeSatellite implements the protocol-1 contract (satellite/docs/contract.md) over real TLS + UDP, so tests drive the production connection stack end to end: SatelliteConnectionManager, SatelliteHttpClient (TOFU cert pinning), SessionCrypto (HMAC proof + HKDF session key), and the native ChaCha20-Poly1305 UDP path. No mocks of the code under test. Each FakeSatellite mints its own self-signed cert at runtime via okhttp-tls (no keys committed to the repo), so two instances present different certs and TOFU rejection is exercised for real.

SatelliteProtocolTest (8): pair-to-Live with hmacProof validation and the declarative session PUT; wrong-PIN never stores a key; encrypted heartbeats decrypt with the derived session key and acks keep the session alive; TOFU rejects a different cert on the same identity; terminal 401 drops the key and stops retrying; an epoch-mismatched heartbeat ack triggers the reconcile GET; forget self-unpairs; a kick close-notify takes the session down.

DashboardIntegrationTest (2): a live session is reflected in MainActivity's own views; the empty state shows the add prompt.

Isolation

Runs under Android Test Orchestrator (execution = "ANDROIDX_TEST_ORCHESTRATOR" + clearPackageData): each test gets a fresh process, so one test's session, retry jobs, and native sockets can't leak into the next. This was necessary because the suite drives real process-wide singletons; without it, later tests flaked deterministically. Verified green across repeated back-to-back runs locally.

CI + local parity

  • A new instrumented job runs the integration package on the same emulator path the screenshots workflow uses (reactivecircus/android-emulator-runner, google_apis api 35, KVM), scoped to com.tinkernorth.dish.integration. (A GMD aosp-atd device was tried first but hung on CI with this app's GameActivity; the GMD device is kept for local --gmd runs.)
  • scripts/ci_local.sh runs every CI gate locally in the same order (clang-format, metadata lint, ktlint+detekt, lint, unit tests, native C++ tests, assemble, integration on an attached device or --gmd), so local green == CI green.

Formatting

Confirmed: ktlint + detekt already lint all source sets including test and androidTest, so unit tests and these new integration tests are auto-formatted like the rest of the code. No formatting gap to close.

🤖 Generated with Claude Code

Adds instrumented integration tests that drive the real connection stack
(SatelliteConnectionManager, SatelliteHttpClient with TOFU pinning,
SessionCrypto, the native UDP path) against an in-process FakeSatellite
implementing the protocol-1 contract: PIN pairing with hmacProof
validation, the declarative session PUT/GET, HTTP keep-alive, self-unpair,
catalog/capabilities, and the encrypted ChaCha20-Poly1305 UDP data plane
with heartbeat acks and session-close notify. Each FakeSatellite mints its
own self-signed cert at runtime via okhttp-tls (no keys committed), so two
instances present different certs, exercising cert pinning for real: pin
one, reject the other on the same identity.

SatelliteProtocolTest covers pair-to-Live, wrong-PIN, heartbeat decrypt,
TOFU mismatch rejection, terminal 401 key-drop, epoch-driven reconcile,
self-unpair, and kick. DashboardIntegrationTest asserts a live session
reaches MainActivity's own views. Tests reach the app's real singletons
through MainActivity injection fields (no production seams), matching the
screenshot harness pattern.

Run under Android Test Orchestrator (execution ANDROIDX_TEST_ORCHESTRATOR
+ clearPackageData): each test gets a fresh process, so one test's
session, retry jobs, and sockets can't leak into the next. A pixel6Api34
managed device is added for local runs.
Adds an `instrumented` job that runs the integration package (fake-
satellite protocol + dashboard) on the same reactivecircus emulator path
the screenshots workflow uses (google_apis api 35, KVM), a known-good CI
image for this app's GameActivity. Scoped to com.tinkernorth.dish.integration:
the screenshots capture harness has its own per-locale workflow, and the
ad-hoc launch tests predate the guided-setup redirect.
scripts/ci_local.sh runs every Android CI gate in the same order against
the local tree (clang-format, metadata lint, ktlint+detekt, lint, unit
tests, native C++ tests, assemble, and the integration suite when a device
or --gmd is available), so a green run locally means a green run in CI.
Requires JAVA_HOME; finds adb via the SDK when it is off PATH.
@emir-hasanbegovic
emir-hasanbegovic force-pushed the feat/integration-tests branch from bff9d1e to e284abe Compare July 9, 2026 00:19
Grows the suite from 10 to 18 tests, all on the emulator + in-process
fake, and migrates singleton access to a reflection helper (drops the
per-test MainActivity launch for the protocol/catalog/input suites).

New protocol flows:
- Path B reverse pairing: client shows a PIN, operator approves, the
  status poll hands the key back and the session opens.
- Session-close notify split: kicked re-enters the bounded backoff (a
  reconnect PUT follows), unpaired is terminal (drops the key).
- Zero-controller session (a user in menus) PUTs an empty controllers array.

Catalog (new CatalogIntegrationTest): GET /api/catalog returns the type
list and publishes host features; a second fetch revalidates with the
ETag and the 304 path serves the cache (both fetches hit the fake).

Input data plane (new InputWireTest): bind the virtual slot, reach Live,
and stream each message type the overlays send. The encrypted UDP packets
must decrypt at the fake: INPUT (0x0001), MOTION (0x000A), BATTERY
(0x000B), TOUCHPAD (0x000C). Proves the native encode + ChaCha20-Poly1305
send path against a session key from the real handshake.

The fake now honours HTTP catalog ETag/304 and the Path B pair/status
poll, and mints a fresh cert per instance (unchanged).
…ycle controller

The input-wire tests registered the virtual slot through the
lifecycle-scoped SlotTopologyController, which does not run on a headless
CI emulator (no foreground window), so the slot never registered and the
three stream tests failed on CI while passing locally. Declare the slot
straight on the live connection with applyDesired instead: it drives the
same declare -> syncSlot -> controller PUT -> registered path without the
lifecycle dependency.
@emir-hasanbegovic
emir-hasanbegovic merged commit 4641549 into main Jul 9, 2026
9 checks passed
@emir-hasanbegovic
emir-hasanbegovic deleted the feat/integration-tests branch July 9, 2026 01:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant