SDT/tests/DevTool.Tests/HeadlessExecutionTests.cs
2026-03-04 16:40:57 -06:00

187 lines
5.3 KiB
C#

using System.Diagnostics;
using Xunit;
namespace DevTool.Tests;
public sealed class HeadlessExecutionTests
{
[Fact]
public async Task HeadlessRun_JsonMode_EmitsVersionedEventsAndSummary()
{
var root = CreateTempProject("""
{
"name": "headless-demo",
"version": "0.1.0",
"workflows": [
{
"id": "build",
"label": "Build",
"description": "Build",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "s1",
"label": "dotnet --version",
"command": "dotnet",
"args": ["--version"],
"workingDir": ".",
"requires": []
}
]
}
]
}
""");
var result = await RunSdtAsync(["run", "build", "--json", "--project-root", root]);
Assert.Equal(0, result.ExitCode);
Assert.Contains("\"run_event_version\":\"1.0\"", result.StdOut, StringComparison.Ordinal);
Assert.Contains("\"event_type\":\"WorkflowStarted\"", result.StdOut, StringComparison.Ordinal);
Assert.Contains("\"category\":\"workflow\"", result.StdOut, StringComparison.Ordinal);
Assert.Contains("\"success\":true", result.StdOut, StringComparison.Ordinal);
}
[Fact]
public async Task HeadlessRun_NonInteractive_MissingPrereq_UsesDeterministicExitCode()
{
var root = CreateTempProject("""
{
"name": "headless-demo",
"version": "0.1.0",
"workflows": [
{
"id": "build",
"label": "Build",
"description": "Build",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "s1",
"label": "dotnet --version",
"command": "dotnet",
"args": ["--version"],
"workingDir": ".",
"requires": [
{ "tool": "definitely-missing-tool", "installPolicy": "Prompt" }
]
}
]
}
]
}
""");
var result = await RunSdtAsync(["run", "build", "--json", "--project-root", root, "--non-interactive"]);
Assert.Equal(14, result.ExitCode);
Assert.Contains("\"stopReason\":\"UserDeclined\"", result.StdOut, StringComparison.Ordinal);
Assert.Contains("\"exactFixCommand\"", result.StdOut, StringComparison.Ordinal);
}
[Fact]
public async Task HeadlessDebug_JsonMode_EmitsDebugSummary()
{
var root = CreateTempProject("""
{
"name": "headless-demo",
"version": "0.1.0",
"workflows": [],
"debug": {
"profiles": [
{
"id": "d1",
"label": "Dotnet Info",
"type": "dotnet",
"command": "dotnet",
"args": ["--info"],
"workingDir": ".",
"env": {},
"requires": []
}
]
}
}
""");
var result = await RunSdtAsync(["debug", "d1", "--json", "--project-root", root]);
Assert.Equal(0, result.ExitCode);
Assert.Contains("\"category\":\"debug\"", result.StdOut, StringComparison.Ordinal);
Assert.Contains("\"event_type\":\"DebugStarted\"", result.StdOut, StringComparison.Ordinal);
Assert.Contains("\"success\":true", result.StdOut, StringComparison.Ordinal);
}
[Fact]
public async Task HeadlessRun_CommandFailure_MapsToDeterministicExitCode()
{
var root = CreateTempProject("""
{
"name": "headless-demo",
"version": "0.1.0",
"workflows": [
{
"id": "build",
"label": "Build",
"description": "Build",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "s1",
"label": "dotnet bad command",
"command": "dotnet",
"args": ["__definitely_invalid_command__"],
"workingDir": ".",
"requires": []
}
]
}
]
}
""");
var result = await RunSdtAsync(["run", "build", "--json", "--project-root", root]);
Assert.Equal(12, result.ExitCode);
Assert.Contains("\"stopReason\":\"CommandFailed\"", result.StdOut, StringComparison.Ordinal);
Assert.Contains("\"retryInstruction\"", result.StdOut, StringComparison.Ordinal);
}
private static async Task<(int ExitCode, string StdOut, string StdErr)> RunSdtAsync(IReadOnlyList<string> args)
{
var exe = Path.Combine(AppContext.BaseDirectory, OperatingSystem.IsWindows() ? "sdt.exe" : "sdt");
if (!File.Exists(exe))
throw new InvalidOperationException($"Could not find test runtime executable: {exe}");
var psi = new ProcessStartInfo
{
FileName = exe,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
foreach (var arg in args)
psi.ArgumentList.Add(arg);
using var process = new Process { StartInfo = psi };
process.Start();
var stdout = await process.StandardOutput.ReadToEndAsync();
var stderr = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
return (process.ExitCode, stdout, stderr);
}
private static string CreateTempProject(string devtoolJson)
{
var root = Path.Combine(Path.GetTempPath(), "sdt-headless-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
File.WriteAllText(Path.Combine(root, "sdtconfig-headless.json"), devtoolJson);
return root;
}
}