using System.Diagnostics; using System.Text.Json; using GameCli; using Xunit; namespace GameCli.Tests; public class ProgramSmokeTests { private static string GetProjectPath() { // Locate the GameCli binary relative to the test binary's output dir. // Tests run from GameCli.Tests/bin//net8.0/, so ../../../.. gets to repo root. var testDir = AppContext.BaseDirectory; var repoRoot = Path.GetFullPath(Path.Combine(testDir, "..", "..", "..", "..")); var projectPath = Path.Combine(repoRoot, "GameCli", "GameCli.csproj"); Assert.True(File.Exists(projectPath), $"could not find GameCli.csproj at {projectPath}"); return projectPath; } private static Process StartGameCli() { var projectPath = GetProjectPath(); var psi = new ProcessStartInfo { FileName = "dotnet", ArgumentList = { "run", "--project", projectPath, "-c", "Release", "--no-build" }, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, }; var p = Process.Start(psi) ?? throw new InvalidOperationException("failed to start GameCli"); return p; } [Fact] public void FirstOutputLine_IsInitHandshake() { // Ensure release build exists so --no-build works reliably. RunOrFail("dotnet", $"build -c Release \"{GetProjectPath()}\""); using var p = StartGameCli(); try { var line = p.StandardOutput.ReadLine(); Assert.NotNull(line); var init = JsonSerializer.Deserialize(line!, ProtocolJsonContext.Default.InitMessage); Assert.NotNull(init?.Init); Assert.Equal(Physics.Dt, init!.Init.Dt); Assert.Equal(Observation.Dim, init.Init.ObsDim); Assert.Equal(4, init.Init.NActions); } finally { p.StandardInput.Close(); p.WaitForExit(3000); } } [Fact] public void StepRoundTrip_ReturnsSevenDimObservation() { RunOrFail("dotnet", $"build -c Release \"{GetProjectPath()}\""); using var p = StartGameCli(); try { _ = p.StandardOutput.ReadLine(); // skip init p.StandardInput.WriteLine("""{"action":0,"target":[0.5,0.5]}"""); p.StandardInput.Flush(); var line = p.StandardOutput.ReadLine(); Assert.NotNull(line); var output = JsonSerializer.Deserialize(line!, ProtocolJsonContext.Default.StepOutput); Assert.NotNull(output); Assert.Equal(7, output!.Obs.Length); Assert.Equal(1, output.Step); } finally { p.StandardInput.Close(); p.WaitForExit(3000); } } [Fact] public void ResetCommand_ResetsStepCounter() { RunOrFail("dotnet", $"build -c Release \"{GetProjectPath()}\""); using var p = StartGameCli(); try { _ = p.StandardOutput.ReadLine(); // init // Take three steps. for (int i = 0; i < 3; i++) { p.StandardInput.WriteLine("""{"action":0,"target":[0.5,0.5]}"""); } p.StandardInput.Flush(); string? last = null; for (int i = 0; i < 3; i++) last = p.StandardOutput.ReadLine(); var afterThree = JsonSerializer.Deserialize(last!, ProtocolJsonContext.Default.StepOutput); Assert.Equal(3, afterThree!.Step); // Reset. Expect step=0 on the next output line. p.StandardInput.WriteLine("""{"cmd":"reset","seed":42}"""); p.StandardInput.Flush(); var afterReset = JsonSerializer.Deserialize( p.StandardOutput.ReadLine()!, ProtocolJsonContext.Default.StepOutput); Assert.Equal(0, afterReset!.Step); } finally { p.StandardInput.Close(); p.WaitForExit(3000); } } private static void RunOrFail(string file, string args) { var psi = new ProcessStartInfo(file, args) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, }; using var p = Process.Start(psi)!; p.WaitForExit(120_000); if (p.ExitCode != 0) throw new Exception($"{file} {args} exited {p.ExitCode}\nstderr:\n{p.StandardError.ReadToEnd()}"); } }