Skip to content

Commit dfd362a

Browse files
test: restore audio/CallKit unit tests deleted in PR3 (greploop pr3 iter 9)
PR3 deleted 446 lines of tests for still-existing code (AudioRingBuffer, AudioManager, CallManager CallKit state machine), leaving the voice-call paths unverified (greptile #81). Restore all three and re-add the test target's explicit-modules config that lives on the combined branch (PR #82): - AudioRingBufferTests / AudioManagerConfigChangeTests: restored as-is; their APIs (AudioRingBuffer init/write/read/count; AudioManager init defaults + sampleRate/channels/frameTimeMs/samplesPerFrame/isActive) match current code, and they don't start the engine (no installTap) so they're simulator-safe. - CallManagerCallKitTests: adapted to PR2's refactor — import RNSAPI (was ReticulumSwift) and handleCallerIdentified now takes the delivery-hash Data (was an Identity); MockCallKitReporter matches the current CallKitReporting protocol verbatim. The outgoing path's startRingback() no-ops in tests (ensureToneOutput guards on audioSessionActivatedByCallKit=false). - pbxproj: SWIFT_ENABLE_EXPLICIT_MODULES=NO on ColumbaAppTests (4 configs) + the app's 6 package products, mirroring the CI-green config on PR #82, so the test target resolves @testable import ColumbaApp + the transitive packages. Verified by analysis (APIs/protocol match, syntax clean, sim-safe); the on-sim build-for-testing run is exercised by the stack's Tests CI on merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent fbc098a commit dfd362a

4 files changed

Lines changed: 510 additions & 0 deletions

File tree

Columba.xcodeproj/project.pbxproj

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,10 @@
108108
084B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F085 /* ZoomableScrollView.swift */; };
109109
13F299C3C99F5D86B797FA11 /* SwiftBLEBridge in Frameworks */ = {isa = PBXBuildFile; productRef = 5BE4B79EB452909EE61ACE6C /* SwiftBLEBridge */; };
110110
238336F8024494A8B57F9419 /* CeaseTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF48C97880B30682DC35613C /* CeaseTelemetry.swift */; };
111+
2B3A3E3FB59D1E22B424E109 /* AudioRingBufferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE307E24C72DBA41D76C9A6C /* AudioRingBufferTests.swift */; };
111112
2F8EC8212FC732AB00235991 /* ReticulumSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 2F8EC8202FC732AB00235991 /* ReticulumSwift */; };
112113
2F8EC8232FC732AF00235991 /* LXMFSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 2F8EC8222FC732AF00235991 /* LXMFSwift */; };
114+
35DF1F7406C71743BBE8C39B /* ReticulumSwift in Frameworks */ = {isa = PBXBuildFile; productRef = DA31FE974552414C399D4949 /* ReticulumSwift */; };
113115
3A790961A270CBDE9A30BC04 /* VoiceCallScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691F2029BDC2C1024AA9B01 /* VoiceCallScreen.swift */; };
114116
4758210ABE17DE6E3BE0B3F6 /* AutoAnnouncePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F87296BB580791A95C977B6 /* AutoAnnouncePolicy.swift */; };
115117
4CC7FE5D6B0D6557B8868210 /* PythonBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 710EBCE55DCC9E71A9AB0E86 /* PythonBridge.swift */; };
@@ -129,6 +131,11 @@
129131
8768F2E6CD7941D82997A1BB /* CallControlButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71922EC204982A357F814F23 /* CallControlButton.swift */; };
130132
886AB689C7699471510BAF9A /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86E428836698BA8A8973A92F /* CallManager.swift */; };
131133
8A321B0938566F0D62D64562 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BEE6E339CC852C9BA8C5D75 /* Python.xcframework */; };
134+
922658C82CEEA53695143F9B /* MapLibre in Frameworks */ = {isa = PBXBuildFile; productRef = DBD8F3A253D413F087742BC0 /* MapLibre */; };
135+
92DDF4AE0AB493CC2AA0BA20 /* LXSTSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 6000D5CED2C3C89F74999BC1 /* LXSTSwift */; };
136+
98547ADE9B17DD692240E7F7 /* LXMFSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 85B9530D8CAE0E16D5371319 /* LXMFSwift */; };
137+
9D9069A3F6302111A4727454 /* SwiftBLEBridge in Frameworks */ = {isa = PBXBuildFile; productRef = DD88CFE74E7E22427BC4D163 /* SwiftBLEBridge */; };
138+
A0C0D9AE12F728A484F0F108 /* AudioManagerConfigChangeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE282FA3937E59BC026E072 /* AudioManagerConfigChangeTests.swift */; };
132139
AC4014BF281AD8BE6FF9E852 /* PeerChildInterfaceRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A2F8952F6F036C94824552 /* PeerChildInterfaceRegistry.swift */; };
133140
BKF001 /* BackendFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BKF002 /* BackendFactory.swift */; };
134141
C2487934101CA21C68F1F458 /* PythonRuntime.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0C41F9E953D0445F3C34AE7 /* PythonRuntime.swift */; };
@@ -138,10 +145,12 @@
138145
DE9220E1D4ABF9A4C2CBA761 /* JetBrainsMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4AB432DD1264CAE1F35B22A /* JetBrainsMono-Regular.ttf */; };
139146
E001 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE01 /* PacketTunnelProvider.swift */; };
140147
E002 /* SharedFrameQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = F076 /* SharedFrameQueue.swift */; };
148+
E49B977D311DA1C9ACE14CAB /* CallManagerCallKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A79B9ECDB4166F8A36A670 /* CallManagerCallKitTests.swift */; };
141149
EA609BF7A35298B1651160C3 /* PttButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68384C48BFF8F5294340EDB /* PttButton.swift */; };
142150
EDF2F6B945FAA23366617FD6 /* TCPClientWizardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD87870C760723D7E77C87E9 /* TCPClientWizardViewModel.swift */; };
143151
F4CA7E2F745F05E21D45E11A /* RNSAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 0FD6A68A52A54D21FDB70324 /* RNSAPI */; };
144152
F80B09722B3A7CCA7C99DE0C /* DiscoveredDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9630845DA60F57A34819CC4B /* DiscoveredDevice.swift */; };
153+
FB2F3248AE405614B36BC798 /* RNSAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E049A7D4D6D58690C0B674CE /* RNSAPI */; };
145154
PNT001 /* PythonNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = PNT002 /* PythonNetworkTransport.swift */; };
146155
PRC001 /* PythonRNodeCallbackBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = PRC002 /* PythonRNodeCallbackBridge.swift */; };
147156
SRB001 /* SwiftRNSBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = SRB002 /* SwiftRNSBackend.swift */; };
@@ -176,11 +185,13 @@
176185
00A8D344945F752C18EF2D9E /* AudioManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
177186
11D4DB375C0C7BB62E8A8B23 /* ColumbaPython-Bridging-Header.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "ColumbaPython-Bridging-Header.h"; sourceTree = "<group>"; };
178187
1F7375908681A4DF99F125C7 /* CallKitManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CallKitManager.swift; sourceTree = "<group>"; };
188+
30A79B9ECDB4166F8A36A670 /* CallManagerCallKitTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CallManagerCallKitTests.swift; sourceTree = "<group>"; };
179189
34E1ECAE91B0A2C56D0FC8AA /* PyLocalIdentity.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PyLocalIdentity.swift; path = Python/Models/PyLocalIdentity.swift; sourceTree = "<group>"; };
180190
3BEE6E339CC852C9BA8C5D75 /* Python.xcframework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.xcframework; name = Python.xcframework; path = Frameworks/Python.xcframework; sourceTree = "<group>"; };
181191
3C562D79BB3B6559A1B1EFDA /* PyAnnounce.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PyAnnounce.swift; path = Python/Models/PyAnnounce.swift; sourceTree = "<group>"; };
182192
3D4C54CECCAF0B117FB6C197 /* IncomingCallScreen.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = IncomingCallScreen.swift; sourceTree = "<group>"; };
183193
51D8CBCF7E546028A043E1C7 /* PyConversation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PyConversation.swift; path = Python/Models/PyConversation.swift; sourceTree = "<group>"; };
194+
6FE282FA3937E59BC026E072 /* AudioManagerConfigChangeTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AudioManagerConfigChangeTests.swift; sourceTree = "<group>"; };
184195
710EBCE55DCC9E71A9AB0E86 /* PythonBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PythonBridge.swift; sourceTree = "<group>"; };
185196
71922EC204982A357F814F23 /* CallControlButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CallControlButton.swift; sourceTree = "<group>"; };
186197
7529BF99835005DE07E1B65F /* CodecSelectionSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CodecSelectionSheet.swift; sourceTree = "<group>"; };
@@ -201,6 +212,7 @@
201212
CCF4DCA18506B96230721ACC /* PythonBLECallbackBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PythonBLECallbackBridge.swift; sourceTree = "<group>"; };
202213
D001472BC7DFD3CD7BF27F0C /* app */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder; path = app; sourceTree = "<group>"; };
203214
D691F2029BDC2C1024AA9B01 /* VoiceCallScreen.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VoiceCallScreen.swift; sourceTree = "<group>"; };
215+
DE307E24C72DBA41D76C9A6C /* AudioRingBufferTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AudioRingBufferTests.swift; sourceTree = "<group>"; };
204216
E4AB432DD1264CAE1F35B22A /* JetBrainsMono-Regular.ttf */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file; path = "JetBrainsMono-Regular.ttf"; sourceTree = "<group>"; };
205217
EBA4F8C4C9008A06DFF5AC8A /* PythonConfigWriter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PythonConfigWriter.swift; sourceTree = "<group>"; };
206218
EPROD /* ColumbaNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ColumbaNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -342,6 +354,12 @@
342354
isa = PBXFrameworksBuildPhase;
343355
buildActionMask = 2147483647;
344356
files = (
357+
922658C82CEEA53695143F9B /* MapLibre in Frameworks */,
358+
92DDF4AE0AB493CC2AA0BA20 /* LXSTSwift in Frameworks */,
359+
FB2F3248AE405614B36BC798 /* RNSAPI in Frameworks */,
360+
9D9069A3F6302111A4727454 /* SwiftBLEBridge in Frameworks */,
361+
35DF1F7406C71743BBE8C39B /* ReticulumSwift in Frameworks */,
362+
98547ADE9B17DD692240E7F7 /* LXMFSwift in Frameworks */,
345363
);
346364
runOnlyForDeploymentPostprocessing = 0;
347365
};
@@ -661,6 +679,9 @@
661679
isa = PBXGroup;
662680
children = (
663681
FT03 /* MicronParserTests.swift */,
682+
DE307E24C72DBA41D76C9A6C /* AudioRingBufferTests.swift */,
683+
6FE282FA3937E59BC026E072 /* AudioManagerConfigChangeTests.swift */,
684+
30A79B9ECDB4166F8A36A670 /* CallManagerCallKitTests.swift */,
664685
);
665686
path = Tests/ColumbaAppTests;
666687
sourceTree = "<group>";
@@ -813,6 +834,14 @@
813834
TTDEP /* PBXTargetDependency */,
814835
);
815836
name = ColumbaAppTests;
837+
packageProductDependencies = (
838+
DBD8F3A253D413F087742BC0 /* MapLibre */,
839+
6000D5CED2C3C89F74999BC1 /* LXSTSwift */,
840+
E049A7D4D6D58690C0B674CE /* RNSAPI */,
841+
DD88CFE74E7E22427BC4D163 /* SwiftBLEBridge */,
842+
DA31FE974552414C399D4949 /* ReticulumSwift */,
843+
85B9530D8CAE0E16D5371319 /* LXMFSwift */,
844+
);
816845
productName = ColumbaAppTests;
817846
productReference = TPROD /* ColumbaAppTests.xctest */;
818847
productType = "com.apple.product-type.bundle.unit-test";
@@ -1050,6 +1079,9 @@
10501079
buildActionMask = 2147483647;
10511080
files = (
10521081
T003 /* MicronParserTests.swift in Sources */,
1082+
2B3A3E3FB59D1E22B424E109 /* AudioRingBufferTests.swift in Sources */,
1083+
A0C0D9AE12F728A484F0F108 /* AudioManagerConfigChangeTests.swift in Sources */,
1084+
E49B977D311DA1C9ACE14CAB /* CallManagerCallKitTests.swift in Sources */,
10531085
);
10541086
runOnlyForDeploymentPostprocessing = 0;
10551087
};
@@ -1233,6 +1265,7 @@
12331265
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
12341266
SUPPORTS_MACCATALYST = NO;
12351267
SWIFT_EMIT_LOC_STRINGS = NO;
1268+
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
12361269
SWIFT_VERSION = 5.0;
12371270
TARGETED_DEVICE_FAMILY = "1,2";
12381271
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ColumbaApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ColumbaApp";
@@ -1337,6 +1370,7 @@
13371370
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
13381371
SUPPORTS_MACCATALYST = NO;
13391372
SWIFT_EMIT_LOC_STRINGS = NO;
1373+
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
13401374
SWIFT_VERSION = 5.0;
13411375
TARGETED_DEVICE_FAMILY = "1,2";
13421376
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ColumbaApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ColumbaApp";
@@ -1646,6 +1680,7 @@
16461680
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
16471681
SUPPORTS_MACCATALYST = NO;
16481682
SWIFT_EMIT_LOC_STRINGS = NO;
1683+
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
16491684
SWIFT_VERSION = 5.0;
16501685
TARGETED_DEVICE_FAMILY = "1,2";
16511686
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ColumbaApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ColumbaApp";
@@ -1666,6 +1701,7 @@
16661701
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
16671702
SUPPORTS_MACCATALYST = NO;
16681703
SWIFT_EMIT_LOC_STRINGS = NO;
1704+
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
16691705
SWIFT_VERSION = 5.0;
16701706
TARGETED_DEVICE_FAMILY = "1,2";
16711707
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ColumbaApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ColumbaApp";
@@ -1782,11 +1818,39 @@
17821818
isa = XCSwiftPackageProductDependency;
17831819
productName = SwiftBLEBridge;
17841820
};
1821+
6000D5CED2C3C89F74999BC1 /* LXSTSwift */ = {
1822+
isa = XCSwiftPackageProductDependency;
1823+
package = PKGREF3 /* XCRemoteSwiftPackageReference "LXST-swift" */;
1824+
productName = LXSTSwift;
1825+
};
1826+
85B9530D8CAE0E16D5371319 /* LXMFSwift */ = {
1827+
isa = XCSwiftPackageProductDependency;
1828+
package = 2F8EC81F2FC7326600235991 /* XCRemoteSwiftPackageReference "LXMF-swift" */;
1829+
productName = LXMFSwift;
1830+
};
17851831
A49337EFC55C10979AEB702B /* LXSTSwift */ = {
17861832
isa = XCSwiftPackageProductDependency;
17871833
package = PKGREF3 /* XCRemoteSwiftPackageReference "LXST-swift" */;
17881834
productName = LXSTSwift;
17891835
};
1836+
DA31FE974552414C399D4949 /* ReticulumSwift */ = {
1837+
isa = XCSwiftPackageProductDependency;
1838+
package = 2F8EC81E2FC7324D00235991 /* XCRemoteSwiftPackageReference "reticulum-swift" */;
1839+
productName = ReticulumSwift;
1840+
};
1841+
DBD8F3A253D413F087742BC0 /* MapLibre */ = {
1842+
isa = XCSwiftPackageProductDependency;
1843+
package = PKGREF2 /* XCRemoteSwiftPackageReference "maplibre-gl-native-distribution" */;
1844+
productName = MapLibre;
1845+
};
1846+
DD88CFE74E7E22427BC4D163 /* SwiftBLEBridge */ = {
1847+
isa = XCSwiftPackageProductDependency;
1848+
productName = SwiftBLEBridge;
1849+
};
1850+
E049A7D4D6D58690C0B674CE /* RNSAPI */ = {
1851+
isa = XCSwiftPackageProductDependency;
1852+
productName = RNSAPI;
1853+
};
17901854
P002 /* MapLibre */ = {
17911855
isa = XCSwiftPackageProductDependency;
17921856
package = PKGREF2 /* XCRemoteSwiftPackageReference "maplibre-gl-native-distribution" */;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import XCTest
2+
import AVFoundation
3+
@testable import ColumbaApp
4+
5+
/// Tests for AudioManager's config change resilience.
6+
///
7+
/// These tests verify the format selection logic that prevents crashes when
8+
/// toggling speaker during a voice call. The core issue: after an
9+
/// AVAudioEngineConfigurationChange, inputNode.outputFormat returns a stale
10+
/// codec rate (e.g. 24kHz) while the hardware has reverted to native 48kHz.
11+
/// Installing a tap with the wrong format crashes with "format mismatch".
12+
@available(macOS 14.0, iOS 17.0, *)
13+
final class AudioManagerConfigChangeTests: XCTestCase {
14+
15+
// MARK: - Initialization
16+
17+
@MainActor
18+
func testDefaultInit() {
19+
let mgr = AudioManager(sampleRate: 48000, channels: 1, frameTimeMs: 20)
20+
XCTAssertEqual(mgr.sampleRate, 48000)
21+
XCTAssertEqual(mgr.channels, 1)
22+
XCTAssertEqual(mgr.frameTimeMs, 20)
23+
XCTAssertEqual(mgr.samplesPerFrame, 960) // 48000 * 20 / 1000
24+
XCTAssertFalse(mgr.isActive)
25+
}
26+
27+
@MainActor
28+
func testLowRateCodecInit() {
29+
let mgr = AudioManager(sampleRate: 24000, channels: 1, frameTimeMs: 60)
30+
XCTAssertEqual(mgr.sampleRate, 24000)
31+
XCTAssertEqual(mgr.samplesPerFrame, 1440) // 24000 * 60 / 1000
32+
}
33+
34+
@MainActor
35+
func testStereoInit() {
36+
let mgr = AudioManager(sampleRate: 48000, channels: 2, frameTimeMs: 20)
37+
XCTAssertEqual(mgr.channels, 2)
38+
XCTAssertEqual(mgr.samplesPerFrame, 960)
39+
}
40+
41+
// MARK: - State Guards
42+
43+
@MainActor
44+
func testStopWhenNotActiveIsNoOp() {
45+
let mgr = AudioManager()
46+
// Should not crash
47+
mgr.stop()
48+
XCTAssertFalse(mgr.isActive)
49+
}
50+
51+
@MainActor
52+
func testHandleAudioSessionActivatedWhenNotActive() {
53+
let mgr = AudioManager()
54+
// Should be a no-op when not active
55+
mgr.handleAudioSessionActivated()
56+
XCTAssertFalse(mgr.isActive)
57+
}
58+
59+
@MainActor
60+
func testPlayDecodedAudioWhenNotActive() {
61+
let mgr = AudioManager(sampleRate: 48000, channels: 1, frameTimeMs: 20)
62+
// Should silently drop without crash
63+
let samples = [Float](repeating: 0.5, count: 960)
64+
mgr.playDecodedAudio(samples)
65+
XCTAssertFalse(mgr.isActive)
66+
}
67+
68+
// MARK: - Format Creation
69+
70+
func testAVAudioFormatCreation48kHz() {
71+
let format = AVAudioFormat(standardFormatWithSampleRate: 48000, channels: 1)
72+
XCTAssertNotNil(format)
73+
XCTAssertEqual(format?.sampleRate, 48000)
74+
XCTAssertEqual(format?.channelCount, 1)
75+
}
76+
77+
func testAVAudioFormatCreation24kHz() {
78+
let format = AVAudioFormat(standardFormatWithSampleRate: 24000, channels: 1)
79+
XCTAssertNotNil(format)
80+
XCTAssertEqual(format?.sampleRate, 24000)
81+
XCTAssertEqual(format?.channelCount, 1)
82+
}
83+
84+
/// Verify that two formats with different sample rates are NOT equal.
85+
/// This is the root cause of the speaker toggle crash — the tap format
86+
/// must match the hardware format exactly.
87+
func testFormatMismatchDetection() {
88+
let fmt24k = AVAudioFormat(standardFormatWithSampleRate: 24000, channels: 1)!
89+
let fmt48k = AVAudioFormat(standardFormatWithSampleRate: 48000, channels: 1)!
90+
XCTAssertNotEqual(fmt24k, fmt48k)
91+
XCTAssertNotEqual(fmt24k.sampleRate, fmt48k.sampleRate)
92+
}
93+
94+
// MARK: - Playback Mono Downmix
95+
96+
@MainActor
97+
func testPlayDecodedAudioMonoDownmix() {
98+
// Verify the mono downmix logic doesn't crash with stereo input
99+
let mgr = AudioManager(sampleRate: 48000, channels: 2, frameTimeMs: 20)
100+
// 960 stereo samples = 1920 interleaved floats
101+
let stereoSamples = [Float](repeating: 0.3, count: 1920)
102+
// Should not crash (audio won't play since engine isn't started)
103+
mgr.playDecodedAudio(stereoSamples)
104+
}
105+
}

0 commit comments

Comments
 (0)