SDT/tests/DevTool.Tests/HeadlessExecutionTests.cs
stan44 d5a74be368 Add guided CLI workflows and config commands
- introduce `sdt` subcommands for run, debug, setup, env, favorite, and explain
- add project/workspace discovery plus config bootstrap and migration helpers
- expand tests for CLI parsing, project role detection, and headless flows
2026-03-29 22:22:48 -05:00

224 lines
6.2 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\":\"{Sdt.Core.HeadlessExecutionService.ContractVersion}\"", 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);
}
[Fact]
public async Task HeadlessRun_AllowsGuidedAliasSelection()
{
var root = CreateTempProject("""
{
"name": "headless-demo",
"version": "0.1.0",
"workflows": [
{
"id": "build",
"label": "Build project",
"guidedName": "build app",
"aliases": ["app", "compile"],
"description": "Build",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "s1",
"label": "dotnet --version",
"command": "dotnet",
"args": ["--version"],
"workingDir": ".",
"requires": []
}
]
}
]
}
""");
var result = await RunSdtAsync(["run", "app", "--json", "--project-root", root]);
Assert.Equal(0, result.ExitCode);
Assert.Contains("\"success\":true", 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;
}
}