Skip to content

Testing and quality checks

Current enforcement

Checks are invoked locally by a developer or agent; the code repository has no checked-in application CI pipeline or configured commit/push hooks yet. The separate documentation export has GitLab Pages CI, which does not test the Android app or API. Instructions are workflow requirements, not automated commit enforcement.

Hooks, automated commit gates, and application CI/CD are explicitly deferred for now. Do not install or enable them as a side effect of this runbook.

Android local regression tests use JUnit 4 (junit:junit in the app's Gradle dependencies). They cover clock timing, themes, effects, layouts, sound synthesis, preview behavior, and display policy. These execute on the build machine, not the phone. Backend tests use xUnit and in-process WebApplicationFactory integration tests. Dependency manifests are the source of truth for versions; test counts grow and should not be treated as fixed.

The Windows build/install helper currently runs testDebugUnitTest, lintDebug, and assembleDebug, but not Spotless. Run formatting/checks separately; invoking this helper does not satisfy the formatting gate.

When to run checks

All created/edited text files must be UTF-8 without a BOM, end with a newline, and follow .editorconfig line endings (currently LF) and whitespace rules. Verify these properties in touched files; git diff --check alone does not detect every BOM/encoding issue. Do not normalize the rest of the repository as incidental work.

Before committing or handing off a change:

  1. Inspect status and preserve unrelated edits. Add a deterministic regression test for changed behavior or document why it is not feasible.
  2. Run focused tests while iterating, then all checks for each affected surface.
  3. Run its formatter on changed files, review the diff, and run the format check. Do not turn a focused change into repository-wide restyling.
  4. For shared contracts, run both Android and backend checks. For lifecycle, rendering, TLS, audio, or OS compatibility, also exercise physical devices.
  5. Run git diff --check. Report commands, results, and checks that could not run. Existing report files are not evidence of a fresh run.

Do not weaken lint, analyzer, test, or formatting configuration to make a check pass. Distinguish pre-existing failures from regressions and report both. Missing SDKs, Docker, devices, or credentials must be reported, not hidden.

Android changes

Dashboard PoC regression and acceptance status

The current test sources include local regressions for sequencer lap counting, expired-card selection, and clearing the cache after asset 401/403 responses. Backend RSS tests cover redirect-disabled handler configuration and 3xx responses as source failures. Presenter instrumented tests cover adopting a feed at a lap boundary, canceling a held manifest request after a mode change, removing a visible card at expiry, and discarding a decoded image callback after bulletin dismissal. The test server binds to device loopback, uses a synthetic token, restores mode/connection preferences, and clears only its own random-origin cache after stopping its workers.

The 1 October 2026 baseline run passed 830 Android JVM tests and 18 instrumented tests on XT1058/API 22. It also passed Spotless, lint (zero errors and seven existing warnings), debug APK/test APK builds, 846 backend tests, and .NET format verification. That baseline predates the acceptance classes below.

Fresh validation on 1 October 2026 passed all 31 instrumented tests on the XT1058 (USB serial T01130J4O9, Android 5.1/API 22), with zero failures or skips. The 13 new acceptance cases comprise six lifecycle/background tests, six mode/UI tests and one bitmap test. Android JVM tests passed 831/831; backend tests passed 846/846. Spotless, .NET format verification, builds, Android lint (zero errors/seven existing warnings), deployment-helper tests and Compose validation passed.

Device testing exposed and fixed a transient split-flap layout crash at 17×15 pixels, delayed Showcase ownership, and an excessive idle bitmap pool. The memory result uses opaque PNGs, matching backend rendering; alpha PNGs may decode as ARGB_8888 even when RGB_565 is requested.

Dashboard PoC Step 3: API-22 acceptance methodology

The focused acceptance suite uses three instrumented classes and a shared fixture: six lifecycle/cache/authentication/background cases in DashboardLifecycleAcceptanceTest, six schedule/mode/native-UI cases in DashboardModesAcceptanceTest, and one steady-state image allocation case in DashboardMemoryAcceptanceTest. DashboardPresenterInstrumentedTest retains the earlier presenter regressions. All of these launch the actual Activity on the phone and use a synthetic manifest server bound to device loopback. The fixture discards request headers, temporarily replaces and then restores connection, mode and theme preferences, and clears only its random-origin cache after the Activity's workers have stopped. It does not clear app data or erase the cache belonging to the configured origin. A fatal process crash can prevent teardown: restore the original connection/mode and reprovision the dev token before subsequent manual checks; do not clear all app data.

Schedule and expiry checks replace only the presenter's wall clock and invoke its normal minute callback with explicit test instants. They do not change the phone or host operating-system clock. The offline regression drops a loopback manifest connection and then restores the fixture server; that verifies the client's failure and recovery paths, not loss of the phone's Wi-Fi connection. The allocation test samples the visible bitmap and reuse pool after cards are shown, checking RGB_565 format, the two-allocation bound, and reuse across rotation. It is a steady-state bound check, not a peak-memory profile or a multi-hour soak.

The Home-key case backgrounds the live Activity and checks that clock motion, card display, and dashboard polling stop, then resumes the same app and checks that the cached card returns. This is distinct from force-stopping the app and relaunching in Just Clock mode; that physical process-restart check was pending at Step 3; it subsequently passed during Step 6 below.

Build both APKs from the WSL android/ directory with ./gradlew assembleDebug assembleDebugAndroidTest. For the XT1058/API 22, install and run the complete instrumented suite from Windows PowerShell using Windows adb.exe. The Linux adb daemon and WSL Gradle's connectedDebugAndroidTest can report no devices while Windows ADB sees the USB phone. Use the serial from & $adbPath devices -l, and adjust the UNC root if the checkout moves:

$adbPath = 'D:\Android\Sdk\platform-tools\adb.exe'
$checkout = '\\wsl.localhost\Ubuntu-26.04\home\justin\repos\MotoXDashboard'
$deviceSerial = 'T01130J4O9'
& $adbPath -s $deviceSerial install -r "$checkout\android\app\build\outputs\apk\debug\app-debug.apk"
& $adbPath -s $deviceSerial install -r "$checkout\android\app\build\outputs\apk\androidTest\debug\app-debug-androidTest.apk"
& $adbPath -s $deviceSerial shell am instrument -w `
  com.masojus.motoxdashboard.test/androidx.test.runner.AndroidJUnitRunner

To run only the acceptance classes, use the same installed APKs and invoke:

foreach ($testClass in @(
  'com.masojus.motoxdashboard.dashboard.DashboardLifecycleAcceptanceTest',
  'com.masojus.motoxdashboard.dashboard.DashboardModesAcceptanceTest',
  'com.masojus.motoxdashboard.dashboard.DashboardMemoryAcceptanceTest'
)) {
  & $adbPath -s $deviceSerial shell am instrument -w -e class $testClass `
    com.masojus.motoxdashboard.test/androidx.test.runner.AndroidJUnitRunner
}

For one focused class, run this command with $testClass set to its fully-qualified class name:

$testClass = 'com.masojus.motoxdashboard.dashboard.DashboardLifecycleAcceptanceTest'
& $adbPath -s $deviceSerial shell am instrument -w -e class $testClass `
  com.masojus.motoxdashboard.test/androidx.test.runner.AndroidJUnitRunner

To rerun the earlier presenter cases, set $testClass to com.masojus.motoxdashboard.dashboard.DashboardPresenterInstrumentedTest. In a native Windows clone, use gradlew.bat and native APK paths. Do not run Windows Gradle against a WSL UNC checkout; see the README build guidance.

The automated API-22 checks above are complete. Manual process-restart and physical connectivity exercises were initially pending, and subsequently passed in Step 6 below; the broader appliance gate is not closed. The local API separately returned anonymous HTTP 401, authenticated HTTP 200 and four real provider cards (Bogotá weather, Yankees, NYT and Guardian). No production deployment was performed during Step 3. Moto X Play/Android 10, modern-phone coverage, Bluetooth audibility and a multi-hour bedside soak remain later checks. A device pass is not implied by JVM results or APK installation.

The fresh Step-4 rerun on 1 October passed all 831 JVM and 846 backend tests and all 31 instrumented XT1058 cases (79.657 seconds), with zero failures/skips. Release build/analyzers and .NET format verification passed with zero warnings or errors. Android Spotless, lint (zero errors/seven existing warnings), debug and test-APK assembly passed with --rerun-tasks. The debug APK SHA-256 is 8804e70c111891e46baf2079d7ef1faaa986481bf3189a191a5570737283ea2a. Final script/docs/container results and the exact frozen image are recorded in the release candidate receipt.

Step 6 production/device checks (1 October 2026)

The exact rehearsed image was deployed without rebuilding. Public TLS health, manifest 401/200/304 and all returned PNG assets' 401/200/hash/304 checks passed. Weather, NYT and Guardian were returned; MLB receives HTTP 406 from the edge reached by OVH, so four-source production acceptance remains open.

On XT1058, production HTTPS activated real cards without ADB reverse. Just Clock persisted across force-stop/relaunch with no polling during observation; Highlights fetched/displayed cards and returned to Just Clock. Bulletins mode persisted across restart, outside a scheduled boundary. Using Android's Wi-Fi settings (the shell svc command did not change connectivity on this device), Wi-Fi was disabled and the active default network was confirmed absent. Refresh failed with UnknownHostException, while cached cards remained visible. Wi-Fi was restored, and resume subsequently obtained a successful NOT_MODIFIED/304 refresh. Mobile-data settings were unchanged and Wi-Fi was restored to enabled. The device was left in Dashboard mode on the production origin.

No OS-clock change, token revocation, expiry wait, multi-hour soak or additional phone coverage was performed during this rollout. Earlier deterministic tests cover schedule/expiry/auth failure; they are not a claim of new physical checks. See deployment and rollback.

The subsequent card-transition regression holds the XT1058's image-decoding worker while the next card is due. It checks that the prior image is released, an opaque black layer still covers the clock, and the next card appears when decoding resumes. The full affected Android gates passed after this client-only change: Spotless, JVM tests, lint and APK assemblies. The complete physical instrumented suite passed 32/32 tests, including the new regression. The API image deployed on OVH remains unchanged.

Moto X Play / Android 10 acceptance (2 October 2026)

The latest debug app was installed on the XT1563 running LineageOS 17.1 / Android 10 (API 29). The complete instrumented suite passed 33/33 cases, including the physical frame test, Dashboard lifecycle/mode/card tests, transport/TLS tests, and the no-clock-flash transition regression. The new frame case temporarily reveals system bars: the panel measured 1920×1080, the root 1776×1080, and the corrected frame x=144..1776, centered at x=960. After returning to immersive mode, root and frame both measured 1920×1080.

The first Play run failed because Android 10 rejected the synthetic local HTTP server (Cleartext HTTP traffic to 127.0.0.1 not permitted). A debug-only network-security resource now permits that exact loopback host; the release manifest has no exception. Repeat on-device testing after any change to this debug configuration, because API-22 acceptance alone cannot detect Android 10's cleartext policy.

The Play was also separately registered as xt1563-bedroom with the existing production API and provisioned with its own raw token in app-private storage. With no ADB reverse mapping it fetched and rotated the live Bogotá weather, NYT, and Guardian cards over HTTPS. The public manifest returned 200 then 304, and the first asset's SHA-256 matched its manifest value. Just Clock and Dashboard selection persisted; with Wi-Fi disabled and no active default network, a cached card stayed visible while refresh failed with UnknownHostException. Wi-Fi was restored, and a new process received 304 while the cards continued rotating. This is a short physical check, not the multi-hour soak. The MLB 406 provider issue remains outstanding.

The local Android quality commands, run with the configured JDK and Android SDK, are:

./gradlew spotlessApply
./gradlew spotlessCheck testDebugUnitTest lintDebug assembleDebug assembleDebugAndroidTest --rerun-tasks

In a native Windows clone, use .\gradlew.bat instead of ./gradlew. Do not run Windows Gradle against a WSL UNC checkout; see the README build guidance.

Spotless owns Java formatting through Palantir Java Format, import order, unused-import removal, wildcard-import rejection, and selected build/resource file whitespace. Android Lint checks Android-specific correctness, including SDK compatibility. Review spotlessApply's diff and exclude unrelated changes from the task; its target set includes more than a single touched Java file.

JUnit regression tests cannot establish that actual audio, Views, Android lifecycle, navigation insets, Bluetooth, or certificate chains work on a phone. Use focused device/instrumented checks when those behaviors change. The existing local suite is not an automated end-to-end device suite. Installation and production operations still require user authorization.

Backend changes

Run from the repository root:

dotnet restore backend/MotoXDashboard.slnx
dotnet build backend/MotoXDashboard.slnx -c Release --no-restore
dotnet format backend/MotoXDashboard.slnx --verify-no-changes --no-restore
dotnet test backend/MotoXDashboard.slnx -c Release --no-build

For local corrections, use dotnet format backend/MotoXDashboard.slnx --no-restore --include <changed C# paths> with explicit file paths, then review the diff and rerun verification. Do not silently apply analyzer-driven rewrites to unrelated files. .editorconfig, project settings, and SDK analyzers own C# style; recommended analysis and warnings-as-errors are already configured. dotnet format verification is an instructed local gate, not yet a CI-enforced gate; establish the existing baseline before claiming it clean.

Pure policy tests control time with TimeProvider and avoid live services. Integration tests cover routing, serialization, validation, dependency wiring, authorization, and error mapping through WebApplicationFactory.

Follow the Minigames backend conventions: Arrange/Act/Assert, Method_Scenario_ExpectedResult names, [Theory]/[InlineData] for parameterized cases, and mocks/lightweight fakes at process boundaries. Minigames uses Moq; this repository does not currently reference it, so add it only when a real test needs it rather than as scaffolding. Control time/randomness, avoid sleeps, and prioritize meaningful assertions over a coverage percentage.

For a local coverage/report run, after building:

dotnet test backend/MotoXDashboard.slnx -c Release --no-build \
  --collect:"XPlat Code Coverage" --results-directory ./artifacts/test-results/backend \
  --logger "trx;LogFileName=test-results.trx"

The existing test project includes coverlet.collector. This command requests TRX test results and coverage output; it does not publish them. Keep generated reports untracked. Minigames uses coverlet.runsettings and separates Category=Flaky tests; neither configuration exists here. Do not copy its integration/composition-root exclusions or make failing tests informational without a project-specific rationale. All current tests should remain blocking.

Contracts, providers, and dashboard regression tests

Provider adapters normalize external payloads into internal category models; selection/rendering and the Android manifest contract must not depend on a specific provider's payload. Adding or replacing a source gets adapter fixture tests plus common normalization/selection tests. Many configured feeds do not imply an unbounded manifest: test selection and bounded batches separately.

Use sanitized fixtures and fake HTTP responses for routine tests. Cover null fields, malformed payloads, timezone boundaries, postponed games, expiry, timeouts, 401/403/429/5xx, bounded retries, and independent source failure. Do not require a provider key, current score, internet access, or the VPS for ordinary regression tests. Live source smoke tests are separate, explicitly invoked checks, not substitutes for deterministic tests.

The locally prepared API-Sports Baseball adapter has fake-response tests for date selection, free-plan entitlement errors, upstream failures, game-state normalization, cache reuse and quota behavior. They require no live key. The earlier full backend run passed 869 tests after this adapter was added; this is a local test result, not production-source acceptance. When activating it, repeat the full backend/container gates with the exact intended source configuration. Then run a bounded live smoke from OVH, verify the Yankees card and asset over authenticated public HTTPS, and check the provider request counter. Do not put a raw key or provider response containing credentials in tests, fixture files, logs, the docs export or a release receipt. Keep live provider checks separate from the deterministic test suite.

BALLDONTLIE MLB is a second fixture-tested adapter. Its tests cover raw-key authorization, the nine-day team-filtered request, lifecycle/score mapping, scheduled-score suppression, empty schedules, pagination rejection and HTTP failure classes. The combined backend suite passed 886 tests locally before the evaluation release was frozen. The five-card release rehearsal requires private key files through MOTO_API_SPORTS_KEY_FILE and MOTO_BALLDONTLIE_KEY_FILE; the helper verifies both rendered sports cards and assets from the exact saved image. Its faster repeated refresh and network fault checks apply to weather/news only, because baseball refreshes retain their free-tier floors. A short rehearsal is not evidence of live-game freshness, provider failover or a bedside soak.

The five-card profile exposed an Android-only limit: the parser/model clamped maxBulletinSeconds to 60 despite the server's 75. The client now accepts 15–240 seconds, enough for eight maximum-duration cards, while retaining the 60-second default for omitted values. Parser/model/sequencer regression tests cover 75 seconds and the fifth card. The follow-up APK passed Spotless, JVM tests, lint and assembly, then 33 instrumented tests on the XT1058 through Windows adb.exe. WSL Gradle's connected-device task reported no devices; it was not the successful on-device test route.

The 2 October five-card evaluation commit dc7568c1e72b passed 886 backend tests, Release build/analyzers, .NET format verification, Compose validation, deployment/release-helper script tests, and touched-script formatting and ShellCheck. The exact saved image was rehearsed against real provider cards; all five PNGs were verified, weather/news publication and network recovery were exercised, and the kernel memory peak was 67,362,816 bytes under the 256 MiB cap with no OOM. Public HTTPS returned five cards, anonymous 401 and authenticated 200/304; the XT1058 displayed all five in Dashboard mode without ADB reverse. See the deployment receipt.

Share small versioned contract fixtures across server/client tests. Cover additive fields, unsupported schemas, stable ETags/304, checksums, size limits, atomic cache activation, failed downloads, and expired content. Mode tests must prove Just Clock never polls, persistent modes survive app restart, one-shot highlights restore the prior mode, and pause cancels owned work.

See the Dashboard PoC plan for phase-specific gates.

Documentation and deployment changes

Authenticated local development loop

Use the WSL checkout for the backend and Gradle; use Windows ADB for USB. Do not depend on the earlier ephemeral ~/.cache/motox/run-api.sh helper. From the repo root, choose a new private token path outside Git, then run:

umask 077
dev_directory=$(mktemp -d)
credential=$(bash scripts/new-device-token.sh dev-xt1058 bedroom "$dev_directory/device.token")
ASPNETCORE_ENVIRONMENT=Development \
ASPNETCORE_URLS=http://127.0.0.1:8089 \
Dashboard__SyntheticFeed=false \
Dashboard__PublicOrigin=http://127.0.0.1:8089 \
Dashboard__DeviceCredentials="$credential" \
dotnet run --no-launch-profile --project backend/src/MotoXDashboard.Api

In another terminal, install the debug APK and use adb.exe reverse tcp:8089 tcp:8089. Provision the same token file with ADB=/mnt/d/Android/Sdk/platform-tools/adb.exe bash scripts/provision-device-token.sh TOKEN_FILE, then set the phone's connection origin to http://127.0.0.1:8089. That cleartext origin works only in debug builds and the Development backend. This override uses real sources; checked-in Development settings default to synthetic data. Dashboard__SyntheticFeed=true is an explicit Development-only option. A token imported after an auth failure needs a fresh app process (or the connection dialog's explicit token-save reset), not just resume. Keep raw tokens out of arguments/logs/URLs. Remove the reverse mapping after testing and deliberately restore the intended phone connection; do not clear unrelated app data. Delete only your identified private dev files.

Saved-image whole-application rehearsal

Run the script regressions and shell checks before preparing a clean explicit commit. Choose new private artifact/report directories outside the checkout:

bash scripts/test-deployment-helpers.sh
bash scripts/test-prepare-release.sh
bash scripts/test-rehearse-release.sh
shellcheck scripts/*.sh
shfmt -d scripts/*.sh
docker compose -f compose.prod.yaml config --quiet
bash scripts/prepare-release.sh FULL_COMMIT /absolute/private/releases/new-release
MOTO_API_SPORTS_KEY_FILE=/absolute/private/api-sports.key \
MOTO_BALLDONTLIE_KEY_FILE=/absolute/private/balldontlie.key \
bash scripts/rehearse-release.sh /absolute/private/releases/new-release /absolute/private/rehearsals/new-report

The release smoke permits an empty cold-start manifest. The separate rehearsal does not: it waits for Open-Meteo, both Yankees sources, NYT and Guardian cards, verifies every observed PNG's length/hash/header/dimensions, and requires repeated successful weather/news refreshes and publication. It runs the saved image with production resource and hardening limits, but accelerates only weather/news to one minute. Both baseball sources retain their free-plan refresh floors. The private key files are required only for the evaluation rehearsal and never enter Git or its report. It records cgroup-v2 current/peak memory, observed swap, OOM events, CPU quota and statistics, PID limits/counts, and the portable saved-image digest separately from Docker Desktop's daemon ID. Missing resource evidence is a failure.

A temporary isolated Docker network is disconnected/reconnected to exercise provider failure and recovery without changing production. Host access to the container may also disappear during the fault; the receipt records that rather than claiming API reachability. Last-confirmed current assets are checked after reconnect; older-snapshot retention is also covered by deterministic backend tests, not inferred from sparsely polled revisions. Reports remain outside Git and do not contain the raw throwaway bearer token. The helper removes only its own temporary container/network/private headers; saved artifacts and reports are retained. No Skia-only spike, empty-feed smoke, or historical report replaces this whole-application gate.

For Markdown, navigation, or site assets, export and run the strict MkDocs build in the documentation runbook. Code formatting and app tests are unnecessary for a documentation-only change unless it also changes executable examples or configuration requiring another check.

For container/Compose changes, validate docker compose -f compose.prod.yaml config --quiet and rehearse the image locally. Test deployment-script argument handling/read-only plans without mutating production. Actual rollout requires authorization and the OVH runbook.

For release-helper changes, format affected shell files with shfmt -w, then verify with shfmt -d and shellcheck. Run both bash scripts/test-deployment-helpers.sh: it uses fake SSH/Docker/curl commands and tests explicit-commit requirements, path validation, read-only planning, image revision rejection, readiness recording, and no-build/no-pull installation. Also run bash scripts/test-rehearse-release.sh for saved-artifact preflight and asset-validation rejections, and bash scripts/test-prepare-release.sh for bounded conditional-request publication races and failures. These do not prove privileged bootstrap or Nginx behavior; those require reviewed VPS execution with the script's backup, syntax check, public probes, and rollback.

Minigames reference and planned GitHub Actions

The reference inspected is the local D:\git\minigames checkout, especially .githooks/pre-commit, .github/workflows/reusable-backend-test.yml, .github/workflows/ci.yml, MiniGamesBackEnd.Tests/MiniGamesBackEnd.Tests.csproj, and MiniGamesBackEnd.Tests/coverlet.runsettings. Actual manifests/workflows take precedence over stale prose (one reference instruction still says test projects are absent, although the test project exists).

Minigames checks staged C# files with dotnet format --include ... --verify-no-changes in its hook, but verifies the entire solution in CI. Use that distinction when adding hooks here; no hook is installed by this documentation change. Local fixes can be changed-file scoped; full-branch verification must not be replaced by checking only the latest commit.

First establish a shared local validation entry point, then have application CI run the same non-mutating gates using GitHub Actions reusable workflows, following Minigames' PR-to-main and push-to-main pattern:

  • Android: spotlessCheck, JUnit tests, lint, and debug APK assembly.
  • Backend: restore, format verification, build/analyzers, and xUnit tests.
  • Docs: export and strict MkDocs build.
  • Containers: configuration validation and image build when affected.

CI checks formatting; it must not run formatters in apply mode or commit fixes. Use a path-detection job with independent Android/backend/docs jobs and ensure shared contracts, SDK manifests, and shared quality configuration trigger all affected checks. Keep required checks predictable when a surface is skipped. Cancel superseded validation runs, pin third-party actions to full commit SHAs, grant minimum job permissions, and do not expose secrets to untrusted PR code.

Follow Minigames' restore → Release build → whole-solution format check → test sequence for the backend. Publish TRX test reports and Cobertura coverage artifacts, plus Android test/lint reports and build artifacts for review. Coverage trends/PR summaries are a later enhancement; do not copy its 50% backend threshold, gist identifiers, Application Insights setup, or test exclusions into this project. Physical API-22 and Android-10 acceptance remains a separate gate until a device test setup exists. Provider smoke tests must be separate from the deterministic default pipeline. Production deployment should be a separately authorized/manual protected job, with runtime secrets, health checks, and rollback—not automatic on every push. Reuse the safety pattern, not Minigames' Azure/ACR/Terraform deployment details; this backend is hosted on OVH and the Android APK has its own delivery path. This section is a roadmap, not a claim that these jobs exist today.