save progress

This commit is contained in:
StellaOps Bot
2026-01-04 14:54:52 +02:00
parent c49b03a254
commit 3098e84de4
132 changed files with 19783 additions and 31 deletions

View File

@@ -0,0 +1,364 @@
// Licensed to StellaOps under the AGPL-3.0-or-later license.
using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.VexLens.Delta;
using StellaOps.VexLens.Models;
using Xunit;
namespace StellaOps.VexLens.Tests.Delta;
/// <summary>
/// Unit tests for <see cref="DeltaReportBuilder"/>.
/// </summary>
[Trait("Category", "Unit")]
public class DeltaReportBuilderTests
{
private readonly FakeTimeProvider _timeProvider;
public DeltaReportBuilderTests()
{
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 4, 12, 0, 0, TimeSpan.Zero));
}
[Fact]
public void Build_EmptyReport_ShouldHaveZeroCounts()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to");
// Act
var report = builder.Build();
// Assert
report.Summary.TotalCount.Should().Be(0);
report.Summary.NewCount.Should().Be(0);
report.Summary.ResolvedCount.Should().Be(0);
report.HasActionableChanges.Should().BeFalse();
}
[Fact]
public void AddNew_ShouldAddNewEntry()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to");
// Act
builder.AddNew(
"CVE-2024-1234",
"pkg:npm/lodash@4.17.21",
VexStatus.Affected,
0.85,
"binary",
null,
["nvd", "github"]);
var report = builder.Build();
// Assert
report.Summary.NewCount.Should().Be(1);
report.HasActionableChanges.Should().BeTrue();
report.GetSection(DeltaSection.New).Should().HaveCount(1);
var entry = report.GetSection(DeltaSection.New)[0];
entry.VulnerabilityId.Should().Be("CVE-2024-1234");
entry.ProductKey.Should().Be("pkg:npm/lodash@4.17.21");
entry.ToStatus.Should().Be(VexStatus.Affected);
entry.ContributingSources.Should().Contain("nvd");
}
[Fact]
public void AddResolved_ShouldAddResolvedEntry()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to");
// Act
builder.AddResolved(
"CVE-2024-1234",
"pkg:npm/lodash@4.17.21",
VexStatus.Affected,
VexStatus.NotAffected,
0.80,
0.95,
VexJustification.VulnerableCodeNotPresent);
var report = builder.Build();
// Assert
report.Summary.ResolvedCount.Should().Be(1);
var entry = report.GetSection(DeltaSection.Resolved)[0];
entry.FromStatus.Should().Be(VexStatus.Affected);
entry.ToStatus.Should().Be(VexStatus.NotAffected);
entry.Justification.Should().Be(VexJustification.VulnerableCodeNotPresent);
}
[Fact]
public void AddConfidenceChange_AboveThreshold_ShouldAddEntry()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to")
.WithOptions(new DeltaReportOptions { ConfidenceChangeThreshold = 0.15 });
// Act
builder.AddConfidenceChange(
"CVE-2024-1234",
"pkg:npm/lodash@4.17.21",
VexStatus.Affected,
0.50,
0.90); // 40% increase
var report = builder.Build();
// Assert
report.Summary.ConfidenceUpCount.Should().Be(1);
var entry = report.GetSection(DeltaSection.ConfidenceUp)[0];
entry.FromConfidence.Should().Be(0.50);
entry.ToConfidence.Should().Be(0.90);
}
[Fact]
public void AddConfidenceChange_BelowThreshold_ShouldNotAddEntry()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to")
.WithOptions(new DeltaReportOptions { ConfidenceChangeThreshold = 0.15 });
// Act
builder.AddConfidenceChange(
"CVE-2024-1234",
"pkg:npm/lodash@4.17.21",
VexStatus.Affected,
0.80,
0.85); // Only 5% increase
var report = builder.Build();
// Assert
report.Summary.ConfidenceUpCount.Should().Be(0);
report.Entries.Should().BeEmpty();
}
[Fact]
public void AddConfidenceChange_Decrease_ShouldAddConfidenceDownEntry()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to")
.WithOptions(new DeltaReportOptions { ConfidenceChangeThreshold = 0.15 });
// Act
builder.AddConfidenceChange(
"CVE-2024-1234",
"pkg:npm/lodash@4.17.21",
VexStatus.Affected,
0.90,
0.50); // 40% decrease
var report = builder.Build();
// Assert
report.Summary.ConfidenceDownCount.Should().Be(1);
report.GetSection(DeltaSection.ConfidenceDown).Should().HaveCount(1);
}
[Fact]
public void AddPolicyImpact_ShouldAddPolicyImpactEntry()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to");
// Act
builder.AddPolicyImpact(
"CVE-2024-1234",
"pkg:npm/lodash@4.17.21",
VexStatus.Affected,
0.85,
"Gate decision changed: pass -> fail");
var report = builder.Build();
// Assert
report.Summary.PolicyImpactCount.Should().Be(1);
report.HasActionableChanges.Should().BeTrue();
var entry = report.GetSection(DeltaSection.PolicyImpact)[0];
entry.Summary.Should().Contain("Gate decision changed");
}
[Fact]
public void AddDamped_WhenExcluded_ShouldNotAddEntry()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to")
.WithOptions(new DeltaReportOptions { IncludeDamped = false });
// Act
builder.AddDamped(
"CVE-2024-1234",
"pkg:npm/lodash@4.17.21",
VexStatus.Affected,
VexStatus.NotAffected,
0.80,
0.75,
"Duration threshold not met");
var report = builder.Build();
// Assert
report.Summary.DampedCount.Should().Be(0);
}
[Fact]
public void AddDamped_WhenIncluded_ShouldAddEntry()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to")
.WithOptions(new DeltaReportOptions { IncludeDamped = true });
// Act
builder.AddDamped(
"CVE-2024-1234",
"pkg:npm/lodash@4.17.21",
VexStatus.Affected,
VexStatus.NotAffected,
0.80,
0.75,
"Duration threshold not met");
var report = builder.Build();
// Assert
report.Summary.DampedCount.Should().Be(1);
report.GetSection(DeltaSection.Damped).Should().HaveCount(1);
}
[Fact]
public void AddEvidenceChange_ShouldAddEvidenceChangedEntry()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to")
.WithOptions(new DeltaReportOptions { IncludeEvidenceChanges = true });
// Act
builder.AddEvidenceChange(
"CVE-2024-1234",
"pkg:npm/lodash@4.17.21",
VexStatus.Affected,
0.85,
"heuristic",
"binary");
var report = builder.Build();
// Assert
report.Summary.EvidenceChangedCount.Should().Be(1);
var entry = report.GetSection(DeltaSection.EvidenceChanged)[0];
entry.FromRationaleClass.Should().Be("heuristic");
entry.ToRationaleClass.Should().Be("binary");
}
[Fact]
public void Build_ShouldSortEntriesDeterministically()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to");
// Add entries in non-sorted order
builder.AddNew("CVE-2024-0002", "pkg:b", VexStatus.Affected, 0.8);
builder.AddNew("CVE-2024-0001", "pkg:a", VexStatus.Affected, 0.8);
builder.AddResolved("CVE-2024-0003", "pkg:c", VexStatus.Affected, VexStatus.Fixed, 0.8, 0.9);
// Act
var report = builder.Build();
// Assert - entries should be sorted by section then vuln ID then product key
report.Entries[0].Section.Should().Be(DeltaSection.New);
report.Entries[0].VulnerabilityId.Should().Be("CVE-2024-0001");
report.Entries[1].VulnerabilityId.Should().Be("CVE-2024-0002");
report.Entries[2].Section.Should().Be(DeltaSection.Resolved);
}
[Fact]
public void Build_ReportId_ShouldBeDeterministic()
{
// Arrange
var builder1 = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to")
.AddNew("CVE-2024-1234", "pkg:npm/lodash", VexStatus.Affected, 0.8);
var builder2 = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to")
.AddNew("CVE-2024-1234", "pkg:npm/lodash", VexStatus.Affected, 0.8);
// Act
var report1 = builder1.Build();
var report2 = builder2.Build();
// Assert - same inputs should produce same report ID
report1.ReportId.Should().Be(report2.ReportId);
}
[Fact]
public void ToNotificationSummary_WithMultipleChanges_ShouldFormatCorrectly()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to")
.AddNew("CVE-2024-0001", "pkg:a", VexStatus.Affected, 0.8)
.AddNew("CVE-2024-0002", "pkg:b", VexStatus.Affected, 0.8)
.AddResolved("CVE-2024-0003", "pkg:c", VexStatus.Affected, VexStatus.Fixed, 0.8, 0.9);
// Act
var report = builder.Build();
var summary = report.ToNotificationSummary();
// Assert
summary.Should().Contain("2 new");
summary.Should().Contain("1 resolved");
}
[Fact]
public void ToNotificationSummary_NoChanges_ShouldReturnNoSignificantChanges()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to");
// Act
var report = builder.Build();
var summary = report.ToNotificationSummary();
// Assert
summary.Should().Be("No significant changes");
}
[Fact]
public void BySection_ShouldGroupEntriesCorrectly()
{
// Arrange
var builder = new DeltaReportBuilder(_timeProvider)
.WithSnapshots("sha256:from", "sha256:to")
.AddNew("CVE-2024-0001", "pkg:a", VexStatus.Affected, 0.8)
.AddResolved("CVE-2024-0002", "pkg:b", VexStatus.Affected, VexStatus.Fixed, 0.8, 0.9);
// Act
var report = builder.Build();
var bySection = report.BySection;
// Assert
bySection.Should().ContainKey(DeltaSection.New);
bySection.Should().ContainKey(DeltaSection.Resolved);
bySection[DeltaSection.New].Should().HaveCount(1);
bySection[DeltaSection.Resolved].Should().HaveCount(1);
}
}

