using System.Runtime.InteropServices; using VoiceCat.Interop; namespace VoiceCat.Interop.Tests; /// /// API-surface smoke tests for StreamFeedPcm / SetPcmSink through the P/Invoke layer. /// These tests need voicecat.dll but NOT a running server — they verify the C# wrapper /// compiles, is callable, and the error paths work at the C-ABI boundary. Full E2E relay /// verification is handled by tests/test_external_pcm.cpp (C++ ctest). /// public sealed class ExternalPcmTests { [Fact] public void StreamFeedPcm_NoActiveStream_ReturnsInvalidArg() { using var client = new VoiceCatClient("ext-pcm-test", "0.1", VcLogLevel.Off); var pcm = new short[960]; // Stream 0 doesn't exist — core must return InvalidArg, not crash. var result = client.StreamFeedPcm(0, pcm, 960, channels: 1); Assert.Equal(VcResult.InvalidArg, result); } [Fact] public void StreamFeedPcm_InvalidChannels_ReturnsInvalidArg() { using var client = new VoiceCatClient("ext-pcm-test", "0.1", VcLogLevel.Off); var pcm = new short[960 * 3]; // channels=3 is not supported — trampoline rejects before touching audio state. var result = client.StreamFeedPcm(0, pcm, 960, channels: 3); Assert.Equal(VcResult.InvalidArg, result); } [Fact] public void SetPcmSink_Zero_ReturnsOk() { using var client = new VoiceCatClient("ext-pcm-test", "0.1", VcLogLevel.Off); // IntPtr.Zero = disable — default state, must be a no-op that returns Ok. var result = client.SetPcmSink(IntPtr.Zero, IntPtr.Zero); Assert.Equal(VcResult.Ok, result); } [Fact] public void SetPcmSink_EnableThenDisable_BothSucceed() { using var client = new VoiceCatClient("ext-pcm-test", "0.1", VcLogLevel.Off); // Keep the delegate alive until after we unregister it (not just until the P/Invoke call). NativeMethods.VcPcmSinkCallback sink = static (_, _, _, _, _, _, _) => { }; nint fp = Marshal.GetFunctionPointerForDelegate(sink); Assert.Equal(VcResult.Ok, client.SetPcmSink(fp, IntPtr.Zero)); Assert.Equal(VcResult.Ok, client.SetPcmSink(IntPtr.Zero, IntPtr.Zero)); GC.KeepAlive(sink); } }