Skip to content

[Fix] Stabilize media-dependent audio room integration tests - #1224

Merged
ipavlidakis merged 4 commits into
developfrom
iliaspavlidakis/fix-integration-tests-flakiness
Aug 7, 2026
Merged

[Fix] Stabilize media-dependent audio room integration tests#1224
ipavlidakis merged 4 commits into
developfrom
iliaspavlidakis/fix-integration-tests-flakiness

Conversation

@ipavlidakis

@ipavlidakis ipavlidakis commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🎯 Goal

Prevent audio-room integration tests from activating the simulator's real audio capture stack, which can cause intermittent TCC, MediaServices, and CoreSimulator failures during repeated runs.

The original CallSettings and production media behavior remain unchanged.

📝 Summary

  • Add internal support for injecting a native WebRTC audio device and its SDK-side controller.
  • Use a silent audio device and inactive audio-session policy in the affected permission tests.
  • Wait for WebRTC session cleanup and reset mocked permissions during teardown.
  • Document the injected and default production audio paths with DocC.

🛠 Implementation

PeerConnectionFactory can now receive an optional RTCAudioDevice and RTCAudioDeviceModuleControlling. The configured factory is forwarded through WebRTCCoordinatorFactory and WebRTCCoordinator.

When no factory or audio device is injected, the existing production path continues to create the standard WebRTC audio engine with the configured audio-processing module.

The affected integration tests inject:

  • A silent native RTCAudioDevice that does not start platform audio capture or playback.
  • The existing mocked SDK audio-device module.
  • An inactive audio-session policy that prevents AVAudioSession activation.

The tests retain their original audio and video CallSettings.

Integration-test teardown now waits until the call's WebRTC session identifier is cleared before dismantling clients and resets the permission helper to avoid leaking state between iterations.

🎨 Showcase

Not applicable. There are no UI changes.

🧪 Manual Testing Notes

Validated on an iPhone 17 Pro simulator running iOS 26.5:

  • Ran both affected tests for 10 iterations each, serially.
    • 20 tests passed.
    • 0 failures.
    • No Unable to initAndStart recording errors.
    • No CoreSimulator RPC timeouts.
  • xcodebuild ... build-for-testing passed.
  • swiftformat --lint --config .swiftformat . passed.
  • swiftlint lint --config .swiftlint.yml --strict passed.

A subsequent live-SFU run encountered a separate pre-join timeout while call.create made no progress. It occurred before WebRTC joining or audio activation and did not reproduce the media-service crash.

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change follows zero ⚠️ policy (required)
  • This change should receive manual QA
  • Changelog is updated with client-facing changes
  • New code is covered by unit tests
  • Comparison screenshots added for visual changes
  • Affected documentation updated (tutorial, CMS)

Summary by CodeRabbit

  • New Features

    • Added more flexible audio handling for WebRTC calls, including silent audio environments and configurable audio availability.
    • Improved call setup customization for different audio configurations.
  • Bug Fixes

    • Improved call reliability when microphone or camera access is unavailable.
    • Improved cleanup after ending a call, including audio-session teardown.
    • Fixed simulator compatibility for audio-session rendering state on newer Swift versions.
    • Improved call-ending and pinning behavior validation.

@ipavlidakis
ipavlidakis requested a review from a team as a code owner August 6, 2026 13:19
@ipavlidakis ipavlidakis self-assigned this Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e22ca87e-82af-4380-9d40-be4d0a75c2ab

📥 Commits

Reviewing files that changed from the base of the PR and between 85d0bc8 and 73ecbba.

📒 Files selected for processing (2)
  • StreamVideoTests/CallStateMachine/CallStateMachine/Stages/CallStateMachine_JoiningStageTests.swift
  • StreamVideoTests/Mock/MockCallController.swift

📝 Walkthrough

Walkthrough

The PR adds injectable audio-engine availability to WebRTC factory construction. It wires the selected factory through coordinators and state adaptation. Integration helpers use silent audio settings and simplified sequential assertions. Simulator audio-session handling uses an empty rendering mode fallback.

Changes

WebRTC audio injection and integration coverage

