Header Logo

Building CI Checks for a React Native AI Avatar App: Smoke Tests, Mock Streams, and Fail Fast

Building CI Checks for a React Native AI Avatar App: Smoke Tests, Mock Streams, and Fail Fast

CI for React Native AI avatars: smoke tests, mock stream events, contract checks, and one thin integration test.

Introduction


When a React Native app depends on a realtime avatar pipeline, the hard part is rarely the UI component itself. The hard part is everything around it: authentication, session creation, network setup, streaming state, reconnection, and the failure modes that only show up in CI when there is no camera, no mic, no GPU, and often no reliable WebRTC path.


This post is about building CI checks that catch those failures early without turning your test suite into a flaky integration lab. By the end, you should have a practical pattern for:


  • smoke-testing the app boot path without a real avatar backend,

  • mocking streaming and session state so your UI logic is deterministic,

  • failing fast on contract and auth regressions before they hit staging.


The examples assume a React Native client that talks to a realtime avatar backend and renders a live video surface. The same approach works whether your avatar is embedded in a native screen, a webview, or a hybrid chat experience.


Start with the smallest useful smoke test


For CI, a smoke test should answer one question: can the app start, reach the avatar integration boundary, and render the expected state transitions without crashing?


Do not try to prove the whole realtime stack in one test. Instead, pick one thin path:


  1. App launches.

  2. Avatar session service initializes.

  3. A session request is made with the expected auth header and payload.

  4. The UI transitions from “connecting” to either “connected” or a controlled error state.


If that path is broken, the release is broken. Everything else belongs in a deeper integration environment.


What to isolate in React Native


In practice, the most fragile boundary is not the video renderer; it is the session orchestration layer. Keep the following behind an interface so tests can replace them:


  • session creation API calls,

  • signaling or stream event handlers,

  • token refresh or API key provisioning,

  • device permission checks,

  • connection lifecycle callbacks.


That interface should return coarse-grained states that the UI can render directly: idle, connecting, connected, reconnecting, error. If you model the stream as low-level WebRTC primitives everywhere, your tests become awkward and your component tree becomes too tightly coupled to transport details.


Mock the stream, not just the HTTP call


Many teams stub the “create session” endpoint and call it done. That is necessary, but not sufficient. A realtime avatar app has a second source of truth: the live stream itself. The stream can fail after a successful API response, or recover after a transient disconnect, or receive media before the UI is ready.


Your mocks should therefore simulate events, not just responses. For example, in a test harness you might expose an event emitter with a few canonical events:


type AvatarStreamEvent =
| { type: 'error'; message: string };
type AvatarStreamEvent =
| { type: 'error'; message: string };
type AvatarStreamEvent =
| { type: 'error'; message: string };


The UI does not need to know whether those events came from WebRTC, a websocket, or a local fake. It only needs to react correctly.


Example: a deterministic mock session controller


A small fake controller is often enough for both Jest and end-to-end tests. The important part is that it lets the test drive state transitions explicitly.


export function createMockAvatarSession() {

}
export function createMockAvatarSession() {

}
export function createMockAvatarSession() {

}


In a component test, you mount the screen with the mock controller, assert that the spinner appears, then emit connected and verify the placeholder video state disappears. That gives you a fast, deterministic test that exercises your UI logic without needing a real network path.


Fail fast on contract drift and auth mistakes


Realtime systems often fail in boring ways: the request shape changed, the wrong environment variable is loaded, an API key is missing, or the backend now requires a field your client did not send. These are exactly the kinds of regressions CI should catch immediately.


Two checks are worth adding early:


  • Request contract checks. Assert that the session creation payload includes the fields your backend requires and that your client sends the expected authorization header in non-development environments.

  • Configuration checks. Verify that API base URLs, feature flags, and environment-specific keys resolve before the app reaches the avatar screen.


A lightweight example using a mocked fetch layer:


global.fetch = vi.fn(async (url, init) => {

});
global.fetch = vi.fn(async (url, init) => {

});
global.fetch = vi.fn(async (url, init) => {

});


This does not prove the backend is healthy. It does prove your app is making the request you think it is making. In CI, that distinction matters.


Use one integration test that crosses the boundary


Pure mocks are fast, but they can hide integration issues. Add one thin, opt-in job that hits a real test environment or a controlled staging endpoint and verifies a complete session lifecycle. Keep it narrow:


  • create a session,

  • wait for the stream to connect,

  • confirm a visible state change in the app,

  • tear down cleanly.


Do not run this on every PR if it is slow or externally dependent. Run it on merge to main, on a schedule, or in a separate pipeline. The point is to catch incompatibilities that a mock cannot see, while keeping the main CI path fast.


How to structure the pipeline so failures are actionable


A good CI stack for this kind of app usually has three layers:


  1. Unit tests for state machines, hooks, reducers, and request builders.

  2. Component smoke tests with the stream and session controller mocked.

  3. One thin integration check against a real environment.


The failure messages should tell you which layer broke. If a request payload test fails, that is a contract issue. If a mock stream test fails, that is usually a UI or state-management regression. If the integration check fails, it is likely an environment, auth, or upstream compatibility problem.


Also, keep the signal tight. Avoid screenshots as the primary assertion for streaming screens. Instead, assert on explicit UI state and event sequencing. Screenshots are fine as a last resort, but they are noisy when video surfaces are involved.


Where Protoface fits


This is where Protoface is useful as a boundary to test against, because the integration surface is explicit: a REST API for sessions and avatar management, plus developer-facing SDKs and plugins. For CI, the important part is that you can mock the client boundary locally while still validating the request contract against the real API shape in docs and staging.


If you are using the Python SDK for backend-driven workflows, or the LiveKit plugin for a voice agent that needs a synchronized face, keep those integration points behind adapters and test the adapters separately. That lets your React Native app stay focused on UI state, while the realtime avatar plumbing lives in a narrower surface area. The docs at docs.protoface.com are the right reference for the exact request fields and session semantics.


For example, a backend session creation call in Python should stay small and explicit:


from protoface import Client

print(session)
from protoface import Client

print(session)
from protoface import Client

print(session)


And if you are wiring a LiveKit agent, the integration should be just as narrow in your application code. The plugin should be the only place that knows how the avatar video face is attached to the agent, so your app can mock that boundary in tests rather than trying to stand up a full voice stack in CI.


Practical gotchas


A few issues show up repeatedly:


  • Test flakiness from timers. If your reconnect logic uses backoff, use fake timers in unit tests so assertions are deterministic.

  • Permission prompts. Treat camera/mic permission failures as their own state, not as a generic connection error.

  • Parallel CI jobs. If integration tests hit shared limits, isolate them or give them their own test keys and rate limits.

  • Environment leakage. Make sure test keys cannot accidentally ship in production builds, especially if you have any browser or iframe surface.


The broader rule is simple: keep transport, auth, and rendering separated enough that each can fail independently and produce a clear error.


Conclusion


For a React Native AI avatar app, CI should not attempt to recreate the entire realtime stack. It should prove that your app handles the important boundaries correctly: session creation, stream lifecycle, and controlled failure states. Mock the stream events, assert on request contracts, and keep one small integration check for end-to-end confidence.


If you need implementation details for the session API, SDK usage, or the agent/plugin integration points, start with docs.protoface.com. The goal is not more tests; it is tests that fail quickly, explain the problem, and prevent broken realtime behavior from reaching users.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.