View File

@@ -0,0 +1,451 @@
// Licensed to StellaOps under the AGPL-3.0-or-later license.
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using StellaOps.Policy.Engine.Gates;
using StellaOps.ReachGraph.Deduplication;
using StellaOps.ReachGraph.Schema;
using StellaOps.VexLens.Models;
using StellaOps.VexLens.NoiseGate;
using Xunit;
namespace StellaOps.VexLens.Tests.NoiseGate;
/// <summary>
/// Unit tests for <see cref="NoiseGateService"/>.
/// </summary>
[Trait("Category", "Unit")]
public class NoiseGateServiceTests
{
private readonly FakeTimeProvider _timeProvider;
private readonly NoiseGateOptions _defaultOptions;
public NoiseGateServiceTests()
{
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 4, 12, 0, 0, TimeSpan.Zero));
_defaultOptions = new NoiseGateOptions
{
Enabled = true,
EdgeDeduplicationEnabled = true,
StabilityDampingEnabled = true,
MinConfidenceThreshold = 0.0,
ConfidenceChangeThreshold = 0.15
};
}
private NoiseGateService CreateService(
IEdgeDeduplicator? edgeDeduplicator = null,
IStabilityDampingGate? dampingGate = null,
NoiseGateOptions? options = null)
{
var opts = options ?? _defaultOptions;
var optionsMonitor = new TestOptionsMonitor<NoiseGateOptions>(opts);
edgeDeduplicator ??= new EdgeDeduplicator();
if (dampingGate is null)
{
var dampingOptions = new StabilityDampingOptions { Enabled = true };
var dampingOptionsMonitor = new TestOptionsMonitor<StabilityDampingOptions>(dampingOptions);
dampingGate = new StabilityDampingGate(
dampingOptionsMonitor,
_timeProvider,
NullLogger<StabilityDampingGate>.Instance);
}
return new NoiseGateService(
edgeDeduplicator,
dampingGate,
optionsMonitor,
_timeProvider,
NullLogger<NoiseGateService>.Instance);
}
[Fact]
public async Task DedupeEdgesAsync_WithDuplicateEdges_ShouldDeduplicates()
{
// Arrange
var service = CreateService();
var edges = new List<ReachGraphEdge>
{
new()
{
From = "node-a",
To = "node-b",
Why = new EdgeExplanation { Type = EdgeExplanationType.DirectCall, Confidence = 0.9, Loc = "file1.cs:10" }
},
new()
{
From = "node-a",
To = "node-b",
Why = new EdgeExplanation { Type = EdgeExplanationType.DirectCall, Confidence = 0.85, Loc = "file2.cs:20" }
}
};
// Act
var result = await service.DedupeEdgesAsync(edges);
// Assert
result.Should().HaveCount(1);
result[0].EntryPointId.Should().Be("node-a");
result[0].SinkId.Should().Be("node-b");
result[0].ProvenanceCount.Should().Be(2);
}
[Fact]
public async Task DedupeEdgesAsync_WhenDisabled_ShouldPassThrough()
{
// Arrange
var options = new NoiseGateOptions { Enabled = false };
var service = CreateService(options: options);
var edges = new List<ReachGraphEdge>
{
new()
{
From = "node-a",
To = "node-b",
Why = new EdgeExplanation { Type = EdgeExplanationType.DirectCall, Confidence = 0.9 }
},
new()
{
From = "node-a",
To = "node-b",
Why = new EdgeExplanation { Type = EdgeExplanationType.DirectCall, Confidence = 0.85 }
}
};
// Act
var result = await service.DedupeEdgesAsync(edges);
// Assert
result.Should().HaveCount(2); // Not deduplicated
}
[Fact]
public async Task ResolveVerdictAsync_NewVerdict_ShouldSurface()
{
// Arrange
var service = CreateService();
var request = new VerdictResolutionRequest
{
Key = "artifact:CVE-2024-1234",
VulnerabilityId = "CVE-2024-1234",
ProductKey = "pkg:npm/lodash@4.17.21",
ProposedStatus = VexStatus.Affected,
ProposedConfidence = 0.85
};
// Act
var result = await service.ResolveVerdictAsync(request);
// Assert
result.WasSurfaced.Should().BeTrue();
result.VulnerabilityId.Should().Be("CVE-2024-1234");
result.Status.Should().Be(VexStatus.Affected);
}
[Fact]
public async Task ResolveVerdictAsync_WhenDampingDisabled_ShouldAlwaysSurface()
{
// Arrange
var options = new NoiseGateOptions { StabilityDampingEnabled = false };
var service = CreateService(options: options);
var request = new VerdictResolutionRequest
{
Key = "artifact:CVE-2024-1234",
VulnerabilityId = "CVE-2024-1234",
ProductKey = "pkg:npm/lodash@4.17.21",
ProposedStatus = VexStatus.Affected,
ProposedConfidence = 0.85
};
// Act
var result = await service.ResolveVerdictAsync(request);
// Assert
result.WasSurfaced.Should().BeTrue();
result.DampingReason.Should().BeNull();
}
[Fact]
public async Task GateAsync_ShouldDeduplicateAndResolve()
{
// Arrange
var service = CreateService();
var graph = new ReachGraphMinimal
{
SchemaVersion = "reachgraph.min@v1",
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
Scope = new ReachGraphScope(["main"], ["*"]),
Nodes = [new ReachGraphNode { Id = "node-a" }, new ReachGraphNode { Id = "node-b" }],
Edges =
[
new ReachGraphEdge
{
From = "node-a",
To = "node-b",
Why = new EdgeExplanation { Type = EdgeExplanationType.DirectCall, Confidence = 0.9 }
},
new ReachGraphEdge
{
From = "node-a",
To = "node-b",
Why = new EdgeExplanation { Type = EdgeExplanationType.DirectCall, Confidence = 0.85 }
}
],
Provenance = new ReachGraphProvenance("scanner", "1.0", _timeProvider.GetUtcNow())
};
var request = new NoiseGateRequest
{
Graph = graph,
SnapshotId = "snapshot-001",
Verdicts =
[
new VerdictResolutionRequest
{
Key = "artifact:CVE-2024-1234",
VulnerabilityId = "CVE-2024-1234",
ProductKey = "pkg:npm/lodash@4.17.21",
ProposedStatus = VexStatus.Affected,
ProposedConfidence = 0.85
}
]
};
// Act
var result = await service.GateAsync(request);
// Assert
result.SnapshotId.Should().Be("snapshot-001");
result.Edges.Should().HaveCount(1); // Deduplicated
result.Verdicts.Should().HaveCount(1);
result.Statistics.EdgeReductionPercent.Should().Be(50.0);
}
[Fact]
public async Task DiffAsync_ShouldDetectNewFindings()
{
// Arrange
var service = CreateService();
var fromSnapshot = new GatedGraphSnapshot
{
SnapshotId = "snapshot-001",
Digest = "sha256:from",
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
Edges = [],
Verdicts = [],
CreatedAt = _timeProvider.GetUtcNow(),
Statistics = new GatingStatistics
{
OriginalEdgeCount = 0,
DeduplicatedEdgeCount = 0,
TotalVerdictCount = 0,
SurfacedVerdictCount = 0,
DampedVerdictCount = 0,
Duration = TimeSpan.Zero
}
};
var toSnapshot = new GatedGraphSnapshot
{
SnapshotId = "snapshot-002",
Digest = "sha256:to",
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
Edges = [],
Verdicts =
[
new ResolvedVerdict
{
VulnerabilityId = "CVE-2024-1234",
ProductKey = "pkg:npm/lodash@4.17.21",
Status = VexStatus.Affected,
Confidence = 0.85,
WasSurfaced = true,
ResolvedAt = _timeProvider.GetUtcNow()
}
],
CreatedAt = _timeProvider.GetUtcNow(),
Statistics = new GatingStatistics
{
OriginalEdgeCount = 0,
DeduplicatedEdgeCount = 0,
TotalVerdictCount = 1,
SurfacedVerdictCount = 1,
DampedVerdictCount = 0,
Duration = TimeSpan.Zero
}
};
// Act
var delta = await service.DiffAsync(fromSnapshot, toSnapshot);
// Assert
delta.Summary.NewCount.Should().Be(1);
delta.Summary.ResolvedCount.Should().Be(0);
delta.Entries.Should().ContainSingle(e => e.Section == Delta.DeltaSection.New);
}
[Fact]
public async Task DiffAsync_ShouldDetectResolvedFindings()
{
// Arrange
var service = CreateService();
var fromSnapshot = new GatedGraphSnapshot
{
SnapshotId = "snapshot-001",
Digest = "sha256:from",
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
Edges = [],
Verdicts =
[
new ResolvedVerdict
{
VulnerabilityId = "CVE-2024-1234",
ProductKey = "pkg:npm/lodash@4.17.21",
Status = VexStatus.Affected,
Confidence = 0.85,
WasSurfaced = true,
ResolvedAt = _timeProvider.GetUtcNow()
}
],
CreatedAt = _timeProvider.GetUtcNow(),
Statistics = new GatingStatistics
{
OriginalEdgeCount = 0,
DeduplicatedEdgeCount = 0,
TotalVerdictCount = 1,
SurfacedVerdictCount = 1,
DampedVerdictCount = 0,
Duration = TimeSpan.Zero
}
};
var toSnapshot = new GatedGraphSnapshot
{
SnapshotId = "snapshot-002",
Digest = "sha256:to",
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
Edges = [],
Verdicts =
[
new ResolvedVerdict
{
VulnerabilityId = "CVE-2024-1234",
ProductKey = "pkg:npm/lodash@4.17.21",
Status = VexStatus.NotAffected,
Confidence = 0.95,
WasSurfaced = true,
Justification = VexJustification.VulnerableCodeNotPresent,
ResolvedAt = _timeProvider.GetUtcNow()
}
],
CreatedAt = _timeProvider.GetUtcNow(),
Statistics = new GatingStatistics
{
OriginalEdgeCount = 0,
DeduplicatedEdgeCount = 0,
TotalVerdictCount = 1,
SurfacedVerdictCount = 1,
DampedVerdictCount = 0,
Duration = TimeSpan.Zero
}
};
// Act
var delta = await service.DiffAsync(fromSnapshot, toSnapshot);
// Assert
delta.Summary.ResolvedCount.Should().Be(1);
delta.Entries.Should().ContainSingle(e => e.Section == Delta.DeltaSection.Resolved);
}
[Fact]
public async Task DiffAsync_ShouldDetectConfidenceChanges()
{
// Arrange
var service = CreateService();
var fromSnapshot = new GatedGraphSnapshot
{
SnapshotId = "snapshot-001",
Digest = "sha256:from",
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
Edges = [],
Verdicts =
[
new ResolvedVerdict
{
VulnerabilityId = "CVE-2024-1234",
ProductKey = "pkg:npm/lodash@4.17.21",
Status = VexStatus.Affected,
Confidence = 0.50,
WasSurfaced = true,
ResolvedAt = _timeProvider.GetUtcNow()
}
],
CreatedAt = _timeProvider.GetUtcNow(),
Statistics = new GatingStatistics
{
OriginalEdgeCount = 0,
DeduplicatedEdgeCount = 0,
TotalVerdictCount = 1,
SurfacedVerdictCount = 1,
DampedVerdictCount = 0,
Duration = TimeSpan.Zero
}
};
var toSnapshot = new GatedGraphSnapshot
{
SnapshotId = "snapshot-002",
Digest = "sha256:to",
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
Edges = [],
Verdicts =
[
new ResolvedVerdict
{
VulnerabilityId = "CVE-2024-1234",
ProductKey = "pkg:npm/lodash@4.17.21",
Status = VexStatus.Affected,
Confidence = 0.90, // Large increase
WasSurfaced = true,
ResolvedAt = _timeProvider.GetUtcNow()
}
],
CreatedAt = _timeProvider.GetUtcNow(),
Statistics = new GatingStatistics
{
OriginalEdgeCount = 0,
DeduplicatedEdgeCount = 0,
TotalVerdictCount = 1,
SurfacedVerdictCount = 1,
DampedVerdictCount = 0,
Duration = TimeSpan.Zero
}
};
// Act
var delta = await service.DiffAsync(fromSnapshot, toSnapshot);
// Assert
delta.Summary.ConfidenceUpCount.Should().Be(1);
delta.Entries.Should().ContainSingle(e => e.Section == Delta.DeltaSection.ConfidenceUp);
}
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;
}
}

View File

@@ -0,0 +1,25 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.VexLens.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Moq" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../StellaOps.VexLens/StellaOps.VexLens.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>