save progress
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
// <copyright file="BlastRadiusTestRunner.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
// Sprint: SPRINT_20260105_002_005_TEST_cross_cutting
|
||||
// Task: CCUT-002
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace StellaOps.TestKit.BlastRadius;
|
||||
|
||||
/// <summary>
|
||||
/// Runs tests filtered by blast radius for incident response.
|
||||
/// </summary>
|
||||
public static class BlastRadiusTestRunner
|
||||
{
|
||||
/// <summary>
|
||||
/// Get xUnit filter for specific blast radii.
|
||||
/// </summary>
|
||||
/// <param name="blastRadii">Blast radii to filter by.</param>
|
||||
/// <returns>xUnit filter string.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when no blast radii provided.</exception>
|
||||
public static string GetFilter(params string[] blastRadii)
|
||||
{
|
||||
if (blastRadii.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("At least one blast radius required", nameof(blastRadii));
|
||||
}
|
||||
|
||||
var filters = blastRadii.Select(br => $"BlastRadius={br}");
|
||||
return string.Join("|", filters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get xUnit filter for specific blast radii (IEnumerable overload).
|
||||
/// </summary>
|
||||
/// <param name="blastRadii">Blast radii to filter by.</param>
|
||||
/// <returns>xUnit filter string.</returns>
|
||||
public static string GetFilter(IEnumerable<string> blastRadii)
|
||||
{
|
||||
return GetFilter(blastRadii.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the dotnet test command for specific blast radii.
|
||||
/// </summary>
|
||||
/// <param name="testProject">Test project path or solution.</param>
|
||||
/// <param name="blastRadii">Blast radii to filter by.</param>
|
||||
/// <param name="additionalArgs">Additional dotnet test arguments.</param>
|
||||
/// <returns>Complete dotnet test command.</returns>
|
||||
public static string GetCommand(
|
||||
string testProject,
|
||||
IEnumerable<string> blastRadii,
|
||||
string? additionalArgs = null)
|
||||
{
|
||||
var filter = GetFilter(blastRadii);
|
||||
var args = $"test {testProject} --filter \"{filter}\"";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(additionalArgs))
|
||||
{
|
||||
args += $" {additionalArgs}";
|
||||
}
|
||||
|
||||
return $"dotnet {args}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run tests for specific operational surfaces.
|
||||
/// </summary>
|
||||
/// <param name="testProject">Test project path or solution.</param>
|
||||
/// <param name="blastRadii">Blast radii to run tests for.</param>
|
||||
/// <param name="workingDirectory">Working directory for test execution.</param>
|
||||
/// <param name="timeoutMs">Timeout in milliseconds.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Test run result.</returns>
|
||||
public static async Task<TestRunResult> RunForBlastRadiiAsync(
|
||||
string testProject,
|
||||
string[] blastRadii,
|
||||
string? workingDirectory = null,
|
||||
int timeoutMs = 600000,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var filter = GetFilter(blastRadii);
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "dotnet",
|
||||
Arguments = $"test {testProject} --filter \"{filter}\" --logger trx --verbosity normal",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(workingDirectory))
|
||||
{
|
||||
startInfo.WorkingDirectory = workingDirectory;
|
||||
}
|
||||
|
||||
var stdout = new List<string>();
|
||||
var stderr = new List<string>();
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
using var process = new Process { StartInfo = startInfo };
|
||||
|
||||
process.OutputDataReceived += (_, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
{
|
||||
stdout.Add(e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (_, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
{
|
||||
stderr.Add(e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(timeoutMs);
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore kill errors
|
||||
}
|
||||
|
||||
return new TestRunResult(
|
||||
ExitCode: -1,
|
||||
BlastRadii: [.. blastRadii],
|
||||
Filter: filter,
|
||||
DurationMs: sw.ElapsedMilliseconds,
|
||||
Output: [.. stdout],
|
||||
Errors: [.. stderr],
|
||||
TimedOut: true);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
return new TestRunResult(
|
||||
ExitCode: process.ExitCode,
|
||||
BlastRadii: [.. blastRadii],
|
||||
Filter: filter,
|
||||
DurationMs: sw.ElapsedMilliseconds,
|
||||
Output: [.. stdout],
|
||||
Errors: [.. stderr],
|
||||
TimedOut: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run tests for a single blast radius.
|
||||
/// </summary>
|
||||
/// <param name="testProject">Test project path or solution.</param>
|
||||
/// <param name="blastRadius">Blast radius to run tests for.</param>
|
||||
/// <param name="workingDirectory">Working directory for test execution.</param>
|
||||
/// <param name="timeoutMs">Timeout in milliseconds.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Test run result.</returns>
|
||||
public static Task<TestRunResult> RunForBlastRadiusAsync(
|
||||
string testProject,
|
||||
string blastRadius,
|
||||
string? workingDirectory = null,
|
||||
int timeoutMs = 600000,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return RunForBlastRadiiAsync(testProject, [blastRadius], workingDirectory, timeoutMs, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse test results from TRX output.
|
||||
/// </summary>
|
||||
/// <param name="result">Test run result.</param>
|
||||
/// <returns>Summary of test results.</returns>
|
||||
public static TestRunSummary ParseSummary(TestRunResult result)
|
||||
{
|
||||
var summary = new TestRunSummary(
|
||||
Passed: 0,
|
||||
Failed: 0,
|
||||
Skipped: 0,
|
||||
Total: 0);
|
||||
|
||||
foreach (var line in result.Output)
|
||||
{
|
||||
// Parse dotnet test output format: "Passed: X" etc.
|
||||
if (line.Contains("Passed:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var match = System.Text.RegularExpressions.Regex.Match(line, @"Passed:\s*(\d+)");
|
||||
if (match.Success && int.TryParse(match.Groups[1].Value, out var passed))
|
||||
{
|
||||
summary = summary with { Passed = passed };
|
||||
}
|
||||
}
|
||||
|
||||
if (line.Contains("Failed:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var match = System.Text.RegularExpressions.Regex.Match(line, @"Failed:\s*(\d+)");
|
||||
if (match.Success && int.TryParse(match.Groups[1].Value, out var failed))
|
||||
{
|
||||
summary = summary with { Failed = failed };
|
||||
}
|
||||
}
|
||||
|
||||
if (line.Contains("Skipped:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var match = System.Text.RegularExpressions.Regex.Match(line, @"Skipped:\s*(\d+)");
|
||||
if (match.Success && int.TryParse(match.Groups[1].Value, out var skipped))
|
||||
{
|
||||
summary = summary with { Skipped = skipped };
|
||||
}
|
||||
}
|
||||
|
||||
if (line.Contains("Total:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var match = System.Text.RegularExpressions.Regex.Match(line, @"Total:\s*(\d+)");
|
||||
if (match.Success && int.TryParse(match.Groups[1].Value, out var total))
|
||||
{
|
||||
summary = summary with { Total = total };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of running tests for blast radii.
|
||||
/// </summary>
|
||||
/// <param name="ExitCode">Process exit code (0 = success).</param>
|
||||
/// <param name="BlastRadii">Blast radii that were tested.</param>
|
||||
/// <param name="Filter">xUnit filter that was used.</param>
|
||||
/// <param name="DurationMs">Duration of test run in milliseconds.</param>
|
||||
/// <param name="Output">Standard output lines.</param>
|
||||
/// <param name="Errors">Standard error lines.</param>
|
||||
/// <param name="TimedOut">Whether the test run timed out.</param>
|
||||
public sealed record TestRunResult(
|
||||
int ExitCode,
|
||||
ImmutableArray<string> BlastRadii,
|
||||
string Filter,
|
||||
long DurationMs,
|
||||
ImmutableArray<string> Output,
|
||||
ImmutableArray<string> Errors,
|
||||
bool TimedOut)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the test run was successful.
|
||||
/// </summary>
|
||||
public bool IsSuccess => ExitCode == 0 && !TimedOut;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary of test run results.
|
||||
/// </summary>
|
||||
/// <param name="Passed">Number of passed tests.</param>
|
||||
/// <param name="Failed">Number of failed tests.</param>
|
||||
/// <param name="Skipped">Number of skipped tests.</param>
|
||||
/// <param name="Total">Total number of tests.</param>
|
||||
public sealed record TestRunSummary(
|
||||
int Passed,
|
||||
int Failed,
|
||||
int Skipped,
|
||||
int Total);
|
||||
@@ -0,0 +1,241 @@
|
||||
// <copyright file="BlastRadiusValidator.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
// Sprint: SPRINT_20260105_002_005_TEST_cross_cutting
|
||||
// Task: CCUT-003
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
|
||||
namespace StellaOps.TestKit.BlastRadius;
|
||||
|
||||
/// <summary>
|
||||
/// Validates that tests have appropriate blast-radius annotations.
|
||||
/// </summary>
|
||||
public sealed class BlastRadiusValidator
|
||||
{
|
||||
private readonly IReadOnlyList<Type> _testClasses;
|
||||
private readonly BlastRadiusValidationConfig _config;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlastRadiusValidator"/> class.
|
||||
/// </summary>
|
||||
/// <param name="testClasses">Test classes to validate.</param>
|
||||
/// <param name="config">Validation configuration.</param>
|
||||
public BlastRadiusValidator(
|
||||
IEnumerable<Type> testClasses,
|
||||
BlastRadiusValidationConfig? config = null)
|
||||
{
|
||||
_testClasses = testClasses.ToList();
|
||||
_config = config ?? new BlastRadiusValidationConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a validator from assemblies.
|
||||
/// </summary>
|
||||
/// <param name="assemblies">Assemblies to scan for test classes.</param>
|
||||
/// <param name="config">Validation configuration.</param>
|
||||
/// <returns>BlastRadiusValidator instance.</returns>
|
||||
public static BlastRadiusValidator FromAssemblies(
|
||||
IEnumerable<Assembly> assemblies,
|
||||
BlastRadiusValidationConfig? config = null)
|
||||
{
|
||||
var testClasses = assemblies
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.Where(IsTestClass)
|
||||
.ToList();
|
||||
|
||||
return new BlastRadiusValidator(testClasses, config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate all tests that require blast-radius annotations.
|
||||
/// </summary>
|
||||
/// <returns>Validation result.</returns>
|
||||
public BlastRadiusValidationResult Validate()
|
||||
{
|
||||
var violations = new List<BlastRadiusViolation>();
|
||||
|
||||
foreach (var testClass in _testClasses)
|
||||
{
|
||||
var classTraits = GetTraits(testClass);
|
||||
|
||||
// Check if class has a category that requires blast radius
|
||||
var categories = classTraits
|
||||
.Where(t => t.Name == "Category")
|
||||
.Select(t => t.Value)
|
||||
.ToList();
|
||||
|
||||
var requiresBlastRadius = categories
|
||||
.Any(c => _config.CategoriesRequiringBlastRadius.Contains(c));
|
||||
|
||||
if (!requiresBlastRadius)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if class has blast radius annotation
|
||||
var hasBlastRadius = classTraits.Any(t => t.Name == "BlastRadius");
|
||||
|
||||
if (!hasBlastRadius)
|
||||
{
|
||||
violations.Add(new BlastRadiusViolation(
|
||||
TestClass: testClass.FullName ?? testClass.Name,
|
||||
Category: string.Join(", ", categories.Where(c => _config.CategoriesRequiringBlastRadius.Contains(c))),
|
||||
Message: $"Test class requires BlastRadius annotation because it has category: {string.Join(", ", categories.Where(c => _config.CategoriesRequiringBlastRadius.Contains(c)))}"));
|
||||
}
|
||||
}
|
||||
|
||||
return new BlastRadiusValidationResult(
|
||||
IsValid: violations.Count == 0,
|
||||
Violations: [.. violations],
|
||||
TotalTestClasses: _testClasses.Count,
|
||||
TestClassesRequiringBlastRadius: _testClasses.Count(c =>
|
||||
GetTraits(c).Any(t =>
|
||||
t.Name == "Category" &&
|
||||
_config.CategoriesRequiringBlastRadius.Contains(t.Value))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get coverage report by blast radius.
|
||||
/// </summary>
|
||||
/// <returns>Coverage report.</returns>
|
||||
public BlastRadiusCoverageReport GetCoverageReport()
|
||||
{
|
||||
var byBlastRadius = new Dictionary<string, List<string>>();
|
||||
var uncategorized = new List<string>();
|
||||
|
||||
foreach (var testClass in _testClasses)
|
||||
{
|
||||
var traits = GetTraits(testClass);
|
||||
var blastRadii = traits
|
||||
.Where(t => t.Name == "BlastRadius")
|
||||
.Select(t => t.Value)
|
||||
.ToList();
|
||||
|
||||
if (blastRadii.Count == 0)
|
||||
{
|
||||
uncategorized.Add(testClass.FullName ?? testClass.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var br in blastRadii)
|
||||
{
|
||||
if (!byBlastRadius.TryGetValue(br, out var list))
|
||||
{
|
||||
list = [];
|
||||
byBlastRadius[br] = list;
|
||||
}
|
||||
|
||||
list.Add(testClass.FullName ?? testClass.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new BlastRadiusCoverageReport(
|
||||
ByBlastRadius: byBlastRadius.ToImmutableDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value.ToImmutableArray()),
|
||||
UncategorizedTestClasses: [.. uncategorized],
|
||||
TotalTestClasses: _testClasses.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all blast radius values found in test classes.
|
||||
/// </summary>
|
||||
/// <returns>Distinct blast radius values.</returns>
|
||||
public IReadOnlyList<string> GetBlastRadiusValues()
|
||||
{
|
||||
return _testClasses
|
||||
.SelectMany(c => GetTraits(c))
|
||||
.Where(t => t.Name == "BlastRadius")
|
||||
.Select(t => t.Value)
|
||||
.Distinct()
|
||||
.OrderBy(v => v)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static bool IsTestClass(Type type)
|
||||
{
|
||||
if (!type.IsClass || type.IsAbstract)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for xUnit test methods
|
||||
return type.GetMethods()
|
||||
.Any(m => m.GetCustomAttributes()
|
||||
.Any(a => a.GetType().Name is "FactAttribute" or "TheoryAttribute"));
|
||||
}
|
||||
|
||||
private static IEnumerable<(string Name, string Value)> GetTraits(Type type)
|
||||
{
|
||||
var traitAttributes = type.GetCustomAttributes()
|
||||
.Where(a => a.GetType().Name == "TraitAttribute")
|
||||
.ToList();
|
||||
|
||||
foreach (var attr in traitAttributes)
|
||||
{
|
||||
var nameProperty = attr.GetType().GetProperty("Name");
|
||||
var valueProperty = attr.GetType().GetProperty("Value");
|
||||
|
||||
if (nameProperty != null && valueProperty != null)
|
||||
{
|
||||
var name = nameProperty.GetValue(attr)?.ToString() ?? string.Empty;
|
||||
var value = valueProperty.GetValue(attr)?.ToString() ?? string.Empty;
|
||||
yield return (name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for blast-radius validation.
|
||||
/// </summary>
|
||||
/// <param name="CategoriesRequiringBlastRadius">Categories that require blast-radius annotations.</param>
|
||||
public sealed record BlastRadiusValidationConfig(
|
||||
ImmutableArray<string> CategoriesRequiringBlastRadius = default)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the categories requiring blast-radius annotations.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> CategoriesRequiringBlastRadius { get; init; } =
|
||||
CategoriesRequiringBlastRadius.IsDefaultOrEmpty
|
||||
? [TestCategories.Integration, TestCategories.Contract, TestCategories.Security]
|
||||
: CategoriesRequiringBlastRadius;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of blast-radius validation.
|
||||
/// </summary>
|
||||
/// <param name="IsValid">Whether all tests pass validation.</param>
|
||||
/// <param name="Violations">List of violations found.</param>
|
||||
/// <param name="TotalTestClasses">Total number of test classes examined.</param>
|
||||
/// <param name="TestClassesRequiringBlastRadius">Number of test classes that require blast-radius.</param>
|
||||
public sealed record BlastRadiusValidationResult(
|
||||
bool IsValid,
|
||||
ImmutableArray<BlastRadiusViolation> Violations,
|
||||
int TotalTestClasses,
|
||||
int TestClassesRequiringBlastRadius);
|
||||
|
||||
/// <summary>
|
||||
/// A blast-radius validation violation.
|
||||
/// </summary>
|
||||
/// <param name="TestClass">Test class with violation.</param>
|
||||
/// <param name="Category">Category requiring blast-radius.</param>
|
||||
/// <param name="Message">Violation message.</param>
|
||||
public sealed record BlastRadiusViolation(
|
||||
string TestClass,
|
||||
string Category,
|
||||
string Message);
|
||||
|
||||
/// <summary>
|
||||
/// Coverage report by blast radius.
|
||||
/// </summary>
|
||||
/// <param name="ByBlastRadius">Test classes grouped by blast radius.</param>
|
||||
/// <param name="UncategorizedTestClasses">Test classes without blast-radius annotation.</param>
|
||||
/// <param name="TotalTestClasses">Total number of test classes.</param>
|
||||
public sealed record BlastRadiusCoverageReport(
|
||||
ImmutableDictionary<string, ImmutableArray<string>> ByBlastRadius,
|
||||
ImmutableArray<string> UncategorizedTestClasses,
|
||||
int TotalTestClasses);
|
||||
@@ -128,4 +128,94 @@ public static class TestCategories
|
||||
/// Storage migration tests: Schema migrations, versioning, idempotent migration application.
|
||||
/// </summary>
|
||||
public const string StorageMigration = "StorageMigration";
|
||||
|
||||
// =========================================================================
|
||||
// Blast-Radius annotations - operational surfaces affected by test failures
|
||||
// Use these to enable targeted test runs during incidents
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Blast-radius annotations for operational surfaces.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Usage with xUnit:
|
||||
/// <code>
|
||||
/// [Fact]
|
||||
/// [Trait("Category", TestCategories.Integration)]
|
||||
/// [Trait("BlastRadius", TestCategories.BlastRadius.Auth)]
|
||||
/// [Trait("BlastRadius", TestCategories.BlastRadius.Api)]
|
||||
/// public async Task TestTokenValidation() { }
|
||||
/// </code>
|
||||
///
|
||||
/// Filter by blast radius during test runs:
|
||||
/// <code>
|
||||
/// dotnet test --filter "BlastRadius=Auth|BlastRadius=Api"
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public static class BlastRadius
|
||||
{
|
||||
/// <summary>
|
||||
/// Authentication, authorization, identity, tokens, sessions.
|
||||
/// </summary>
|
||||
public const string Auth = "Auth";
|
||||
|
||||
/// <summary>
|
||||
/// SBOM generation, vulnerability scanning, reachability analysis.
|
||||
/// </summary>
|
||||
public const string Scanning = "Scanning";
|
||||
|
||||
/// <summary>
|
||||
/// Attestation, evidence storage, audit trails, proof chains.
|
||||
/// </summary>
|
||||
public const string Evidence = "Evidence";
|
||||
|
||||
/// <summary>
|
||||
/// Regulatory compliance, GDPR, data retention, audit logging.
|
||||
/// </summary>
|
||||
public const string Compliance = "Compliance";
|
||||
|
||||
/// <summary>
|
||||
/// Advisory ingestion, VEX processing, feed synchronization.
|
||||
/// </summary>
|
||||
public const string Advisories = "Advisories";
|
||||
|
||||
/// <summary>
|
||||
/// Risk scoring, policy evaluation, verdicts.
|
||||
/// </summary>
|
||||
public const string RiskPolicy = "RiskPolicy";
|
||||
|
||||
/// <summary>
|
||||
/// Cryptographic operations, signing, verification, key management.
|
||||
/// </summary>
|
||||
public const string Crypto = "Crypto";
|
||||
|
||||
/// <summary>
|
||||
/// External integrations, webhooks, notifications.
|
||||
/// </summary>
|
||||
public const string Integrations = "Integrations";
|
||||
|
||||
/// <summary>
|
||||
/// Data persistence, database operations, storage.
|
||||
/// </summary>
|
||||
public const string Persistence = "Persistence";
|
||||
|
||||
/// <summary>
|
||||
/// API surface, contract compatibility, endpoint behavior.
|
||||
/// </summary>
|
||||
public const string Api = "Api";
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Schema evolution categories
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Schema evolution tests: Backward/forward compatibility across schema versions.
|
||||
/// </summary>
|
||||
public const string SchemaEvolution = "SchemaEvolution";
|
||||
|
||||
/// <summary>
|
||||
/// Config-diff tests: Behavioral delta tests for configuration changes.
|
||||
/// </summary>
|
||||
public const string ConfigDiff = "ConfigDiff";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user