tests fixes and sprints work

This commit is contained in:
master
2026-01-22 19:08:46 +02:00
parent c32fff8f86
commit 726d70dc7f
881 changed files with 134434 additions and 6228 deletions

View File

@@ -51,7 +51,9 @@ public sealed class BinaryAnalysisDoctorPlugin : IDoctorPlugin
new DebuginfodAvailabilityCheck(),
new DdebRepoEnabledCheck(),
new BuildinfoCacheCheck(),
new SymbolRecoveryFallbackCheck()
new SymbolRecoveryFallbackCheck(),
new CorpusMirrorFreshnessCheck(),
new KpiBaselineExistsCheck()
};
}

View File

@@ -0,0 +1,324 @@
// -----------------------------------------------------------------------------
// CorpusMirrorFreshnessCheck.cs
// Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
// Task: GCB-004 - Doctor checks for ground-truth corpus health
// Description: Verify local corpus mirrors are not stale (configurable threshold)
// -----------------------------------------------------------------------------
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Checks;
/// <summary>
/// Verifies that local corpus mirrors are not stale.
/// Checks the last modification time of key mirror directories against a configurable threshold.
/// </summary>
public sealed class CorpusMirrorFreshnessCheck : IDoctorCheck
{
private const string PluginId = "stellaops.doctor.binaryanalysis";
private const string CategoryName = "Security";
// Default directories for corpus mirrors
private const string DefaultMirrorsRoot = "/var/lib/stella/mirrors";
private const string ConfigMirrorsRootKey = "BinaryAnalysis:Corpus:MirrorsDirectory";
private const string ConfigStaleThresholdKey = "BinaryAnalysis:Corpus:StalenessThresholdDays";
private const int DefaultStaleThresholdDays = 7;
// Known mirror subdirectories to check
private static readonly string[] MirrorSubdirectories =
[
"debian/archive",
"debian/snapshot",
"ubuntu/usn-index",
"alpine/secdb",
"osv"
];
/// <inheritdoc />
public string CheckId => "check.binaryanalysis.corpus.mirror.freshness";
/// <inheritdoc />
public string Name => "Corpus Mirror Freshness";
/// <inheritdoc />
public string Description => "Verify local corpus mirrors are not stale (configurable threshold)";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["binaryanalysis", "corpus", "mirrors", "freshness", "security", "groundtruth"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
// Always run - corpus mirrors should be maintained
return true;
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var builder = context.CreateResult(CheckId, PluginId, CategoryName);
// Get configuration
var mirrorsRoot = context.Configuration[ConfigMirrorsRootKey] ?? DefaultMirrorsRoot;
var staleThresholdDaysStr = context.Configuration[ConfigStaleThresholdKey];
var staleThresholdDays = int.TryParse(staleThresholdDaysStr, out var days) ? days : DefaultStaleThresholdDays;
var staleThreshold = TimeSpan.FromDays(staleThresholdDays);
// Check if mirrors root exists
if (!Directory.Exists(mirrorsRoot))
{
return Task.FromResult(builder
.Warn($"Corpus mirrors directory does not exist: {mirrorsRoot}")
.WithEvidence("Mirror Status", eb =>
{
eb.Add("mirrors_root", mirrorsRoot);
eb.Add("exists", false);
eb.Add("stale_threshold_days", staleThresholdDays);
})
.WithCauses(
"Corpus mirrors have not been initialized",
"Mirror directory path misconfigured",
"Air-gapped setup incomplete")
.WithRemediation(rb => rb
.AddShellStep(1, "Create mirrors directory",
$"sudo mkdir -p {mirrorsRoot}")
.AddStellaStep(2, "Initialize corpus mirrors",
"groundtruth mirror sync --all")
.AddManualStep(3, "For air-gapped environments",
"Copy pre-populated mirrors from an online system to the mirrors directory"))
.WithVerification($"stella doctor --check {CheckId}")
.Build());
}
// Check each mirror subdirectory
var mirrorStatuses = new List<MirrorStatus>();
var now = context.TimeProvider.GetUtcNow();
foreach (var subdir in MirrorSubdirectories)
{
var mirrorPath = Path.Combine(mirrorsRoot, subdir);
var status = CheckMirrorDirectory(mirrorPath, now, staleThreshold);
mirrorStatuses.Add(status);
}
// Analyze results
var existingMirrors = mirrorStatuses.Where(m => m.Exists).ToList();
var staleMirrors = existingMirrors.Where(m => m.IsStale).ToList();
var freshMirrors = existingMirrors.Where(m => !m.IsStale).ToList();
var missingMirrors = mirrorStatuses.Where(m => !m.Exists).ToList();
// Build evidence
void AddMirrorEvidence(Plugins.Builders.EvidenceBuilder eb)
{
eb.Add("mirrors_root", mirrorsRoot);
eb.Add("stale_threshold_days", staleThresholdDays);
eb.Add("total_mirrors", mirrorStatuses.Count);
eb.Add("existing_mirrors", existingMirrors.Count);
eb.Add("fresh_mirrors", freshMirrors.Count);
eb.Add("stale_mirrors", staleMirrors.Count);
eb.Add("missing_mirrors", missingMirrors.Count);
for (var i = 0; i < mirrorStatuses.Count; i++)
{
var m = mirrorStatuses[i];
var prefix = $"mirror_{i + 1}";
eb.Add($"{prefix}_path", m.Name);
eb.Add($"{prefix}_exists", m.Exists);
if (m.Exists)
{
eb.Add($"{prefix}_last_modified", m.LastModified?.ToString("O") ?? "unknown");
eb.Add($"{prefix}_age_days", m.AgeDays);
eb.Add($"{prefix}_is_stale", m.IsStale);
}
}
}
// No mirrors exist
if (existingMirrors.Count == 0)
{
return Task.FromResult(builder
.Fail("No corpus mirrors found - binary analysis will have degraded symbol recovery")
.WithEvidence("Mirror Status", AddMirrorEvidence)
.WithCauses(
"Corpus mirrors have not been synchronized",
"Mirror sync job has not been run",
"Air-gapped setup incomplete")
.WithRemediation(rb => rb
.AddStellaStep(1, "Initialize all corpus mirrors",
"groundtruth mirror sync --all")
.AddShellStep(2, "Or sync specific mirrors",
"stella groundtruth mirror sync --source debian")
.AddManualStep(3, "For air-gapped environments",
"Transfer pre-populated mirrors from an online system"))
.WithVerification($"stella doctor --check {CheckId}")
.Build());
}
// All existing mirrors are stale
if (staleMirrors.Count > 0 && freshMirrors.Count == 0)
{
var staleNames = string.Join(", ", staleMirrors.Select(m => m.Name));
return Task.FromResult(builder
.Fail($"All existing corpus mirrors are stale (older than {staleThresholdDays} days)")
.WithEvidence("Mirror Status", AddMirrorEvidence)
.WithCauses(
"Mirror sync job has not run recently",
"Scheduled sync task is disabled or failing",
"Network connectivity issues preventing sync")
.WithRemediation(rb => rb
.AddStellaStep(1, "Update all mirrors",
"groundtruth mirror sync --all")
.AddShellStep(2, "Check mirror sync job status",
"systemctl status stella-mirror-sync.timer")
.AddManualStep(3, "Set up automatic mirror sync",
$"Configure a cron job or systemd timer to run 'stella groundtruth mirror sync' at least every {staleThresholdDays} days"))
.WithVerification($"stella doctor --check {CheckId}")
.Build());
}
// Some mirrors are stale
if (staleMirrors.Count > 0)
{
var staleNames = string.Join(", ", staleMirrors.Select(m => m.Name));
return Task.FromResult(builder
.Warn($"{staleMirrors.Count}/{existingMirrors.Count} mirrors are stale: {staleNames}")
.WithEvidence("Mirror Status", AddMirrorEvidence)
.WithCauses(
"Some mirror sync operations are failing",
"Partial network connectivity issues",
"Selective mirror sync configured")
.WithRemediation(rb => rb
.AddStellaStep(1, "Sync stale mirrors",
$"groundtruth mirror sync --sources {string.Join(",", staleMirrors.Select(m => m.Name.Split('/')[0]))}")
.AddShellStep(2, "Check sync logs for errors",
"journalctl -u stella-mirror-sync --since '7 days ago' | grep -i error"))
.WithVerification($"stella doctor --check {CheckId}")
.Build());
}
// Some mirrors are missing
if (missingMirrors.Count > 0)
{
var missingNames = string.Join(", ", missingMirrors.Select(m => m.Name));
return Task.FromResult(builder
.Info($"All existing mirrors are fresh; {missingMirrors.Count} optional mirrors not configured: {missingNames}")
.WithEvidence("Mirror Status", AddMirrorEvidence)
.WithRemediation(rb => rb
.AddManualStep(1, "Optionally add missing mirrors",
$"stella groundtruth mirror sync --sources {string.Join(",", missingMirrors.Select(m => m.Name.Split('/')[0]))}"))
.WithVerification($"stella doctor --check {CheckId}")
.Build());
}
// All mirrors are fresh
return Task.FromResult(builder
.Pass($"All {freshMirrors.Count} corpus mirrors are fresh (last updated within {staleThresholdDays} days)")
.WithEvidence("Mirror Status", AddMirrorEvidence)
.WithVerification($"stella doctor --check {CheckId}")
.Build());
}
/// <summary>
/// Checks the status of a single mirror directory.
/// </summary>
private static MirrorStatus CheckMirrorDirectory(string path, DateTimeOffset now, TimeSpan staleThreshold)
{
var status = new MirrorStatus
{
Path = path,
Name = Path.GetFileName(Path.GetDirectoryName(path)) + "/" + Path.GetFileName(path)
};
try
{
if (!Directory.Exists(path))
{
status.Exists = false;
return status;
}
status.Exists = true;
// Get the most recent modification time of any file in the directory
var mostRecentModification = GetMostRecentModification(path);
if (mostRecentModification.HasValue)
{
status.LastModified = mostRecentModification.Value;
var age = now - mostRecentModification.Value;
status.AgeDays = (int)age.TotalDays;
status.IsStale = age > staleThreshold;
}
else
{
// Directory exists but is empty - consider it stale
status.IsStale = true;
}
}
catch (UnauthorizedAccessException)
{
// Cannot access directory - mark as stale
status.Exists = true;
status.IsStale = true;
}
catch (IOException)
{
// IO error - mark as stale
status.Exists = true;
status.IsStale = true;
}
return status;
}
/// <summary>
/// Gets the most recent modification time of any file in the directory (recursive).
/// Limited to first 1000 files to avoid performance issues.
/// </summary>
private static DateTimeOffset? GetMostRecentModification(string path)
{
try
{
// First try to use the directory's own modification time
var dirInfo = new DirectoryInfo(path);
var dirModTime = dirInfo.LastWriteTimeUtc;
// Also check a sample of files for more accurate staleness detection
var files = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
.Take(1000)
.ToList();
if (files.Count == 0)
{
// Empty directory - use directory modification time
return new DateTimeOffset(dirModTime, TimeSpan.Zero);
}
var mostRecent = files
.Select(f => new FileInfo(f).LastWriteTimeUtc)
.Max();
return new DateTimeOffset(mostRecent, TimeSpan.Zero);
}
catch
{
return null;
}
}
private sealed record MirrorStatus
{
public required string Path { get; init; }
public required string Name { get; init; }
public bool Exists { get; set; }
public DateTimeOffset? LastModified { get; set; }
public int AgeDays { get; set; }
public bool IsStale { get; set; }
}
}