Layer / File(s) Summary
Peer connection factory audio override
Sources/StreamVideo/WebRTC/PeerConnectionFactory.swift
PeerConnectionFactory stores and propagates an optional audio-engine availability override.
Coordinator factory injection
Sources/StreamVideo/WebRTC/v2/WebRTCCoordinator.swift, Sources/StreamVideo/WebRTC/v2/WebRTCCoordinatorProviding.swift, Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift
Coordinators accept an optional factory. WebRTCStateAdapter uses the override before the existing availability logic.
Silent audio integration test environment
StreamVideoTests/IntegrationTests/Components/Helpers/*, StreamVideoTests/IntegrationTests/Call_IntegrationTests.swift
Integration helpers configure silent audio and inactive audio sessions. Call-ending and pinning tests use sequential joins and direct state assertions.
Simulator audio-session fallback
Sources/StreamVideo/Utils/AudioSession/RTCAudioStore/Components/AVAudioSessionObserver.swift, StreamVideoTests/Utils/AudioSession/RTCAudioStore/Components/AVAudioSessionObserver_Tests.swift
Simulator builds use an empty rendering mode. A test verifies the default snapshot value.
Test call-settings support
StreamVideoTests/Mock/MockCallController.swift, StreamVideoTests/CallStateMachine/CallStateMachine/Stages/CallStateMachine_JoiningStageTests.swift
MockCallController accepts initial call settings. Joining-stage tests reuse shared expected settings.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CallIntegrationTests
  participant StreamVideoHelper
  participant WebRTCCoordinatorFactory
  participant PeerConnectionFactory
  participant WebRTCStateAdapter
  CallIntegrationTests->>StreamVideoHelper: buildClient with silent audio environment
  StreamVideoHelper->>WebRTCCoordinatorFactory: provide optional PeerConnectionFactory
  WebRTCCoordinatorFactory->>PeerConnectionFactory: configure audio-engine override
  WebRTCCoordinatorFactory->>WebRTCStateAdapter: pass selected factory
  WebRTCStateAdapter->>PeerConnectionFactory: read audio-engine availability override
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: martinmitrevski

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: stabilizing media-dependent audio room integration tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch iliaspavlidakis/fix-integration-tests-flakiness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
StreamVideoTests/IntegrationTests/Components/Helpers/Call_IntegrationTests+Helpers.swift (1)

67-85: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Complete cleanup before propagating a session-cleanup failure.

If the assertion at Line 67 throws, permissions.dismantle() and client.dismantle() at Lines 84-85 do not run. The next test can inherit retained calls, audio state, or mocked permissions.

Capture the assertion failure, complete all cleanup, and then rethrow the captured error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@StreamVideoTests/IntegrationTests/Components/Helpers/Call_IntegrationTests`+Helpers.swift
around lines 67 - 85, Ensure the cleanup helper captures any error from the
sessionID assertion instead of exiting immediately, then always completes
registered-call, audio-device, permission, and client cleanup before rethrowing
the captured error. Update the surrounding async cleanup flow in
Call_IntegrationTests helpers while preserving successful execution behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/StreamVideo/WebRTC/PeerConnectionFactory.swift`:
- Around line 76-116: Update PeerConnectionFactory.build and its private init to
require a nonnil audioDeviceModuleSource whenever audioDevice is provided,
rejecting the mismatched combination before constructing the factory. Preserve
support for callers that provide neither device or provide matching audioDevice
and audioDeviceModuleSource values, and ensure the exposed audioDeviceModule
uses the supplied source.

---

Outside diff comments:
In
`@StreamVideoTests/IntegrationTests/Components/Helpers/Call_IntegrationTests`+Helpers.swift:
- Around line 67-85: Ensure the cleanup helper captures any error from the
sessionID assertion instead of exiting immediately, then always completes
registered-call, audio-device, permission, and client cleanup before rethrowing
the captured error. Update the surrounding async cleanup flow in
Call_IntegrationTests helpers while preserving successful execution behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 749cb736-7c3f-4286-b5af-0fb05fb7373b

📥 Commits

Reviewing files that changed from the base of the PR and between 5d4250f and 34f4d25.

📒 Files selected for processing (6)
  • Sources/StreamVideo/WebRTC/PeerConnectionFactory.swift
  • Sources/StreamVideo/WebRTC/v2/WebRTCCoordinator.swift
  • Sources/StreamVideo/WebRTC/v2/WebRTCCoordinatorProviding.swift
  • StreamVideoTests/IntegrationTests/Call_IntegrationTests.swift
  • StreamVideoTests/IntegrationTests/Components/Helpers/Call_IntegrationTests+Helpers.swift
  • StreamVideoTests/IntegrationTests/Components/Helpers/Call_IntegrationTests+StreamVideoHelper.swift

Comment thread Sources/StreamVideo/WebRTC/PeerConnectionFactory.swift Outdated
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
1 Warning
⚠️ Please be sure to complete the Contributor Checklist in the Pull Request description

Generated by 🚫 Danger

@martinmitrevski martinmitrevski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@StreamVideoTests/IntegrationTests/Call_IntegrationTests.swift`:
- Around line 385-395: Update the participant join flow in the integration test
to assert, after participantUserFlow.perform completes and before
creatorFlow.call.end(), that the participant client's activeCall is this call.
Keep the existing eventual assertion for activeCall becoming nil after the call
ends.

In
`@StreamVideoTests/Utils/AudioSession/RTCAudioStore/Components/AVAudioSessionObserver_Tests.swift`:
- Around line 25-27: Rename the test method test_snapshot_renderingModeIsEmpty
to test_snapshot_rendering_mode_is_empty_ to follow the repository’s naming
convention, without changing its assertions or behavior.
- Around line 25-27: Scope test_snapshot_renderingModeIsEmpty to simulator
builds so the empty-string assertion runs only where AVAudioSession.Snapshot
does not expose the device rendering mode. Use the project’s existing
conditional-compilation conventions, or add a separate device assertion for
supported iOS 17.2+ builds.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 10461037-c58a-4b9b-8468-dfa5e83bd4d6

📥 Commits

Reviewing files that changed from the base of the PR and between 34f4d25 and f6de4dd.

📒 Files selected for processing (7)
  • Sources/StreamVideo/Utils/AudioSession/RTCAudioStore/Components/AVAudioSessionObserver.swift
  • Sources/StreamVideo/WebRTC/PeerConnectionFactory.swift
  • Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift
  • StreamVideoTests/IntegrationTests/Call_IntegrationTests.swift
  • StreamVideoTests/IntegrationTests/Components/Helpers/Call_IntegrationTests+Helpers.swift
  • StreamVideoTests/IntegrationTests/Components/Helpers/Call_IntegrationTests+StreamVideoHelper.swift
  • StreamVideoTests/Utils/AudioSession/RTCAudioStore/Components/AVAudioSessionObserver_Tests.swift

Comment on lines +385 to +395
.perform { try await $0.call.join() }

try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
try await creatorFlow
.perform { try await $0.call.join() }
.subscribe(for: CustomVideoEvent.self)
.assertEventually { (event: CustomVideoEvent) in event.custom["state"] == "joined" }
.perform { try await $0.call.end() }
}
let participantFlow = try await participantUserFlow
.perform { try await $0.call.join() }

group.addTask {
try await participantUserFlow
.perform { try await $0.call.join() }
.perform { try await $0.call.sendCustomEvent(["state": "joined"]) }
.assertEventuallyInMainActor { $0.call.streamVideo.state.activeCall == nil }
}
_ = try await creatorFlow.call.end()

try await group.waitForAll()
}
try await participantFlow
.assertEventuallyInMainActor(timeout: 30) {
$0.call.streamVideo.state.activeCall == nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Establish participant membership before ending the call.

The assertion only checks the final activeCall state. The test can pass if the participant never becomes active before end() runs. Assert that the participant client has this call as its active call after joining and before Line 390.

Proposed fix
 let participantFlow = try await participantUserFlow
     .perform { try await $0.call.join() }
+    .assertEventuallyInMainActor {
+        $0.call.streamVideo.state.activeCall?.cId == $0.call.cId
+    }

 _ = try await creatorFlow.call.end()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.perform { try await $0.call.join() }
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
try await creatorFlow
.perform { try await $0.call.join() }
.subscribe(for: CustomVideoEvent.self)
.assertEventually { (event: CustomVideoEvent) in event.custom["state"] == "joined" }
.perform { try await $0.call.end() }
}
let participantFlow = try await participantUserFlow
.perform { try await $0.call.join() }
group.addTask {
try await participantUserFlow
.perform { try await $0.call.join() }
.perform { try await $0.call.sendCustomEvent(["state": "joined"]) }
.assertEventuallyInMainActor { $0.call.streamVideo.state.activeCall == nil }
}
_ = try await creatorFlow.call.end()
try await group.waitForAll()
}
try await participantFlow
.assertEventuallyInMainActor(timeout: 30) {
$0.call.streamVideo.state.activeCall == nil
}
.perform { try await $0.call.join() }
let participantFlow = try await participantUserFlow
.perform { try await $0.call.join() }
.assertEventuallyInMainActor {
$0.call.streamVideo.state.activeCall?.cId == $0.call.cId
}
_ = try await creatorFlow.call.end()
try await participantFlow
.assertEventuallyInMainActor(timeout: 30) {
$0.call.streamVideo.state.activeCall == nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@StreamVideoTests/IntegrationTests/Call_IntegrationTests.swift` around lines
385 - 395, Update the participant join flow in the integration test to assert,
after participantUserFlow.perform completes and before creatorFlow.call.end(),
that the participant client's activeCall is this call. Keep the existing
eventual assertion for activeCall becoming nil after the call ends.

Comment on lines +25 to +27
func test_snapshot_renderingModeIsEmpty() {
XCTAssertEqual(AVAudioSession.Snapshot().renderingMode, "")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rename the test to match the repository convention.

Use test_snapshot_rendering_mode_is_empty_() instead of test_snapshot_renderingModeIsEmpty.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@StreamVideoTests/Utils/AudioSession/RTCAudioStore/Components/AVAudioSessionObserver_Tests.swift`
around lines 25 - 27, Rename the test method test_snapshot_renderingModeIsEmpty
to test_snapshot_rendering_mode_is_empty_ to follow the repository’s naming
convention, without changing its assertions or behavior.

Source: Coding guidelines


🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scope this assertion to simulator builds.

AVAudioSession.Snapshot uses source.renderingMode on supported Swift 6 device builds running iOS 17.2 or later. The unconditional empty-string assertion can fail on those devices. Compile this test only for simulator targets, or assert the device path separately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@StreamVideoTests/Utils/AudioSession/RTCAudioStore/Components/AVAudioSessionObserver_Tests.swift`
around lines 25 - 27, Scope test_snapshot_renderingModeIsEmpty to simulator
builds so the empty-string assertion runs only where AVAudioSession.Snapshot
does not expose the device rendering mode. Use the project’s existing
conditional-compilation conventions, or add a separate device assertion for
supported iOS 17.2+ builds.

@Stream-SDK-Bot

Copy link
Copy Markdown
Collaborator

SDK Size

title develop branch diff status
StreamVideo 9.36 MB 9.36 MB 0 KB 🟢
StreamVideoSwiftUI 2.48 MB 2.48 MB 0 KB 🟢
StreamVideoUIKit 2.62 MB 2.62 MB 0 KB 🟢
StreamWebRTC 11.87 MB 11.87 MB 0 KB 🟢
StreamCore 0.92 MB 0.92 MB 0 KB 🟢

@Stream-SDK-Bot

Copy link
Copy Markdown
Collaborator

StreamVideo XCSize

Object Diff (bytes)
WebRTCCoordinatorProviding.o +470
CallController.o -184
PeerConnectionFactory.o +136
StreamVideoEnvironment.o +80

@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

@ipavlidakis
ipavlidakis merged commit 989e94f into develop Aug 7, 2026
25 of 26 checks passed
@ipavlidakis
ipavlidakis deleted the iliaspavlidakis/fix-integration-tests-flakiness branch August 7, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants