Files
git.stella-ops.org/src/__Libraries/StellaOps.Facet.Tests/FacetDriftVexEmitterTests.cs

438 lines
14 KiB
C#

// <copyright file="FacetDriftVexEmitterTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
// </copyright>
// Sprint: SPRINT_20260105_002_003_FACET (QTA-020)
using System.Collections.Immutable;
using Microsoft.Extensions.Time.Testing;
using Xunit;
namespace StellaOps.Facet.Tests;
/// <summary>
/// Unit tests for <see cref="FacetDriftVexEmitter"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class FacetDriftVexEmitterTests
{
private readonly FakeTimeProvider _timeProvider;
private readonly FacetDriftVexEmitter _emitter;
private readonly FacetDriftVexEmitterOptions _options;
public FacetDriftVexEmitterTests()
{
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 7, 12, 0, 0, TimeSpan.Zero));
_options = FacetDriftVexEmitterOptions.Default;
_emitter = new FacetDriftVexEmitter(_options, _timeProvider);
}
[Fact]
public void EmitDrafts_WithNoRequiresVexFacets_ReturnsEmptyResult()
{
// Arrange
var report = CreateDriftReport(QuotaVerdict.Ok);
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = _emitter.EmitDrafts(context);
// Assert
Assert.Equal(0, result.DraftsEmitted);
Assert.Empty(result.Drafts);
}
[Fact]
public void EmitDrafts_WithRequiresVexFacet_CreatesDraft()
{
// Arrange
var report = CreateDriftReport(QuotaVerdict.RequiresVex);
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = _emitter.EmitDrafts(context);
// Assert
Assert.Equal(1, result.DraftsEmitted);
Assert.Single(result.Drafts);
}
[Fact]
public void EmitDrafts_DraftContainsCorrectImageDigest()
{
// Arrange
var report = CreateDriftReport(QuotaVerdict.RequiresVex, imageDigest: "sha256:abc123");
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = _emitter.EmitDrafts(context);
// Assert
Assert.Equal("sha256:abc123", result.ImageDigest);
Assert.Equal("sha256:abc123", result.Drafts[0].ImageDigest);
}
[Fact]
public void EmitDrafts_DraftContainsBaselineSealId()
{
// Arrange
var report = CreateDriftReport(QuotaVerdict.RequiresVex, baselineSealId: "seal-xyz");
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = _emitter.EmitDrafts(context);
// Assert
Assert.Equal("seal-xyz", result.BaselineSealId);
Assert.Equal("seal-xyz", result.Drafts[0].BaselineSealId);
}
[Fact]
public void EmitDrafts_DraftHasDeterministicId()
{
// Arrange
var report = CreateDriftReport(QuotaVerdict.RequiresVex);
var context = new FacetDriftVexEmissionContext(report);
// Act
var result1 = _emitter.EmitDrafts(context);
var result2 = _emitter.EmitDrafts(context);
// Assert
Assert.Equal(result1.Drafts[0].DraftId, result2.Drafts[0].DraftId);
Assert.StartsWith("vexfd-", result1.Drafts[0].DraftId);
}
[Fact]
public void EmitDrafts_DraftIdsDifferForDifferentFacets()
{
// Arrange
var facetDrifts = new[]
{
CreateFacetDrift("facet-a", QuotaVerdict.RequiresVex),
CreateFacetDrift("facet-b", QuotaVerdict.RequiresVex)
};
var report = new FacetDriftReport
{
ImageDigest = "sha256:abc123",
BaselineSealId = "seal-123",
AnalyzedAt = _timeProvider.GetUtcNow(),
FacetDrifts = [.. facetDrifts],
OverallVerdict = QuotaVerdict.RequiresVex
};
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = _emitter.EmitDrafts(context);
// Assert
Assert.Equal(2, result.DraftsEmitted);
Assert.NotEqual(result.Drafts[0].DraftId, result.Drafts[1].DraftId);
}
[Fact]
public void EmitDrafts_DraftContainsChurnInformation()
{
// Arrange
var report = CreateDriftReportWithChurn(25m);
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = _emitter.EmitDrafts(context);
// Assert
var summary = result.Drafts[0].DriftSummary;
Assert.Equal(25m, summary.ChurnPercent);
Assert.Equal(100, summary.BaselineFileCount);
}
[Fact]
public void EmitDrafts_DraftHasCorrectExpirationTime()
{
// Arrange
var options = new FacetDriftVexEmitterOptions { DraftTtl = TimeSpan.FromDays(14) };
var emitter = new FacetDriftVexEmitter(options, _timeProvider);
var report = CreateDriftReport(QuotaVerdict.RequiresVex);
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = emitter.EmitDrafts(context);
// Assert
var expectedExpiry = _timeProvider.GetUtcNow().AddDays(14);
Assert.Equal(expectedExpiry, result.Drafts[0].ExpiresAt);
}
[Fact]
public void EmitDrafts_DraftHasCorrectReviewDeadline()
{
// Arrange
var options = new FacetDriftVexEmitterOptions { ReviewSlaDays = 5 };
var emitter = new FacetDriftVexEmitter(options, _timeProvider);
var report = CreateDriftReport(QuotaVerdict.RequiresVex);
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = emitter.EmitDrafts(context);
// Assert
var expectedDeadline = _timeProvider.GetUtcNow().AddDays(5);
Assert.Equal(expectedDeadline, result.Drafts[0].ReviewDeadline);
}
[Fact]
public void EmitDrafts_DraftRequiresReview()
{
// Arrange
var report = CreateDriftReport(QuotaVerdict.RequiresVex);
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = _emitter.EmitDrafts(context);
// Assert
Assert.True(result.Drafts[0].RequiresReview);
}
[Fact]
public void EmitDrafts_DraftHasEvidenceLinks()
{
// Arrange
var report = CreateDriftReportWithChanges(added: 5, removed: 3, modified: 2);
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = _emitter.EmitDrafts(context);
// Assert
var links = result.Drafts[0].EvidenceLinks;
Assert.Contains(links, l => l.Type == "facet_drift_analysis");
Assert.Contains(links, l => l.Type == "baseline_seal");
Assert.Contains(links, l => l.Type == "added_files");
Assert.Contains(links, l => l.Type == "removed_files");
Assert.Contains(links, l => l.Type == "modified_files");
}
[Fact]
public void EmitDrafts_RationaleDescribesChurn()
{
// Arrange - 15 files added out of 100 baseline = 15.0% churn
var report = CreateDriftReportWithChurn(15m);
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = _emitter.EmitDrafts(context);
// Assert
var rationale = result.Drafts[0].Rationale;
Assert.Contains("15.0%", rationale);
Assert.Contains("quota", rationale, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void EmitDrafts_HighChurnTriggersWarningInNotes()
{
// Arrange
var options = new FacetDriftVexEmitterOptions { HighChurnThreshold = 20m };
var emitter = new FacetDriftVexEmitter(options, _timeProvider);
var report = CreateDriftReportWithChurn(35m);
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = emitter.EmitDrafts(context);
// Assert
var notes = result.Drafts[0].ReviewerNotes;
Assert.NotNull(notes);
Assert.Contains("WARNING", notes);
Assert.Contains("High churn", notes);
}
[Fact]
public void EmitDrafts_RemovedFilesTriggersNoteInReviewerNotes()
{
// Arrange
var report = CreateDriftReportWithChanges(added: 0, removed: 5, modified: 0);
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = _emitter.EmitDrafts(context);
// Assert
var notes = result.Drafts[0].ReviewerNotes;
Assert.NotNull(notes);
Assert.Contains("removed", notes, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void EmitDrafts_RespectsMaxDraftsLimit()
{
// Arrange
var options = new FacetDriftVexEmitterOptions { MaxDraftsPerBatch = 2 };
var emitter = new FacetDriftVexEmitter(options, _timeProvider);
var facetDrifts = Enumerable.Range(0, 5)
.Select(i => CreateFacetDrift($"facet-{i}", QuotaVerdict.RequiresVex))
.ToImmutableArray();
var report = new FacetDriftReport
{
ImageDigest = "sha256:abc123",
BaselineSealId = "seal-123",
AnalyzedAt = _timeProvider.GetUtcNow(),
FacetDrifts = facetDrifts,
OverallVerdict = QuotaVerdict.RequiresVex
};
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = emitter.EmitDrafts(context);
// Assert
Assert.Equal(2, result.DraftsEmitted);
Assert.Equal(2, result.Drafts.Length);
}
[Fact]
public void EmitDrafts_SkipsNonRequiresVexFacets()
{
// Arrange
var facetDrifts = new[]
{
CreateFacetDrift("facet-ok", QuotaVerdict.Ok),
CreateFacetDrift("facet-warn", QuotaVerdict.Warning),
CreateFacetDrift("facet-block", QuotaVerdict.Blocked),
CreateFacetDrift("facet-vex", QuotaVerdict.RequiresVex)
};
var report = new FacetDriftReport
{
ImageDigest = "sha256:abc123",
BaselineSealId = "seal-123",
AnalyzedAt = _timeProvider.GetUtcNow(),
FacetDrifts = [.. facetDrifts],
OverallVerdict = QuotaVerdict.RequiresVex
};
var context = new FacetDriftVexEmissionContext(report);
// Act
var result = _emitter.EmitDrafts(context);
// Assert
Assert.Equal(1, result.DraftsEmitted);
Assert.Equal("facet-vex", result.Drafts[0].FacetId);
}
[Fact]
public void EmitDrafts_NullContext_ThrowsArgumentNullException()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => _emitter.EmitDrafts(null!));
}
#region Helper Methods
private FacetDriftReport CreateDriftReport(
QuotaVerdict verdict,
string imageDigest = "sha256:default",
string baselineSealId = "seal-default")
{
return new FacetDriftReport
{
ImageDigest = imageDigest,
BaselineSealId = baselineSealId,
AnalyzedAt = _timeProvider.GetUtcNow(),
FacetDrifts = [CreateFacetDrift("test-facet", verdict)],
OverallVerdict = verdict
};
}
private FacetDriftReport CreateDriftReportWithChurn(decimal churnPercent)
{
var addedCount = (int)(churnPercent * 100 / 100);
var addedFiles = Enumerable.Range(0, addedCount)
.Select(i => new FacetFileEntry($"/added{i}.txt", $"sha256:added{i}", 100, null))
.ToImmutableArray();
var facetDrift = new FacetDrift
{
FacetId = "test-facet",
Added = addedFiles,
Removed = [],
Modified = [],
DriftScore = churnPercent,
QuotaVerdict = QuotaVerdict.RequiresVex,
BaselineFileCount = 100
};
return new FacetDriftReport
{
ImageDigest = "sha256:churn-test",
BaselineSealId = "seal-churn",
AnalyzedAt = _timeProvider.GetUtcNow(),
FacetDrifts = [facetDrift],
OverallVerdict = QuotaVerdict.RequiresVex
};
}
private FacetDriftReport CreateDriftReportWithChanges(int added, int removed, int modified)
{
var addedFiles = Enumerable.Range(0, added)
.Select(i => new FacetFileEntry($"/added{i}.txt", $"sha256:added{i}", 100, null))
.ToImmutableArray();
var removedFiles = Enumerable.Range(0, removed)
.Select(i => new FacetFileEntry($"/removed{i}.txt", $"sha256:removed{i}", 100, null))
.ToImmutableArray();
var modifiedFiles = Enumerable.Range(0, modified)
.Select(i => new FacetFileModification(
$"/modified{i}.txt",
$"sha256:old{i}",
$"sha256:new{i}",
100,
110))
.ToImmutableArray();
var facetDrift = new FacetDrift
{
FacetId = "test-facet",
Added = addedFiles,
Removed = removedFiles,
Modified = modifiedFiles,
DriftScore = added + removed + modified,
QuotaVerdict = QuotaVerdict.RequiresVex,
BaselineFileCount = 100
};
return new FacetDriftReport
{
ImageDigest = "sha256:changes-test",
BaselineSealId = "seal-changes",
AnalyzedAt = _timeProvider.GetUtcNow(),
FacetDrifts = [facetDrift],
OverallVerdict = QuotaVerdict.RequiresVex
};
}
private FacetDrift CreateFacetDrift(string facetId, QuotaVerdict verdict)
{
var addedCount = verdict == QuotaVerdict.RequiresVex ? 50 : 0;
var addedFiles = Enumerable.Range(0, addedCount)
.Select(i => new FacetFileEntry($"/added{i}.txt", $"sha256:added{i}", 100, null))
.ToImmutableArray();
return new FacetDrift
{
FacetId = facetId,
Added = addedFiles,
Removed = [],
Modified = [],
DriftScore = addedCount,
QuotaVerdict = verdict,
BaselineFileCount = 100
};
}
#endregion
}