using System.Diagnostics; using static VoiceCat.Tests.ServerTests; namespace VoiceCat.Tests; public class ManagedCliTests { [Fact] public async Task ManagedCliInteroperatesWithExistingCppCli() { string? nativeCli = Environment.GetEnvironmentVariable("VOICECAT_VCCLI"); if (string.IsNullOrEmpty(nativeCli)) return; // Full conformance runs set this explicitly. await using var fixture = new ServerFixture(); string cli = Path.Combine(FindRoot(), "dotnet", "src", "VoiceCat.Cli", "bin", "Release", "net10.0", "VoiceCat.Cli.dll"); using Process managed = Start(cli, fixture, "Managed", "Managed checkpoint", "Native checkpoint", 2); await Task.Delay(750); // Native vccli sends its one-shot text immediately after auth. var nativeStart = new ProcessStartInfo(nativeCli) { WorkingDirectory = fixture.Directory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; foreach (string argument in new[] { "--host", "127.0.0.1", "--port", fixture.Server.EndPoint.Port.ToString(), "--nick", "Native", "--channel", "2", "--text", "Native checkpoint", "--test-tone-ms", "5000" }) nativeStart.ArgumentList.Add(argument); using Process native = Process.Start(nativeStart)!; Task managedOut = managed.StandardOutput.ReadToEndAsync(), managedError = managed.StandardError.ReadToEndAsync(); Task nativeOut = native.StandardOutput.ReadToEndAsync(), nativeError = native.StandardError.ReadToEndAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(35)); await Task.WhenAll(managed.WaitForExitAsync(timeout.Token), native.WaitForExitAsync(timeout.Token)); string mout = await managedOut, nout = await nativeOut; Assert.True(managed.ExitCode == 0, await managedError + Environment.NewLine + mout); Assert.True(native.ExitCode == 0, await nativeError + Environment.NewLine + nout); Assert.Contains("Native checkpoint", mout); Assert.Contains("Managed checkpoint", nout); Assert.Contains("[test-tone] received=", nout); Assert.DoesNotContain("\"voiceEnergy\":0", mout); } [Theory] [InlineData(1u)] [InlineData(2u)] public async Task TwoManagedCliProcessesExchangeTextAndDecodedVoice(uint channel) { await using var fixture = new ServerFixture(); string root = FindRoot(); string cli = Path.Combine(root, "dotnet", "src", "VoiceCat.Cli", "bin", "Release", "net10.0", "VoiceCat.Cli.dll"); Assert.True(File.Exists(cli), $"Managed CLI was not built at {cli}."); using Process alice = Start(cli, fixture, "Alice", "Alice says hello", "Bob says hello", channel); using Process bob = Start(cli, fixture, "Bob", "Bob says hello", "Alice says hello", channel); Task aliceOut = alice.StandardOutput.ReadToEndAsync(); Task aliceError = alice.StandardError.ReadToEndAsync(); Task bobOut = bob.StandardOutput.ReadToEndAsync(); Task bobError = bob.StandardError.ReadToEndAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(35)); await Task.WhenAll(alice.WaitForExitAsync(timeout.Token), bob.WaitForExitAsync(timeout.Token)); string aout = await aliceOut, bout = await bobOut; Assert.True(alice.ExitCode == 0, await aliceError + Environment.NewLine + aout); Assert.True(bob.ExitCode == 0, await bobError + Environment.NewLine + bout); Assert.Contains("Bob says hello", aout); Assert.Contains("Alice says hello", bout); Assert.Contains("\"type\":\"complete\"", aout); Assert.Contains("\"type\":\"complete\"", bout); Assert.DoesNotContain("\"voiceEnergy\":0", aout); Assert.DoesNotContain("\"voiceEnergy\":0", bout); } private static Process Start(string cli, ServerFixture fixture, string name, string send, string expect, uint channel) { var start = new ProcessStartInfo("dotnet") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false }; foreach (string argument in new[] { cli, "--host", "127.0.0.1", "--port", fixture.Server.EndPoint.Port.ToString(), "--nickname", name, "--pins", Path.Combine(fixture.Directory, name + ".cli.pins"), "--trust-first", "--channel", channel.ToString(), "--voice", "--expect-voice", "--send-text", send, "--expect-text", expect, "--start-delay-ms", "1500", "--timeout-seconds", "20" }) start.ArgumentList.Add(argument); return Process.Start(start) ?? throw new InvalidOperationException("Could not start managed CLI."); } private static string FindRoot() { DirectoryInfo? directory = new(AppContext.BaseDirectory); while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "CMakePresets.json"))) directory = directory.Parent; return directory?.FullName ?? throw new DirectoryNotFoundException("Repository root not found."); } }