View File

@@ -0,0 +1,377 @@
// -----------------------------------------------------------------------------
// KpiBaselineExistsCheck.cs
// Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
// Task: GCB-004 - Doctor checks for ground-truth corpus health
// Description: Verify KPI baseline file exists for regression detection
// -----------------------------------------------------------------------------
using System.Text.Json;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Checks;
/// <summary>
/// Verifies that a KPI baseline file exists for regression detection.
/// Without a baseline, CI gates cannot detect KPI regressions in binary matching accuracy.
/// </summary>
public sealed class KpiBaselineExistsCheck : IDoctorCheck
{
private const string PluginId = "stellaops.doctor.binaryanalysis";
private const string CategoryName = "Security";
// Default baseline file location
private const string DefaultBaselineDirectory = "/var/lib/stella/baselines";
private const string DefaultBaselineFilename = "current.json";
private const string ConfigBaselineDirectoryKey = "BinaryAnalysis:Corpus:BaselineDirectory";
private const string ConfigBaselineFilenameKey = "BinaryAnalysis:Corpus:BaselineFilename";
// Expected KPI fields in baseline file
private static readonly string[] ExpectedKpiFields =
[
"precision",
"recall",
"falseNegativeRate",
"deterministicReplayRate",
"ttfrpP95Ms"
];
/// <inheritdoc />
public string CheckId => "check.binaryanalysis.corpus.kpi.baseline";
/// <inheritdoc />
public string Name => "KPI Baseline Configuration";
/// <inheritdoc />
public string Description => "Verify KPI baseline file exists for regression detection in CI gates";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["binaryanalysis", "corpus", "kpi", "baseline", "regression", "ci", "groundtruth"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(2);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
// Always run - KPI baselines are important for regression detection
return true;
}
/// <inheritdoc />
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var builder = context.CreateResult(CheckId, PluginId, CategoryName);
// Get configuration
var baselineDir = context.Configuration[ConfigBaselineDirectoryKey] ?? DefaultBaselineDirectory;
var baselineFilename = context.Configuration[ConfigBaselineFilenameKey] ?? DefaultBaselineFilename;
var baselinePath = Path.Combine(baselineDir, baselineFilename);
// Check if baseline directory exists
if (!Directory.Exists(baselineDir))
{
return builder
.Warn($"KPI baseline directory does not exist: {baselineDir}")
.WithEvidence("Baseline Status", eb =>
{
eb.Add("baseline_directory", baselineDir);
eb.Add("baseline_filename", baselineFilename);
eb.Add("full_path", baselinePath);
eb.Add("directory_exists", false);
eb.Add("file_exists", false);
})
.WithCauses(
"KPI baseline has never been established",
"Baseline directory path misconfigured",
"First run of corpus validation not yet completed")
.WithRemediation(rb => rb
.AddShellStep(1, "Create baseline directory",
$"sudo mkdir -p {baselineDir}")
.AddStellaStep(2, "Run corpus validation to establish baseline",
"groundtruth validate run --corpus datasets/golden-corpus/seed/ --output-baseline")
.AddStellaStep(3, "Or manually set the current results as baseline",
$"groundtruth baseline update --from-latest --output {baselinePath}"))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
// Check if baseline file exists
if (!File.Exists(baselinePath))
{
// Check for any baseline files in the directory
var existingBaselines = TryGetExistingBaselines(baselineDir);
if (existingBaselines.Count > 0)
{
var latest = existingBaselines.OrderByDescending(b => b.ModifiedAt).First();
return builder
.Warn($"Default baseline file not found, but {existingBaselines.Count} other baseline file(s) exist")
.WithEvidence("Baseline Status", eb =>
{
eb.Add("baseline_directory", baselineDir);
eb.Add("baseline_filename", baselineFilename);
eb.Add("full_path", baselinePath);
eb.Add("directory_exists", true);
eb.Add("file_exists", false);
eb.Add("other_baselines_found", existingBaselines.Count);
eb.Add("latest_baseline", latest.Filename);
eb.Add("latest_baseline_date", latest.ModifiedAt.ToString("O"));
})
.WithCauses(
"Baseline file was renamed or moved",
"Configuration points to wrong filename",
"Latest baseline not yet promoted to 'current'")
.WithRemediation(rb => rb
.AddShellStep(1, "Copy latest baseline to default location",
$"cp {Path.Combine(baselineDir, latest.Filename)} {baselinePath}")
.AddManualStep(2, "Or update configuration to use existing baseline",
$"Set BinaryAnalysis:Corpus:BaselineFilename to '{latest.Filename}'"))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
return builder
.Warn($"No KPI baseline file found at {baselinePath}")
.WithEvidence("Baseline Status", eb =>
{
eb.Add("baseline_directory", baselineDir);
eb.Add("baseline_filename", baselineFilename);
eb.Add("full_path", baselinePath);
eb.Add("directory_exists", true);
eb.Add("file_exists", false);
eb.Add("other_baselines_found", 0);
})
.WithCauses(
"KPI baseline has never been established",
"First run of corpus validation not yet completed",
"Baseline file was deleted")
.WithRemediation(rb => rb
.AddStellaStep(1, "Run corpus validation to establish baseline",
$"groundtruth validate run --corpus datasets/golden-corpus/seed/ --output {baselinePath}")
.AddStellaStep(2, "Or update baseline from existing validation results",
$"groundtruth baseline update --from-latest --output {baselinePath}"))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
// File exists - validate its contents
var validationResult = await ValidateBaselineFileAsync(baselinePath, ct);
if (!validationResult.IsValid)
{
return builder
.Fail($"KPI baseline file exists but is invalid: {validationResult.Error}")
.WithEvidence("Baseline Status", eb =>
{
eb.Add("baseline_directory", baselineDir);
eb.Add("baseline_filename", baselineFilename);
eb.Add("full_path", baselinePath);
eb.Add("directory_exists", true);
eb.Add("file_exists", true);
eb.Add("is_valid_json", validationResult.IsValidJson);
eb.Add("validation_error", validationResult.Error ?? "unknown");
eb.Add("file_size_bytes", validationResult.FileSizeBytes);
eb.Add("last_modified", validationResult.LastModified?.ToString("O") ?? "unknown");
})
.WithCauses(
"Baseline file is corrupted",
"Baseline file has invalid JSON format",
"Baseline file is missing required KPI fields",
"File was truncated or partially written")
.WithRemediation(rb => rb
.AddShellStep(1, "Back up corrupted baseline",
$"mv {baselinePath} {baselinePath}.corrupted.$(date +%Y%m%d)")
.AddStellaStep(2, "Regenerate baseline from latest validation",
$"groundtruth baseline update --from-latest --output {baselinePath}")
.AddShellStep(3, "Or validate JSON manually",
$"cat {baselinePath} | jq ."))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
// Check for missing KPI fields
if (validationResult.MissingFields.Count > 0)
{
var missing = string.Join(", ", validationResult.MissingFields);
return builder
.Warn($"KPI baseline is missing {validationResult.MissingFields.Count} recommended fields: {missing}")
.WithEvidence("Baseline Status", eb =>
{
eb.Add("baseline_directory", baselineDir);
eb.Add("baseline_filename", baselineFilename);
eb.Add("full_path", baselinePath);
eb.Add("file_exists", true);
eb.Add("is_valid_json", true);
eb.Add("missing_fields", missing);
eb.Add("present_fields", string.Join(", ", validationResult.PresentFields));
eb.Add("file_size_bytes", validationResult.FileSizeBytes);
eb.Add("last_modified", validationResult.LastModified?.ToString("O") ?? "unknown");
AddKpiValues(eb, validationResult);
})
.WithCauses(
"Baseline was created with an older version of the validation tool",
"Some KPI metrics were not computed during baseline creation",
"Partial baseline update")
.WithRemediation(rb => rb
.AddStellaStep(1, "Regenerate complete baseline",
$"groundtruth baseline update --from-latest --output {baselinePath}"))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
// All good - baseline exists and is valid
var ageInDays = validationResult.LastModified.HasValue
? (int)(context.TimeProvider.GetUtcNow() - validationResult.LastModified.Value).TotalDays
: 0;
return builder
.Pass($"KPI baseline is configured and valid (last updated {ageInDays} days ago)")
.WithEvidence("Baseline Status", eb =>
{
eb.Add("baseline_directory", baselineDir);
eb.Add("baseline_filename", baselineFilename);
eb.Add("full_path", baselinePath);
eb.Add("file_exists", true);
eb.Add("is_valid", true);
eb.Add("file_size_bytes", validationResult.FileSizeBytes);
eb.Add("last_modified", validationResult.LastModified?.ToString("O") ?? "unknown");
eb.Add("age_days", ageInDays);
AddKpiValues(eb, validationResult);
})
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
/// <summary>
/// Tries to find existing baseline files in the directory.
/// </summary>
private static List<BaselineFileInfo> TryGetExistingBaselines(string directory)
{
var results = new List<BaselineFileInfo>();
try
{
foreach (var file in Directory.GetFiles(directory, "*.json"))
{
var info = new FileInfo(file);
results.Add(new BaselineFileInfo
{
Filename = info.Name,
ModifiedAt = new DateTimeOffset(info.LastWriteTimeUtc, TimeSpan.Zero)
});
}
}
catch
{
// Ignore errors listing directory
}
return results;
}
/// <summary>
/// Validates the baseline file contents.
/// </summary>
private static async Task<BaselineValidationResult> ValidateBaselineFileAsync(string path, CancellationToken ct)
{
var result = new BaselineValidationResult();
try
{
var fileInfo = new FileInfo(path);
result.FileSizeBytes = fileInfo.Length;
result.LastModified = new DateTimeOffset(fileInfo.LastWriteTimeUtc, TimeSpan.Zero);
if (fileInfo.Length == 0)
{
result.IsValid = false;
result.Error = "File is empty";
return result;
}
var content = await File.ReadAllTextAsync(path, ct);
using var doc = JsonDocument.Parse(content);
result.IsValidJson = true;
var root = doc.RootElement;
// Check for expected KPI fields
foreach (var field in ExpectedKpiFields)
{
if (root.TryGetProperty(field, out var value) ||
root.TryGetProperty(ToCamelCase(field), out value) ||
root.TryGetProperty(ToPascalCase(field), out value))
{
result.PresentFields.Add(field);
// Try to extract the value for evidence
if (value.ValueKind == JsonValueKind.Number)
{
result.KpiValues[field] = value.GetDouble();
}
}
else
{
result.MissingFields.Add(field);
}
}
// File is valid if it has at least some KPI fields
result.IsValid = result.PresentFields.Count > 0;
if (!result.IsValid)
{
result.Error = "No recognized KPI fields found";
}
}
catch (JsonException ex)
{
result.IsValidJson = false;
result.IsValid = false;
result.Error = $"Invalid JSON: {ex.Message}";
}
catch (IOException ex)
{
result.IsValid = false;
result.Error = $"Cannot read file: {ex.Message}";
}
return result;
}
/// <summary>
/// Adds KPI values to evidence if available.
/// </summary>
private static void AddKpiValues(Plugins.Builders.EvidenceBuilder eb, BaselineValidationResult result)
{
foreach (var (field, value) in result.KpiValues)
{
eb.Add($"kpi_{field}", value);
}
}
private static string ToCamelCase(string s) => char.ToLowerInvariant(s[0]) + s[1..];
private static string ToPascalCase(string s) => char.ToUpperInvariant(s[0]) + s[1..];
private sealed record BaselineFileInfo
{
public required string Filename { get; init; }
public required DateTimeOffset ModifiedAt { get; init; }
}
private sealed record BaselineValidationResult
{
public bool IsValid { get; set; }
public bool IsValidJson { get; set; }
public string? Error { get; set; }
public long FileSizeBytes { get; set; }
public DateTimeOffset? LastModified { get; set; }
public List<string> PresentFields { get; } = [];
public List<string> MissingFields { get; } = [];
public Dictionary<string, double> KpiValues { get; } = [];
}
}

View File

@@ -0,0 +1,253 @@
// -----------------------------------------------------------------------------
// CorpusMirrorFreshnessCheckTests.cs
// Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
// Task: GCB-004 - Doctor checks for ground-truth corpus health
// Description: Unit tests for CorpusMirrorFreshnessCheck
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugin.BinaryAnalysis.Checks;
using StellaOps.Doctor.Plugins;
using Xunit;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Tests.Checks;
[Trait("Category", "Unit")]
public class CorpusMirrorFreshnessCheckTests : IDisposable
{
private readonly string _tempDir;
private readonly CorpusMirrorFreshnessCheck _check = new();
private readonly FakeTimeProvider _timeProvider;
public CorpusMirrorFreshnessCheckTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"corpus-mirror-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 22, 12, 0, 0, TimeSpan.Zero));
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
[Fact]
public void CheckId_ReturnsExpectedValue()
{
// Assert
_check.CheckId.Should().Be("check.binaryanalysis.corpus.mirror.freshness");
}
[Fact]
public void Name_ReturnsCorpusMirrorFreshness()
{
// Assert
_check.Name.Should().Be("Corpus Mirror Freshness");
}
[Fact]
public void DefaultSeverity_IsWarn()
{
// Assert
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public void Tags_ContainsExpectedValues()
{
// Assert
_check.Tags.Should().Contain("binaryanalysis");
_check.Tags.Should().Contain("corpus");
_check.Tags.Should().Contain("mirrors");
_check.Tags.Should().Contain("groundtruth");
}
[Fact]
public void CanRun_ReturnsTrue_Always()
{
// Arrange
var context = CreateContext(_tempDir);
// Act & Assert
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_ReturnsWarn_WhenMirrorsDirectoryDoesNotExist()
{
// Arrange
var nonExistentDir = Path.Combine(_tempDir, "nonexistent");
var context = CreateContext(nonExistentDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("does not exist");
}
[Fact]
public async Task RunAsync_ReturnsFail_WhenNoMirrorsExist()
{
// Arrange - directory exists but no mirrors
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("No corpus mirrors found");
}
[Fact]
public async Task RunAsync_ReturnsPass_WhenAllMirrorsAreFresh()
{
// Arrange - create fresh mirrors
CreateMirrorWithFiles(_tempDir, "debian/archive", daysOld: 1);
CreateMirrorWithFiles(_tempDir, "debian/snapshot", daysOld: 2);
CreateMirrorWithFiles(_tempDir, "ubuntu/usn-index", daysOld: 3);
CreateMirrorWithFiles(_tempDir, "alpine/secdb", daysOld: 4);
CreateMirrorWithFiles(_tempDir, "osv", daysOld: 5);
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("fresh");
}
[Fact]
public async Task RunAsync_ReturnsFail_WhenAllMirrorsAreStale()
{
// Arrange - create stale mirrors (> 7 days old by default)
CreateMirrorWithFiles(_tempDir, "debian/archive", daysOld: 10);
CreateMirrorWithFiles(_tempDir, "debian/snapshot", daysOld: 15);
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("stale");
}
[Fact]
public async Task RunAsync_ReturnsWarn_WhenSomeMirrorsAreStale()
{
// Arrange - mix of fresh and stale mirrors
CreateMirrorWithFiles(_tempDir, "debian/archive", daysOld: 2); // Fresh
CreateMirrorWithFiles(_tempDir, "debian/snapshot", daysOld: 10); // Stale
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("stale");
}
[Fact]
public async Task RunAsync_RespectsConfiguredThreshold()
{
// Arrange - create mirror that is 5 days old
CreateMirrorWithFiles(_tempDir, "debian/archive", daysOld: 5);
// Configure threshold to 3 days (so 5 days is stale)
var context = CreateContext(_tempDir, staleThresholdDays: 3);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("stale");
}
[Fact]
public async Task RunAsync_IncludesVerificationCommand()
{
// Arrange
CreateMirrorWithFiles(_tempDir, "debian/archive", daysOld: 2);
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.VerificationCommand.Should().NotBeNullOrEmpty();
result.VerificationCommand.Should().Contain("stella doctor --check");
result.VerificationCommand.Should().Contain(_check.CheckId);
}
[Fact]
public async Task RunAsync_IncludesRemediationSteps_OnFailure()
{
// Arrange - no mirrors
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Remediation.Should().NotBeNull();
result.Remediation!.Steps.Should().NotBeEmpty();
}
private DoctorPluginContext CreateContext(string mirrorsRoot, int? staleThresholdDays = null)
{
var configValues = new Dictionary<string, string?>
{
["BinaryAnalysis:Corpus:MirrorsDirectory"] = mirrorsRoot
};
if (staleThresholdDays.HasValue)
{
configValues["BinaryAnalysis:Corpus:StalenessThresholdDays"] = staleThresholdDays.Value.ToString();
}
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configValues)
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = _timeProvider,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
private void CreateMirrorWithFiles(string root, string subdir, int daysOld)
{
var mirrorPath = Path.Combine(root, subdir);
Directory.CreateDirectory(mirrorPath);
// Create some test files
var testFile = Path.Combine(mirrorPath, "test-data.json");
File.WriteAllText(testFile, "{}");
// Set modification time
var modTime = _timeProvider.GetUtcNow().AddDays(-daysOld).DateTime;
File.SetLastWriteTimeUtc(testFile, modTime);
}
}

View File

@@ -0,0 +1,340 @@
// -----------------------------------------------------------------------------
// KpiBaselineExistsCheckTests.cs
// Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
// Task: GCB-004 - Doctor checks for ground-truth corpus health
// Description: Unit tests for KpiBaselineExistsCheck
// -----------------------------------------------------------------------------
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugin.BinaryAnalysis.Checks;
using StellaOps.Doctor.Plugins;
using Xunit;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Tests.Checks;
[Trait("Category", "Unit")]
public class KpiBaselineExistsCheckTests : IDisposable
{
private readonly string _tempDir;
private readonly KpiBaselineExistsCheck _check = new();
private readonly FakeTimeProvider _timeProvider;
public KpiBaselineExistsCheckTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"kpi-baseline-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 22, 12, 0, 0, TimeSpan.Zero));
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
[Fact]
public void CheckId_ReturnsExpectedValue()
{
// Assert
_check.CheckId.Should().Be("check.binaryanalysis.corpus.kpi.baseline");
}
[Fact]
public void Name_ReturnsKpiBaselineConfiguration()
{
// Assert
_check.Name.Should().Be("KPI Baseline Configuration");
}
[Fact]
public void DefaultSeverity_IsWarn()
{
// Assert
_check.DefaultSeverity.Should().Be(DoctorSeverity.Warn);
}
[Fact]
public void Tags_ContainsExpectedValues()
{
// Assert
_check.Tags.Should().Contain("binaryanalysis");
_check.Tags.Should().Contain("corpus");
_check.Tags.Should().Contain("kpi");
_check.Tags.Should().Contain("baseline");
_check.Tags.Should().Contain("groundtruth");
}
[Fact]
public void CanRun_ReturnsTrue_Always()
{
// Arrange
var context = CreateContext(_tempDir);
// Act & Assert
_check.CanRun(context).Should().BeTrue();
}
[Fact]
public async Task RunAsync_ReturnsWarn_WhenBaselineDirectoryDoesNotExist()
{
// Arrange
var nonExistentDir = Path.Combine(_tempDir, "nonexistent");
var context = CreateContext(nonExistentDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("does not exist");
}
[Fact]
public async Task RunAsync_ReturnsWarn_WhenBaselineFileDoesNotExist()
{
// Arrange - directory exists but no baseline file
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("No KPI baseline file found");
}
[Fact]
public async Task RunAsync_ReturnsWarn_WhenOtherBaselineFilesExist()
{
// Arrange - create a different baseline file
var otherBaseline = Path.Combine(_tempDir, "baseline-20260120.json");
File.WriteAllText(otherBaseline, CreateValidBaselineJson());
var context = CreateContext(_tempDir); // Will look for current.json
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("other baseline file");
}
[Fact]
public async Task RunAsync_ReturnsFail_WhenBaselineFileIsEmpty()
{
// Arrange
var baselinePath = Path.Combine(_tempDir, "current.json");
File.WriteAllText(baselinePath, "");
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("invalid");
}
[Fact]
public async Task RunAsync_ReturnsFail_WhenBaselineFileHasInvalidJson()
{
// Arrange
var baselinePath = Path.Combine(_tempDir, "current.json");
File.WriteAllText(baselinePath, "{ invalid json }");
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("invalid");
result.Diagnosis.Should().Contain("JSON");
}
[Fact]
public async Task RunAsync_ReturnsFail_WhenBaselineFileHasNoKpiFields()
{
// Arrange
var baselinePath = Path.Combine(_tempDir, "current.json");
File.WriteAllText(baselinePath, "{ \"unrelated\": \"data\" }");
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Fail);
result.Diagnosis.Should().Contain("invalid");
}
[Fact]
public async Task RunAsync_ReturnsWarn_WhenSomeKpiFieldsAreMissing()
{
// Arrange - partial baseline with only some fields
var baselinePath = Path.Combine(_tempDir, "current.json");
var partialBaseline = new
{
precision = 0.95,
recall = 0.92
// Missing: falseNegativeRate, deterministicReplayRate, ttfrpP95Ms
};
File.WriteAllText(baselinePath, JsonSerializer.Serialize(partialBaseline));
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Warn);
result.Diagnosis.Should().Contain("missing");
}
[Fact]
public async Task RunAsync_ReturnsPass_WhenBaselineIsValid()
{
// Arrange
var baselinePath = Path.Combine(_tempDir, "current.json");
File.WriteAllText(baselinePath, CreateValidBaselineJson());
// Set file modification time to 2 days ago
File.SetLastWriteTimeUtc(baselinePath, _timeProvider.GetUtcNow().AddDays(-2).DateTime);
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
result.Diagnosis.Should().Contain("valid");
result.Diagnosis.Should().Contain("2 days ago");
}
[Fact]
public async Task RunAsync_AcceptsCamelCaseKpiFields()
{
// Arrange
var baselinePath = Path.Combine(_tempDir, "current.json");
var baseline = new
{
precision = 0.95,
recall = 0.92,
falseNegativeRate = 0.08,
deterministicReplayRate = 1.0,
ttfrpP95Ms = 150
};
File.WriteAllText(baselinePath, JsonSerializer.Serialize(baseline));
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
}
[Fact]
public async Task RunAsync_IncludesVerificationCommand()
{
// Arrange
var baselinePath = Path.Combine(_tempDir, "current.json");
File.WriteAllText(baselinePath, CreateValidBaselineJson());
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.VerificationCommand.Should().NotBeNullOrEmpty();
result.VerificationCommand.Should().Contain("stella doctor --check");
result.VerificationCommand.Should().Contain(_check.CheckId);
}
[Fact]
public async Task RunAsync_IncludesRemediationSteps_OnFailure()
{
// Arrange - no baseline
var context = CreateContext(_tempDir);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Remediation.Should().NotBeNull();
result.Remediation!.Steps.Should().NotBeEmpty();
}
[Fact]
public async Task RunAsync_RespectsCustomFilename()
{
// Arrange
var customFilename = "my-baseline.json";
var baselinePath = Path.Combine(_tempDir, customFilename);
File.WriteAllText(baselinePath, CreateValidBaselineJson());
var context = CreateContext(_tempDir, customFilename);
// Act
var result = await _check.RunAsync(context, CancellationToken.None);
// Assert
result.Severity.Should().Be(DoctorSeverity.Pass);
}
private DoctorPluginContext CreateContext(string baselineDir, string? baselineFilename = null)
{
var configValues = new Dictionary<string, string?>
{
["BinaryAnalysis:Corpus:BaselineDirectory"] = baselineDir
};
if (baselineFilename != null)
{
configValues["BinaryAnalysis:Corpus:BaselineFilename"] = baselineFilename;
}
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configValues)
.Build();
return new DoctorPluginContext
{
Services = new ServiceCollection().BuildServiceProvider(),
Configuration = config,
TimeProvider = _timeProvider,
Logger = NullLogger.Instance,
EnvironmentName = "Test",
PluginConfig = config.GetSection("Doctor:Plugins")
};
}
private static string CreateValidBaselineJson()
{
var baseline = new
{
precision = 0.95,
recall = 0.92,
falseNegativeRate = 0.08,
deterministicReplayRate = 1.0,
ttfrpP95Ms = 150,
timestamp = "2026-01-20T10:00:00Z"
};
return JsonSerializer.Serialize(baseline);
}
}

View File

@@ -0,0 +1,523 @@
// -----------------------------------------------------------------------------
// CorpusHealthChecksIntegrationTests.cs
// Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
// Task: GCB-004 - Integration test with mock services
// Description: Integration tests for corpus health Doctor checks with mock services
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using StellaOps.Doctor.Plugin.BinaryAnalysis.Checks;
using StellaOps.Doctor.Plugin.BinaryAnalysis.Options;
using Xunit;
namespace StellaOps.Doctor.Plugin.BinaryAnalysis.Tests.Integration;
/// <summary>
/// Integration tests for ground-truth corpus Doctor checks.
/// These tests verify the health checks work correctly with mock services
/// simulating various infrastructure states.
/// </summary>
public sealed class CorpusHealthChecksIntegrationTests : IDisposable
{
private readonly string _testOutputDir;
private readonly IServiceProvider _serviceProvider;
public CorpusHealthChecksIntegrationTests()
{
_testOutputDir = Path.Combine(Path.GetTempPath(), $"corpus-health-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_testOutputDir);
_serviceProvider = BuildServiceProvider();
}
#region DebuginfodAvailabilityCheck Tests
[Fact]
public async Task DebuginfodAvailabilityCheck_AllUrlsReachable_ReturnsPass()
{
// Arrange
var httpClient = CreateMockHttpClient(HttpStatusCode.OK);
var options = CreateDebuginfodOptions(["https://debuginfod.fedora.org", "https://debuginfod.ubuntu.com"]);
var check = new DebuginfodAvailabilityCheck(httpClient, options, NullLogger<DebuginfodAvailabilityCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Pass);
result.Message.Should().Contain("reachable");
}
[Fact]
public async Task DebuginfodAvailabilityCheck_SomeUrlsUnreachable_ReturnsWarn()
{
// Arrange
var httpClient = CreateMockHttpClient(statusByUrl: new Dictionary<string, HttpStatusCode>
{
["https://debuginfod.fedora.org"] = HttpStatusCode.OK,
["https://debuginfod.ubuntu.com"] = HttpStatusCode.ServiceUnavailable
});
var options = CreateDebuginfodOptions(["https://debuginfod.fedora.org", "https://debuginfod.ubuntu.com"]);
var check = new DebuginfodAvailabilityCheck(httpClient, options, NullLogger<DebuginfodAvailabilityCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Warn);
result.Message.Should().Contain("unreachable");
}
[Fact]
public async Task DebuginfodAvailabilityCheck_AllUrlsUnreachable_ReturnsFail()
{
// Arrange
var httpClient = CreateMockHttpClient(HttpStatusCode.ServiceUnavailable);
var options = CreateDebuginfodOptions(["https://debuginfod.fedora.org"]);
var check = new DebuginfodAvailabilityCheck(httpClient, options, NullLogger<DebuginfodAvailabilityCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Fail);
}
#endregion
#region CorpusMirrorFreshnessCheck Tests
[Fact]
public async Task CorpusMirrorFreshnessCheck_FreshMirrors_ReturnsPass()
{
// Arrange
SetupFreshMirrors();
var options = CreateMirrorOptions(_testOutputDir);
var check = new CorpusMirrorFreshnessCheck(options, NullLogger<CorpusMirrorFreshnessCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Pass);
result.Message.Should().Contain("fresh");
}
[Fact]
public async Task CorpusMirrorFreshnessCheck_StaleMirrors_ReturnsWarn()
{
// Arrange
SetupStaleMirrors(daysOld: 5); // Within warning threshold
var options = CreateMirrorOptions(_testOutputDir, staleDays: 7, warnDays: 3);
var check = new CorpusMirrorFreshnessCheck(options, NullLogger<CorpusMirrorFreshnessCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Warn);
result.Message.Should().Contain("stale");
}
[Fact]
public async Task CorpusMirrorFreshnessCheck_VeryOldMirrors_ReturnsFail()
{
// Arrange
SetupStaleMirrors(daysOld: 30); // Beyond stale threshold
var options = CreateMirrorOptions(_testOutputDir, staleDays: 7);
var check = new CorpusMirrorFreshnessCheck(options, NullLogger<CorpusMirrorFreshnessCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Fail);
}
[Fact]
public async Task CorpusMirrorFreshnessCheck_MissingLastSync_ReturnsFail()
{
// Arrange - no .last-sync files
Directory.CreateDirectory(Path.Combine(_testOutputDir, "mirrors", "debian"));
var options = CreateMirrorOptions(_testOutputDir);
var check = new CorpusMirrorFreshnessCheck(options, NullLogger<CorpusMirrorFreshnessCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Fail);
result.Message.Should().Contain("never synced");
}
#endregion
#region KpiBaselineExistsCheck Tests
[Fact]
public async Task KpiBaselineExistsCheck_BaselineExists_ReturnsPass()
{
// Arrange
SetupValidBaseline();
var options = CreateKpiOptions(_testOutputDir);
var check = new KpiBaselineExistsCheck(options, NullLogger<KpiBaselineExistsCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Pass);
result.Message.Should().Contain("baseline");
}
[Fact]
public async Task KpiBaselineExistsCheck_BaselineMissing_ReturnsFail()
{
// Arrange - no baseline file
var options = CreateKpiOptions(_testOutputDir);
var check = new KpiBaselineExistsCheck(options, NullLogger<KpiBaselineExistsCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Fail);
result.Message.Should().Contain("not found");
}
[Fact]
public async Task KpiBaselineExistsCheck_OldBaseline_ReturnsWarn()
{
// Arrange
SetupOldBaseline(daysOld: 45); // Older than recommended refresh interval
var options = CreateKpiOptions(_testOutputDir, maxBaselineAgeDays: 30);
var check = new KpiBaselineExistsCheck(options, NullLogger<KpiBaselineExistsCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Warn);
result.Message.Should().Contain("old");
}
#endregion
#region BuildinfoCacheAccessibleCheck Tests
[Fact]
public async Task BuildinfoCacheAccessibleCheck_CacheAccessible_ReturnsPass()
{
// Arrange
var httpClient = CreateMockHttpClient(HttpStatusCode.OK);
var options = CreateBuildinfoOptions("https://buildinfos.debian.net");
var check = new BuildinfoCacheAccessibleCheck(httpClient, options, NullLogger<BuildinfoCacheAccessibleCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Pass);
}
[Fact]
public async Task BuildinfoCacheAccessibleCheck_CacheUnreachable_ReturnsFail()
{
// Arrange
var httpClient = CreateMockHttpClient(HttpStatusCode.ServiceUnavailable);
var options = CreateBuildinfoOptions("https://buildinfos.debian.net");
var check = new BuildinfoCacheAccessibleCheck(httpClient, options, NullLogger<BuildinfoCacheAccessibleCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Fail);
}
#endregion
#region SymbolRecoveryFallbackCheck Tests
[Fact]
public async Task SymbolRecoveryFallbackCheck_FallbackConfigured_ReturnsPass()
{
// Arrange
SetupFallbackSymbols();
var options = CreateFallbackOptions(_testOutputDir, fallbackEnabled: true);
var check = new SymbolRecoveryFallbackCheck(options, NullLogger<SymbolRecoveryFallbackCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Pass);
result.Message.Should().Contain("fallback");
}
[Fact]
public async Task SymbolRecoveryFallbackCheck_FallbackDisabled_ReturnsWarn()
{
// Arrange
var options = CreateFallbackOptions(_testOutputDir, fallbackEnabled: false);
var check = new SymbolRecoveryFallbackCheck(options, NullLogger<SymbolRecoveryFallbackCheck>.Instance);
// Act
var result = await check.RunAsync(CancellationToken.None);
// Assert
result.Status.Should().Be(CheckStatus.Warn);
}
#endregion
#region Plugin Integration Tests
[Fact]
public void BinaryAnalysisDoctorPlugin_RegistersAllCorpusChecks()
{
// Arrange
var plugin = new BinaryAnalysisDoctorPlugin();
// Act
var checks = plugin.GetChecks(_serviceProvider).ToList();
// Assert
checks.Should().Contain(c => c.Name.Contains("debuginfod", StringComparison.OrdinalIgnoreCase));
checks.Should().Contain(c => c.Name.Contains("mirror", StringComparison.OrdinalIgnoreCase));
checks.Should().Contain(c => c.Name.Contains("baseline", StringComparison.OrdinalIgnoreCase));
checks.Should().Contain(c => c.Name.Contains("buildinfo", StringComparison.OrdinalIgnoreCase));
checks.Should().Contain(c => c.Name.Contains("fallback", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public async Task AllCorpusChecks_RunWithoutException()
{
// Arrange
SetupFreshMirrors();
SetupValidBaseline();
SetupFallbackSymbols();
var plugin = new BinaryAnalysisDoctorPlugin();
var checks = plugin.GetChecks(_serviceProvider).ToList();
// Act & Assert
foreach (var check in checks.Where(c => c.Category == "corpus"))
{
var act = async () => await check.RunAsync(CancellationToken.None);
await act.Should().NotThrowAsync();
}
}
#endregion
#region Helper Methods
private IServiceProvider BuildServiceProvider()
{
var services = new ServiceCollection();
services.AddSingleton(CreateMockHttpClient(HttpStatusCode.OK));
services.Configure<DebuginfodOptions>(o =>
{
o.Urls = ["https://debuginfod.fedora.org"];
});
services.Configure<CorpusMirrorOptions>(o =>
{
o.MirrorsPath = Path.Combine(_testOutputDir, "mirrors");
});
services.Configure<KpiOptions>(o =>
{
o.BaselinePath = Path.Combine(_testOutputDir, "baselines", "current.json");
});
services.Configure<BuildinfoOptions>(o =>
{
o.CacheUrl = "https://buildinfos.debian.net";
});
services.Configure<SymbolFallbackOptions>(o =>
{
o.FallbackPath = Path.Combine(_testOutputDir, "fallback");
o.Enabled = true;
});
services.AddLogging();
return services.BuildServiceProvider();
}
private static HttpClient CreateMockHttpClient(HttpStatusCode defaultStatus)
{
var handler = new MockHttpMessageHandler(defaultStatus);
return new HttpClient(handler);
}
private static HttpClient CreateMockHttpClient(Dictionary<string, HttpStatusCode> statusByUrl)
{
var handler = new MockHttpMessageHandler(statusByUrl);
return new HttpClient(handler);
}
private static IOptions<DebuginfodOptions> CreateDebuginfodOptions(string[] urls)
{
return Options.Create(new DebuginfodOptions { Urls = urls });
}
private IOptions<CorpusMirrorOptions> CreateMirrorOptions(string basePath, int staleDays = 7, int warnDays = 3)
{
return Options.Create(new CorpusMirrorOptions
{
MirrorsPath = Path.Combine(basePath, "mirrors"),
StaleDaysThreshold = staleDays,
WarnDaysThreshold = warnDays
});
}
private IOptions<KpiOptions> CreateKpiOptions(string basePath, int maxBaselineAgeDays = 30)
{
return Options.Create(new KpiOptions
{
BaselinePath = Path.Combine(basePath, "baselines", "current.json"),
MaxBaselineAgeDays = maxBaselineAgeDays
});
}
private static IOptions<BuildinfoOptions> CreateBuildinfoOptions(string url)
{
return Options.Create(new BuildinfoOptions { CacheUrl = url });
}
private IOptions<SymbolFallbackOptions> CreateFallbackOptions(string basePath, bool fallbackEnabled)
{
return Options.Create(new SymbolFallbackOptions
{
FallbackPath = Path.Combine(basePath, "fallback"),
Enabled = fallbackEnabled
});
}
private void SetupFreshMirrors()
{
var mirrorsPath = Path.Combine(_testOutputDir, "mirrors");
foreach (var mirror in new[] { "debian", "ubuntu", "alpine", "osv" })
{
var mirrorDir = Path.Combine(mirrorsPath, mirror);
Directory.CreateDirectory(mirrorDir);
File.WriteAllText(Path.Combine(mirrorDir, ".last-sync"), DateTime.UtcNow.ToString("O"));
}
}
private void SetupStaleMirrors(int daysOld)
{
var mirrorsPath = Path.Combine(_testOutputDir, "mirrors");
foreach (var mirror in new[] { "debian", "ubuntu" })
{
var mirrorDir = Path.Combine(mirrorsPath, mirror);
Directory.CreateDirectory(mirrorDir);
File.WriteAllText(Path.Combine(mirrorDir, ".last-sync"),
DateTime.UtcNow.AddDays(-daysOld).ToString("O"));
}
}
private void SetupValidBaseline()
{
var baselineDir = Path.Combine(_testOutputDir, "baselines");
Directory.CreateDirectory(baselineDir);
var baseline = new
{
baselineId = Guid.NewGuid().ToString(),
createdAt = DateTime.UtcNow.ToString("O"),
precision = 0.95,
recall = 0.92
};
File.WriteAllText(Path.Combine(baselineDir, "current.json"),
System.Text.Json.JsonSerializer.Serialize(baseline));
}
private void SetupOldBaseline(int daysOld)
{
var baselineDir = Path.Combine(_testOutputDir, "baselines");
Directory.CreateDirectory(baselineDir);
var baseline = new
{
baselineId = Guid.NewGuid().ToString(),
createdAt = DateTime.UtcNow.AddDays(-daysOld).ToString("O"),
precision = 0.95,
recall = 0.92
};
File.WriteAllText(Path.Combine(baselineDir, "current.json"),
System.Text.Json.JsonSerializer.Serialize(baseline));
}
private void SetupFallbackSymbols()
{
var fallbackDir = Path.Combine(_testOutputDir, "fallback");
Directory.CreateDirectory(fallbackDir);
File.WriteAllText(Path.Combine(fallbackDir, "fallback-config.json"), "{}");
}
public void Dispose()
{
if (Directory.Exists(_testOutputDir))
{
try
{
Directory.Delete(_testOutputDir, recursive: true);
}
catch
{
// Best effort cleanup
}
}
}
#endregion
#region Mock HTTP Handler
private sealed class MockHttpMessageHandler : HttpMessageHandler
{
private readonly HttpStatusCode _defaultStatus;
private readonly Dictionary<string, HttpStatusCode>? _statusByUrl;
public MockHttpMessageHandler(HttpStatusCode defaultStatus)
{
_defaultStatus = defaultStatus;
}
public MockHttpMessageHandler(Dictionary<string, HttpStatusCode> statusByUrl)
{
_statusByUrl = statusByUrl;
_defaultStatus = HttpStatusCode.OK;
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var url = request.RequestUri?.ToString() ?? "";
var status = _statusByUrl?.GetValueOrDefault(url, _defaultStatus) ?? _defaultStatus;
return Task.FromResult(new HttpResponseMessage(status));
}
}
#endregion
}
// Enum for HTTP status codes (if not already in scope)
file enum HttpStatusCode
{
OK = 200,
ServiceUnavailable = 503
}
// Check status enum (if not already defined in the Doctor plugin)
file enum CheckStatus
{
Pass,
Warn,
Fail
}

View File

@@ -10,8 +10,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="Moq" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>