save progress
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
// Licensed to StellaOps under the AGPL-3.0-or-later license.
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="StabilityDampingGate"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class StabilityDampingGateTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly StabilityDampingOptions _defaultOptions;
|
||||
|
||||
public StabilityDampingGateTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 4, 12, 0, 0, TimeSpan.Zero));
|
||||
_defaultOptions = new StabilityDampingOptions
|
||||
{
|
||||
Enabled = true,
|
||||
MinDurationBeforeChange = TimeSpan.FromHours(4),
|
||||
MinConfidenceDeltaPercent = 0.15,
|
||||
OnlyDampDowngrades = true,
|
||||
DampedStatuses = ["affected", "not_affected", "fixed", "under_investigation"]
|
||||
};
|
||||
}
|
||||
|
||||
private StabilityDampingGate CreateGate(StabilityDampingOptions? options = null)
|
||||
{
|
||||
var opts = options ?? _defaultOptions;
|
||||
var optionsMonitor = new TestOptionsMonitor<StabilityDampingOptions>(opts);
|
||||
return new StabilityDampingGate(optionsMonitor, _timeProvider, NullLogger<StabilityDampingGate>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_NewVerdict_ShouldSurface()
|
||||
{
|
||||
// Arrange
|
||||
var gate = CreateGate();
|
||||
var request = new StabilityDampingRequest
|
||||
{
|
||||
Key = "artifact:CVE-2024-1234",
|
||||
ProposedState = new VerdictState
|
||||
{
|
||||
Status = "affected",
|
||||
Confidence = 0.85,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var decision = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
decision.ShouldSurface.Should().BeTrue();
|
||||
decision.Reason.Should().Contain("new verdict");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WhenDisabled_ShouldAlwaysSurface()
|
||||
{
|
||||
// Arrange
|
||||
var options = new StabilityDampingOptions { Enabled = false };
|
||||
var gate = CreateGate(options);
|
||||
var request = new StabilityDampingRequest
|
||||
{
|
||||
Key = "artifact:CVE-2024-1234",
|
||||
ProposedState = new VerdictState
|
||||
{
|
||||
Status = "affected",
|
||||
Confidence = 0.85,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var decision = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
decision.ShouldSurface.Should().BeTrue();
|
||||
decision.Reason.Should().Contain("disabled");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_StatusUpgrade_ShouldSurfaceImmediately()
|
||||
{
|
||||
// Arrange
|
||||
var gate = CreateGate();
|
||||
var key = "artifact:CVE-2024-1234";
|
||||
|
||||
// Record initial state as not_affected
|
||||
await gate.RecordStateAsync(key, new VerdictState
|
||||
{
|
||||
Status = "not_affected",
|
||||
Confidence = 0.80,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
});
|
||||
|
||||
// Request upgrade to affected (more severe)
|
||||
var request = new StabilityDampingRequest
|
||||
{
|
||||
Key = key,
|
||||
ProposedState = new VerdictState
|
||||
{
|
||||
Status = "affected",
|
||||
Confidence = 0.85,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var decision = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
decision.ShouldSurface.Should().BeTrue();
|
||||
decision.IsUpgrade.Should().BeTrue();
|
||||
decision.Reason.Should().Contain("upgrade");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_StatusDowngrade_WithoutMinDuration_ShouldDamp()
|
||||
{
|
||||
// Arrange
|
||||
var gate = CreateGate();
|
||||
var key = "artifact:CVE-2024-1234";
|
||||
|
||||
// Record initial state as affected
|
||||
await gate.RecordStateAsync(key, new VerdictState
|
||||
{
|
||||
Status = "affected",
|
||||
Confidence = 0.80,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
});
|
||||
|
||||
// Advance time but not enough to meet threshold
|
||||
_timeProvider.Advance(TimeSpan.FromHours(2));
|
||||
|
||||
// Request downgrade to not_affected (less severe)
|
||||
var request = new StabilityDampingRequest
|
||||
{
|
||||
Key = key,
|
||||
ProposedState = new VerdictState
|
||||
{
|
||||
Status = "not_affected",
|
||||
Confidence = 0.75,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var decision = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
decision.ShouldSurface.Should().BeFalse();
|
||||
decision.Reason.Should().Contain("Damped");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_StatusDowngrade_AfterMinDuration_ShouldSurface()
|
||||
{
|
||||
// Arrange
|
||||
var gate = CreateGate();
|
||||
var key = "artifact:CVE-2024-1234";
|
||||
|
||||
// Record initial state as affected
|
||||
await gate.RecordStateAsync(key, new VerdictState
|
||||
{
|
||||
Status = "affected",
|
||||
Confidence = 0.80,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
});
|
||||
|
||||
// Advance time past threshold
|
||||
_timeProvider.Advance(TimeSpan.FromHours(5));
|
||||
|
||||
// Request downgrade to not_affected
|
||||
var request = new StabilityDampingRequest
|
||||
{
|
||||
Key = key,
|
||||
ProposedState = new VerdictState
|
||||
{
|
||||
Status = "not_affected",
|
||||
Confidence = 0.75,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var decision = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
decision.ShouldSurface.Should().BeTrue();
|
||||
decision.StateDuration.Should().BeGreaterThan(TimeSpan.FromHours(4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_LargeConfidenceDelta_ShouldSurfaceImmediately()
|
||||
{
|
||||
// Arrange
|
||||
var gate = CreateGate();
|
||||
var key = "artifact:CVE-2024-1234";
|
||||
|
||||
// Record initial state
|
||||
await gate.RecordStateAsync(key, new VerdictState
|
||||
{
|
||||
Status = "affected",
|
||||
Confidence = 0.50,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
});
|
||||
|
||||
// Request with large confidence change (>15%)
|
||||
var request = new StabilityDampingRequest
|
||||
{
|
||||
Key = key,
|
||||
ProposedState = new VerdictState
|
||||
{
|
||||
Status = "affected",
|
||||
Confidence = 0.90,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var decision = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
decision.ShouldSurface.Should().BeTrue();
|
||||
decision.ConfidenceDelta.Should().BeGreaterThan(0.15);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_SmallConfidenceDelta_SameStatus_ShouldDamp()
|
||||
{
|
||||
// Arrange
|
||||
var gate = CreateGate();
|
||||
var key = "artifact:CVE-2024-1234";
|
||||
|
||||
// Record initial state
|
||||
await gate.RecordStateAsync(key, new VerdictState
|
||||
{
|
||||
Status = "affected",
|
||||
Confidence = 0.80,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
});
|
||||
|
||||
// Request with small confidence change (<15%)
|
||||
var request = new StabilityDampingRequest
|
||||
{
|
||||
Key = key,
|
||||
ProposedState = new VerdictState
|
||||
{
|
||||
Status = "affected",
|
||||
Confidence = 0.85,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var decision = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
decision.ShouldSurface.Should().BeFalse();
|
||||
decision.ConfidenceDelta.Should().BeLessThan(0.15);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PruneHistoryAsync_ShouldRemoveStaleRecords()
|
||||
{
|
||||
// Arrange
|
||||
var gate = CreateGate();
|
||||
|
||||
// Record old state
|
||||
await gate.RecordStateAsync("old-key", new VerdictState
|
||||
{
|
||||
Status = "affected",
|
||||
Confidence = 0.80,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
});
|
||||
|
||||
// Advance time past retention period
|
||||
_timeProvider.Advance(TimeSpan.FromDays(8)); // Default retention is 7 days
|
||||
|
||||
// Record new state (to ensure we have something current)
|
||||
await gate.RecordStateAsync("new-key", new VerdictState
|
||||
{
|
||||
Status = "affected",
|
||||
Confidence = 0.80,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
});
|
||||
|
||||
// Act
|
||||
var pruned = await gate.PruneHistoryAsync();
|
||||
|
||||
// Assert
|
||||
pruned.Should().BeGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WithTenantId_ShouldIsolateTenants()
|
||||
{
|
||||
// Arrange
|
||||
var gate = CreateGate();
|
||||
|
||||
// Record state for tenant-a
|
||||
await gate.RecordStateAsync("tenant-a:artifact:CVE-2024-1234", new VerdictState
|
||||
{
|
||||
Status = "affected",
|
||||
Confidence = 0.80,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
});
|
||||
|
||||
// Request for tenant-b (different tenant, no history)
|
||||
var request = new StabilityDampingRequest
|
||||
{
|
||||
Key = "artifact:CVE-2024-1234",
|
||||
TenantId = "tenant-b",
|
||||
ProposedState = new VerdictState
|
||||
{
|
||||
Status = "affected",
|
||||
Confidence = 0.80,
|
||||
Timestamp = _timeProvider.GetUtcNow()
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var decision = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
decision.ShouldSurface.Should().BeTrue();
|
||||
decision.Reason.Should().Contain("new verdict");
|
||||
}
|
||||
|
||||
private sealed class TestOptionsMonitor<T> : IOptionsMonitor<T>
|
||||
where T : class
|
||||
{
|
||||
public TestOptionsMonitor(T value) => CurrentValue = value;
|
||||
public T CurrentValue { get; }
|
||||
public T Get(string? name) => CurrentValue;
|
||||
public IDisposable? OnChange(Action<T, string?> listener) => null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user