Merge remote changes (theirs)
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
// <copyright file="FacetQuotaGateTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
// Sprint: SPRINT_20260105_002_003_FACET (QTA-014)
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using StellaOps.Facet;
|
||||
using StellaOps.Policy.Confidence.Models;
|
||||
using StellaOps.Policy.Gates;
|
||||
using StellaOps.Policy.TrustLattice;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="FacetQuotaGate"/> evaluation scenarios.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class FacetQuotaGateTests
|
||||
{
|
||||
private readonly Mock<IFacetDriftDetector> _driftDetectorMock;
|
||||
private readonly FacetQuotaGateOptions _options;
|
||||
private FacetQuotaGate _gate;
|
||||
|
||||
public FacetQuotaGateTests()
|
||||
{
|
||||
_driftDetectorMock = new Mock<IFacetDriftDetector>();
|
||||
_options = new FacetQuotaGateOptions { Enabled = true };
|
||||
_gate = CreateGate(_options);
|
||||
}
|
||||
|
||||
private FacetQuotaGate CreateGate(FacetQuotaGateOptions options)
|
||||
{
|
||||
return new FacetQuotaGate(options, _driftDetectorMock.Object, NullLogger<FacetQuotaGate>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WhenDisabled_ReturnsPass()
|
||||
{
|
||||
// Arrange
|
||||
var options = new FacetQuotaGateOptions { Enabled = false };
|
||||
var gate = CreateGate(options);
|
||||
var context = CreateContext();
|
||||
var mergeResult = CreateMergeResult();
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Passed);
|
||||
Assert.Equal("Gate disabled", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WhenNoSealAndNoSealActionIsPass_ReturnsPass()
|
||||
{
|
||||
// Arrange - no drift report in context means no seal
|
||||
var options = new FacetQuotaGateOptions { Enabled = true, NoSealAction = NoSealAction.Pass };
|
||||
var gate = CreateGate(options);
|
||||
var context = CreateContext();
|
||||
var mergeResult = CreateMergeResult();
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Passed);
|
||||
Assert.Contains("first scan", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WhenNoSealAndNoSealActionIsWarn_ReturnsPassWithWarning()
|
||||
{
|
||||
// Arrange
|
||||
var options = new FacetQuotaGateOptions { Enabled = true, NoSealAction = NoSealAction.Warn };
|
||||
var gate = CreateGate(options);
|
||||
var context = CreateContext();
|
||||
var mergeResult = CreateMergeResult();
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Passed);
|
||||
Assert.Equal("no_baseline_seal", result.Reason);
|
||||
Assert.True(result.Details.ContainsKey("action"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WhenNoSealAndNoSealActionIsBlock_ReturnsFail()
|
||||
{
|
||||
// Arrange
|
||||
var options = new FacetQuotaGateOptions { Enabled = true, NoSealAction = NoSealAction.Block };
|
||||
var gate = CreateGate(options);
|
||||
var context = CreateContext();
|
||||
var mergeResult = CreateMergeResult();
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Passed);
|
||||
Assert.Equal("no_baseline_seal", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_NullMergeResult_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() =>
|
||||
_gate.EvaluateAsync(null!, context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_NullContext_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var mergeResult = CreateMergeResult();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() =>
|
||||
_gate.EvaluateAsync(mergeResult, null!));
|
||||
}
|
||||
|
||||
private static PolicyGateContext CreateContext()
|
||||
{
|
||||
return new PolicyGateContext
|
||||
{
|
||||
Environment = "test"
|
||||
};
|
||||
}
|
||||
|
||||
private static PolicyGateContext CreateContextWithDriftReportJson(string driftReportJson)
|
||||
{
|
||||
return new PolicyGateContext
|
||||
{
|
||||
Environment = "test",
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["FacetDriftReport"] = driftReportJson
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static MergeResult CreateMergeResult()
|
||||
{
|
||||
var emptyClaim = new ScoredClaim
|
||||
{
|
||||
SourceId = "test",
|
||||
Status = VexStatus.NotAffected,
|
||||
OriginalScore = 1.0,
|
||||
AdjustedScore = 1.0,
|
||||
ScopeSpecificity = 1,
|
||||
Accepted = true,
|
||||
Reason = "test"
|
||||
};
|
||||
|
||||
return new MergeResult
|
||||
{
|
||||
Status = VexStatus.NotAffected,
|
||||
Confidence = 0.9,
|
||||
HasConflicts = false,
|
||||
AllClaims = [emptyClaim],
|
||||
WinningClaim = emptyClaim,
|
||||
Conflicts = []
|
||||
};
|
||||
}
|
||||
|
||||
private static FacetDriftReport CreateDriftReport(QuotaVerdict verdict)
|
||||
{
|
||||
return new FacetDriftReport
|
||||
{
|
||||
ImageDigest = "sha256:abc123",
|
||||
BaselineSealId = "seal-123",
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
FacetDrifts = [CreateFacetDrift("test-facet", verdict)],
|
||||
OverallVerdict = verdict
|
||||
};
|
||||
}
|
||||
|
||||
private static FacetDrift CreateFacetDrift(
|
||||
string facetId,
|
||||
QuotaVerdict verdict,
|
||||
int baselineFileCount = 100)
|
||||
{
|
||||
// ChurnPercent is computed from TotalChanges / BaselineFileCount
|
||||
// For different verdicts, we add files appropriately
|
||||
var addedCount = verdict switch
|
||||
{
|
||||
QuotaVerdict.Warning => 10, // 10% churn
|
||||
QuotaVerdict.Blocked => 30, // 30% churn
|
||||
QuotaVerdict.RequiresVex => 50, // 50% churn
|
||||
_ => 0
|
||||
};
|
||||
|
||||
var addedFiles = Enumerable.Range(0, addedCount)
|
||||
.Select(i => new FacetFileEntry(
|
||||
$"/added/file{i}.txt",
|
||||
$"sha256:added{i}",
|
||||
100,
|
||||
null))
|
||||
.ToImmutableArray();
|
||||
|
||||
return new FacetDrift
|
||||
{
|
||||
FacetId = facetId,
|
||||
Added = addedFiles,
|
||||
Removed = [],
|
||||
Modified = [],
|
||||
DriftScore = addedCount,
|
||||
QuotaVerdict = verdict,
|
||||
BaselineFileCount = baselineFileCount
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ rules:
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var timeProvider = new FakeTimeProvider();
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
|
||||
await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, null), CancellationToken.None);
|
||||
|
||||
@@ -80,7 +80,7 @@ rules:
|
||||
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
|
||||
|
||||
var findings = ImmutableArray.Create(
|
||||
@@ -111,7 +111,7 @@ rules:
|
||||
{
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
|
||||
|
||||
const string invalid = "version: 1.0";
|
||||
@@ -159,7 +159,7 @@ rules:
|
||||
Assert.False(binding.Document.Rules[0].Metadata.ContainsKey("quiet"));
|
||||
Assert.True(binding.Document.Rules[0].Action.Quiet);
|
||||
|
||||
var store = new PolicySnapshotStore(new InMemoryPolicySnapshotRepository(), new InMemoryPolicyAuditRepository(), TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(new InMemoryPolicySnapshotRepository(), new InMemoryPolicyAuditRepository(), TimeProvider.System, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, "quiet test"), CancellationToken.None);
|
||||
var snapshot = await store.GetLatestAsync();
|
||||
Assert.NotNull(snapshot);
|
||||
|
||||
@@ -25,7 +25,7 @@ rules:
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 10, 0, 0, TimeSpan.Zero));
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
|
||||
var content = new PolicySnapshotContent(BasePolicyYaml, PolicyDocumentFormat.Yaml, "cli", "test", null);
|
||||
|
||||
@@ -56,7 +56,7 @@ rules:
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 10, 0, 0, TimeSpan.Zero));
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
|
||||
var content = new PolicySnapshotContent(BasePolicyYaml, PolicyDocumentFormat.Yaml, "cli", "test", null);
|
||||
var first = await store.SaveAsync(content, CancellationToken.None);
|
||||
@@ -81,7 +81,7 @@ rules:
|
||||
{
|
||||
var snapshotRepo = new InMemoryPolicySnapshotRepository();
|
||||
var auditRepo = new InMemoryPolicyAuditRepository();
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
|
||||
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, null, NullLogger<PolicySnapshotStore>.Instance);
|
||||
|
||||
const string invalidYaml = "version: '1.0'\nrules: []";
|
||||
var content = new PolicySnapshotContent(invalidYaml, PolicyDocumentFormat.Yaml, null, null, null);
|
||||
|
||||
@@ -32,6 +32,7 @@ public sealed class ReplayEngineTests
|
||||
_snapshotService,
|
||||
sourceResolver,
|
||||
verdictComparer,
|
||||
null,
|
||||
NullLogger<ReplayEngine>.Instance);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy/StellaOps.Policy.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Determinization/StellaOps.Policy.Determinization.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Facet/StellaOps.Facet.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user