SDT/tests/DevTool.Tests/DebugServicesTests.cs
2026-03-01 20:52:56 -06:00

258 lines
9.5 KiB
C#

using Sdt.Config;
using Sdt.Core;
using Sdt.Core.Debug;
using Sdt.Runner;
using System.Text.Json;
using Xunit;
namespace DevTool.Tests;
public sealed class DebugServicesTests
{
[Fact]
public async Task DebugRunner_MissingPrereqDeclined_ReturnsUserDeclined()
{
var runner = new DebugProfileRunner(
new FakeProbeService(false),
new FakeInstallerService(true));
var profile = new DebugProfileDefinition
{
Id = "p1",
Label = "Profile",
Type = "python",
Command = "python",
Args = ["--version"],
Requires = [new ToolRequirement { Tool = "python", InstallPolicy = InstallPolicy.Prompt }]
};
var result = await runner.RunAsync(
profile,
new DevToolConfig(),
Directory.GetCurrentDirectory(),
verbose: false,
confirmInstallAsync: (_, _) => Task.FromResult(false),
onOutput: (_, _) => { });
Assert.False(result.Success);
Assert.Equal(ExecutionStopReason.UserDeclined, result.StopReason);
}
[Fact]
public async Task DiagnosticsBundle_WritesFiles()
{
var service = new DiagnosticsBundleService();
var root = Path.Combine(Path.GetTempPath(), "sdt-diag-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var request = new DiagnosticsBundleRequest(
Category: "workflow",
ProjectRoot: root,
SummaryMessage: "failed",
OutputLines: ["hello"],
WorkflowSteps: [],
Probes: [],
DiagnosticsOptions: new DebugDiagnosticsOptions { OutputDir = ".sdt/debug" },
Config: new DevToolConfig(),
StopReason: ExecutionStopReason.CommandFailed);
var result = await service.WriteBundleAsync(request);
Assert.True(result.Success);
Assert.True(File.Exists(Path.Combine(result.BundleDirectory, "summary.json")));
Assert.True(File.Exists(Path.Combine(result.BundleDirectory, "output.log")));
}
[Fact]
public async Task DiagnosticsBundle_EmptyAllowlist_CapturesNoEnvByDefault()
{
var service = new DiagnosticsBundleService();
var root = Path.Combine(Path.GetTempPath(), "sdt-diag-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var request = new DiagnosticsBundleRequest(
Category: "workflow",
ProjectRoot: root,
SummaryMessage: "failed",
OutputLines: [],
WorkflowSteps: [],
Probes: [],
DiagnosticsOptions: new DebugDiagnosticsOptions
{
OutputDir = ".sdt/debug",
IncludeAllEnv = false,
CaptureEnvKeys = []
},
Config: new DevToolConfig(),
StopReason: ExecutionStopReason.CommandFailed);
var result = await service.WriteBundleAsync(request);
Assert.True(result.Success);
var envJson = await File.ReadAllTextAsync(Path.Combine(result.BundleDirectory, "env.json"));
using var doc = JsonDocument.Parse(envJson);
Assert.Empty(doc.RootElement.EnumerateObject());
}
[Fact]
public async Task DiagnosticsBundle_Allowlist_CapturesOnlyListedKeys()
{
var service = new DiagnosticsBundleService();
var root = Path.Combine(Path.GetTempPath(), "sdt-diag-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
Environment.SetEnvironmentVariable("SDT_TEST_ENV_A", "A");
Environment.SetEnvironmentVariable("SDT_TEST_ENV_B", "B");
var request = new DiagnosticsBundleRequest(
Category: "workflow",
ProjectRoot: root,
SummaryMessage: "failed",
OutputLines: [],
WorkflowSteps: [],
Probes: [],
DiagnosticsOptions: new DebugDiagnosticsOptions
{
OutputDir = ".sdt/debug",
IncludeAllEnv = false,
CaptureEnvKeys = ["SDT_TEST_ENV_A"]
},
Config: new DevToolConfig(),
StopReason: ExecutionStopReason.CommandFailed);
var result = await service.WriteBundleAsync(request);
Assert.True(result.Success);
var envJson = await File.ReadAllTextAsync(Path.Combine(result.BundleDirectory, "env.json"));
using var doc = JsonDocument.Parse(envJson);
Assert.True(doc.RootElement.TryGetProperty("SDT_TEST_ENV_A", out _));
Assert.False(doc.RootElement.TryGetProperty("SDT_TEST_ENV_B", out _));
}
[Fact]
public async Task DiagnosticsBundle_RedactsSensitiveEnvKeys()
{
var service = new DiagnosticsBundleService();
var root = Path.Combine(Path.GetTempPath(), "sdt-diag-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
Environment.SetEnvironmentVariable("MY_SECRET_TOKEN", "super-secret");
var request = new DiagnosticsBundleRequest(
Category: "workflow",
ProjectRoot: root,
SummaryMessage: "failed",
OutputLines: [],
WorkflowSteps: [],
Probes: [],
DiagnosticsOptions: new DebugDiagnosticsOptions
{
OutputDir = ".sdt/debug",
IncludeAllEnv = false,
CaptureEnvKeys = ["MY_SECRET_TOKEN"],
RedactSensitive = true
},
Config: new DevToolConfig(),
StopReason: ExecutionStopReason.CommandFailed);
var result = await service.WriteBundleAsync(request);
Assert.True(result.Success);
var envJson = await File.ReadAllTextAsync(Path.Combine(result.BundleDirectory, "env.json"));
using var doc = JsonDocument.Parse(envJson);
Assert.Equal("***REDACTED***", doc.RootElement.GetProperty("MY_SECRET_TOKEN").GetString());
}
[Fact]
public async Task DiagnosticsBundle_RedactsSensitiveOutputPatterns()
{
var service = new DiagnosticsBundleService();
var root = Path.Combine(Path.GetTempPath(), "sdt-diag-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var request = new DiagnosticsBundleRequest(
Category: "workflow",
ProjectRoot: root,
SummaryMessage: "failed",
OutputLines: ["token=abc123", "Authorization: Bearer verysecretvalue"],
WorkflowSteps: [],
Probes: [],
DiagnosticsOptions: new DebugDiagnosticsOptions
{
OutputDir = ".sdt/debug",
RedactSensitive = true
},
Config: new DevToolConfig(),
StopReason: ExecutionStopReason.CommandFailed);
var result = await service.WriteBundleAsync(request);
Assert.True(result.Success);
var output = await File.ReadAllTextAsync(Path.Combine(result.BundleDirectory, "output.log"));
Assert.Contains("***REDACTED***", output, StringComparison.Ordinal);
Assert.DoesNotContain("verysecretvalue", output, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task DebugRunner_EmitsRunEvents()
{
var runner = new DebugProfileRunner(
new FakeProbeService(true),
new FakeInstallerService(true));
var profile = new DebugProfileDefinition
{
Id = "p1",
Label = "Profile",
Type = "python",
Command = "python",
Args = ["--version"],
Requires = [new ToolRequirement { Tool = "python", InstallPolicy = InstallPolicy.Prompt }]
};
var events = new List<RunEvent>();
var result = await runner.RunAsync(
profile,
new DevToolConfig(),
Directory.GetCurrentDirectory(),
verbose: false,
confirmInstallAsync: (_, _) => Task.FromResult(false),
onOutput: (_, _) => { },
onEvent: events.Add);
Assert.True(result.Success);
Assert.Contains(events, e => e.Type == RunEventType.DebugStarted);
Assert.Contains(events, e => e.Type == RunEventType.DebugCommandStarted);
Assert.Contains(events, e => e.Type == RunEventType.DebugCommandCompleted);
Assert.Contains(events, e => e.Type == RunEventType.DebugCompleted && e.Success == true);
}
private sealed class FakeProbeService(bool isAvailable) : IToolProbe
{
public Task<ProbeResult> ProbeAsync(
string tool,
string projectRoot,
DevToolConfig? config = null,
IReadOnlyDictionary<string, string>? envOverrides = null,
CancellationToken cancellationToken = default)
=> Task.FromResult(new ProbeResult(tool, isAvailable, Version: isAvailable ? "1.0.0" : null));
}
private sealed class FakeInstallerService(bool success) : IPrereqInstaller
{
public Task<InstallPlan> GetInstallPlanAsync(
string tool,
string projectRoot,
DevToolConfig? config = null,
CancellationToken cancellationToken = default)
=> Task.FromResult(new InstallPlan(tool, Supported: true, "test", [new InstallCommand("echo", ["ok"])]));
public Task<RunResult> RunInstallAsync(
InstallCommand command,
string projectRoot,
Action<string, bool> onOutput,
IReadOnlyDictionary<string, string>? envOverrides = null,
CancellationToken cancellationToken = default)
=> Task.FromResult(new RunResult(success ? 0 : 1, TimeSpan.FromMilliseconds(5)));
}
}