258 lines
9.5 KiB
C#
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)));
|
|
}
|
|
}
|