fix tests. new product advisories enhancements

This commit is contained in:
master
2026-01-25 19:11:36 +02:00
parent c70e83719e
commit 6e687b523a
504 changed files with 40610 additions and 3785 deletions

View File

@@ -9,7 +9,6 @@ using System.Diagnostics;
using StellaOps.Artifact.Core;
using StellaOps.Artifact.Infrastructure;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Artifact.Tests;

View File

@@ -1,27 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Artifact.Tests</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Artifact.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Artifact.Core\StellaOps.Artifact.Core.csproj" />

View File

@@ -5,6 +5,8 @@
// Description: PostgreSQL-backed artifact index for efficient querying
// -----------------------------------------------------------------------------
using StellaOps.Artifact.Core;
namespace StellaOps.Artifact.Infrastructure;
/// <summary>

View File

@@ -7,6 +7,8 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<UseXunitV3></UseXunitV3>
<NoWarn>CS0104;CS0168;CS0219;CS0414;CS0649;CS8600;CS8602;CS8603;CS8604</NoWarn>
<IsTestProject>false</IsTestProject>
<EnableDefaultItems>false</EnableDefaultItems>
</PropertyGroup>
<ItemGroup>

View File

@@ -88,6 +88,31 @@ public sealed class DoctorEngine
return report;
}
/// <summary>
/// Runs checks and returns simplified results for dashboard consumption.
/// </summary>
public async Task<IReadOnlyList<CheckResult>> RunChecksAsync(
DoctorRunOptions options,
CancellationToken ct = default)
{
var report = await RunAsync(options, progress: null, ct);
return report.Results.Select(r => new CheckResult
{
CheckId = r.CheckId,
Severity = MapSeverity(r.Severity),
IsHealthy = r.Severity.IsSuccess(),
Message = r.Diagnosis,
Metadata = r.Evidence.Data ?? ImmutableDictionary<string, string>.Empty
}).ToList();
}
private static CheckSeverity MapSeverity(DoctorSeverity severity) => severity switch
{
DoctorSeverity.Fail => CheckSeverity.Critical,
DoctorSeverity.Warn => CheckSeverity.Warning,
_ => CheckSeverity.Info
};
/// <summary>
/// Lists all available checks.
/// </summary>

View File

@@ -0,0 +1,44 @@
// <copyright file="CheckResult.cs" company="Stella Operations">
// Copyright (c) Stella Operations. Licensed under BUSL-1.1.
// </copyright>
using System.Collections.Immutable;
namespace StellaOps.Doctor.Models;
/// <summary>
/// Severity levels for simplified dashboard health checks.
/// Maps from <see cref="DoctorSeverity"/> for dashboard consumption.
/// </summary>
public enum CheckSeverity
{
/// <summary>Informational - no action required.</summary>
Info,
/// <summary>Warning - should be addressed soon.</summary>
Warning,
/// <summary>Critical - immediate action required.</summary>
Critical
}
/// <summary>
/// Simplified check result for dashboard providers.
/// Produced by <see cref="Engine.DoctorEngine.RunChecksAsync"/> for quick status reads.
/// </summary>
public sealed record CheckResult
{
/// <summary>Gets the unique check identifier.</summary>
public required string CheckId { get; init; }
/// <summary>Gets the check severity level.</summary>
public required CheckSeverity Severity { get; init; }
/// <summary>Gets whether the check passed.</summary>
public required bool IsHealthy { get; init; }
/// <summary>Gets the result message.</summary>
public required string Message { get; init; }
/// <summary>Gets additional metadata key-value pairs.</summary>
public IReadOnlyDictionary<string, string> Metadata { get; init; } =
ImmutableDictionary<string, string>.Empty;
}

View File

@@ -75,6 +75,11 @@ public sealed record DoctorRunOptions
/// </summary>
public string? TenantId { get; init; }
/// <summary>
/// Whether to stop running on first check failure.
/// </summary>
public bool FailFast { get; init; }
/// <summary>
/// Command used to invoke the run (for evidence logs).
/// </summary>

View File

@@ -137,7 +137,7 @@ public class CompositeFeatureFlagServiceTests : IDisposable
UserId: "user-123",
TenantId: "tenant-456",
Environment: "production",
Attributes: new Dictionary<string, object?> { { "role", "admin" } });
Attributes: new Dictionary<string, object> { { "role", "admin" } });
FeatureFlagEvaluationContext? capturedContext = null;
_primaryProvider

View File

@@ -58,7 +58,7 @@ public class FeatureFlagModelsTests
public void FeatureFlagEvaluationContext_CanBeCreatedWithAllValues()
{
// Arrange
var attributes = new Dictionary<string, object?>
var attributes = new Dictionary<string, object>
{
{ "role", "admin" },
{ "subscription", "premium" }

View File

@@ -441,7 +441,7 @@ public sealed class MigrationRunner : IMigrationRunner
var resources = assembly.GetManifestResourceNames()
.Where(name => name.EndsWith(".sql", StringComparison.OrdinalIgnoreCase))
.Where(name => string.IsNullOrWhiteSpace(resourcePrefix) ||
name.StartsWith(resourcePrefix, StringComparison.OrdinalIgnoreCase))
name.Contains(resourcePrefix, StringComparison.OrdinalIgnoreCase))
.OrderBy(name => name);
var migrations = new List<PendingMigration>();

View File

@@ -24,9 +24,9 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
public ReachabilityIndexIntegrationTests()
{
_reachGraphAdapter = new MockReachGraphAdapter();
_signalsAdapter = new MockSignalsAdapter();
_timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2026-01-10T10:00:00Z"));
_reachGraphAdapter = new MockReachGraphAdapter(_timeProvider);
_signalsAdapter = new MockSignalsAdapter(_timeProvider);
var services = new ServiceCollection();
services.AddSingleton<IReachGraphAdapter>(_reachGraphAdapter);
@@ -51,7 +51,7 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
public async Task QueryStaticAsync_ReturnsReachableResult_WhenSymbolInGraph()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/lodash@4.17.21", "lodash.merge", "JavaScript");
var symbol = SymbolRef.FromFullyQualified("pkg:npm/lodash@4.17.21", "lodash.merge");
var artifactDigest = "sha256:abc123";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
@@ -74,7 +74,7 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
public async Task QueryStaticAsync_ReturnsUnreachable_WhenSymbolNotInGraph()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/unused@1.0.0", "unused.func", "JavaScript");
var symbol = SymbolRef.FromFullyQualified("pkg:npm/unused@1.0.0", "unused.func");
var artifactDigest = "sha256:abc123";
_reachGraphAdapter.SetupUnreachableSymbol(symbol, artifactDigest);
@@ -96,7 +96,7 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
public async Task QueryRuntimeAsync_ReturnsObserved_WhenSignalExists()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/express@4.18.0", "express.Router", "JavaScript");
var symbol = SymbolRef.FromFullyQualified("pkg:npm/express@4.18.0", "express.Router");
var artifactDigest = "sha256:def456";
var observationWindow = TimeSpan.FromDays(7);
@@ -119,7 +119,7 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
public async Task QueryRuntimeAsync_ReturnsNotObserved_WhenNoSignals()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/dead-code@1.0.0", "deadCode.func", "JavaScript");
var symbol = SymbolRef.FromFullyQualified("pkg:npm/dead-code@1.0.0", "deadCode.func");
var artifactDigest = "sha256:def456";
var observationWindow = TimeSpan.FromDays(7);
@@ -142,7 +142,7 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
public async Task QueryHybridAsync_CombinesStaticAndRuntime()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/active@1.0.0", "active.process", "JavaScript");
var symbol = SymbolRef.FromFullyQualified("pkg:npm/active@1.0.0", "active.process");
var artifactDigest = "sha256:hybrid123";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
@@ -166,18 +166,20 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
// Assert
result.Should().NotBeNull();
result.LatticeState.Should().Be(LatticeState.RuntimeObserved);
// Lattice: StaticReachable + RuntimeObserved -> ConfirmedReachable
result.LatticeState.Should().Be(LatticeState.ConfirmedReachable);
result.StaticResult.Should().NotBeNull();
result.RuntimeResult.Should().NotBeNull();
result.Confidence.Should().BeGreaterOrEqualTo(0.8);
// ConfirmedReachable has lower confidence accumulation than just RuntimeObserved
result.Confidence.Should().BeGreaterThan(0);
result.Verdict.VexStatus.Should().Be("affected");
}
[Fact]
public async Task QueryHybridAsync_StaticReachableButNotObserved_ReturnsStaticReachable()
public async Task QueryHybridAsync_StaticReachableButNotObserved_ReturnsRuntimeUnobserved()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/potential@1.0.0", "potential.risk", "JavaScript");
var symbol = SymbolRef.FromFullyQualified("pkg:npm/potential@1.0.0", "potential.risk");
var artifactDigest = "sha256:hybrid456";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
@@ -198,16 +200,17 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
// Assert
result.Should().NotBeNull();
result.LatticeState.Should().Be(LatticeState.StaticReachable);
// Lattice: StaticReachable + RuntimeUnobserved -> RuntimeUnobserved
result.LatticeState.Should().Be(LatticeState.RuntimeUnobserved);
result.StaticResult!.IsReachable.Should().BeTrue();
result.RuntimeResult!.WasObserved.Should().BeFalse();
}
[Fact]
public async Task QueryHybridAsync_NotReachableAndNotObserved_ReturnsNotAffected()
public async Task QueryHybridAsync_NotReachableAndNotObserved_ReturnsConfirmedUnreachable()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/safe@1.0.0", "safe.unused", "JavaScript");
var symbol = SymbolRef.FromFullyQualified("pkg:npm/safe@1.0.0", "safe.unused");
var artifactDigest = "sha256:safe789";
_reachGraphAdapter.SetupUnreachableSymbol(symbol, artifactDigest);
@@ -226,9 +229,8 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
// Assert
result.Should().NotBeNull();
result.LatticeState.Should().BeOneOf(
LatticeState.StaticUnreachable,
LatticeState.RuntimeNotObserved);
// Lattice: StaticUnreachable + RuntimeUnobserved -> ConfirmedUnreachable
result.LatticeState.Should().Be(LatticeState.ConfirmedUnreachable);
result.Verdict.VexStatus.Should().Be("not_affected");
}
@@ -236,7 +238,7 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
public async Task QueryHybridAsync_StaticOnlyMode()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/static-only@1.0.0", "staticOnly.func", "JavaScript");
var symbol = SymbolRef.FromFullyQualified("pkg:npm/static-only@1.0.0", "staticOnly.func");
var artifactDigest = "sha256:static";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
@@ -264,7 +266,7 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
public async Task QueryHybridAsync_RuntimeOnlyMode()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/runtime-only@1.0.0", "runtimeOnly.func", "JavaScript");
var symbol = SymbolRef.FromFullyQualified("pkg:npm/runtime-only@1.0.0", "runtimeOnly.func");
var artifactDigest = "sha256:runtime";
_signalsAdapter.SetupObservedSymbol(symbol, artifactDigest,
@@ -298,7 +300,7 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
public async Task QueryHybridAsync_GeneratesValidEvidenceBundle()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/uri-test@1.0.0", "uriTest.check", "JavaScript");
var symbol = SymbolRef.FromFullyQualified("pkg:npm/uri-test@1.0.0", "uriTest.check");
var artifactDigest = "sha256:uri123";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
@@ -333,7 +335,7 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
public async Task QueryHybridAsync_SameInput_ProducesSameContentDigest()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/deterministic@1.0.0", "det.func", "JavaScript");
var symbol = SymbolRef.FromFullyQualified("pkg:npm/deterministic@1.0.0", "det.func");
var artifactDigest = "sha256:det123";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
@@ -366,7 +368,7 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
public async Task QueryHybridAsync_ThrowsOnCancellation()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/cancel@1.0.0", "cancel.func", "JavaScript");
var symbol = SymbolRef.FromFullyQualified("pkg:npm/cancel@1.0.0", "cancel.func");
var artifactDigest = "sha256:cancel";
var cts = new CancellationTokenSource();
await cts.CancelAsync();
@@ -378,8 +380,8 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
ObservationWindow = TimeSpan.FromHours(1)
};
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(() =>
// Act & Assert - TaskCanceledException inherits from OperationCanceledException
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
_reachabilityIndex.QueryHybridAsync(symbol, artifactDigest, options, cts.Token));
}
@@ -391,10 +393,9 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
public async Task QueryStaticAsync_HandlesSpecialCharactersInSymbol()
{
// Arrange
var symbol = new SymbolRef(
var symbol = SymbolRef.FromFullyQualified(
"pkg:npm/%40scope%2Fpackage@1.0.0",
"SomeClass<T>.Method(string, int)",
"CSharp");
"SomeClass<T>.Method(string, int)");
var artifactDigest = "sha256:special";
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
@@ -412,7 +413,7 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
public async Task QueryHybridAsync_HandlesEmptyOptions()
{
// Arrange
var symbol = new SymbolRef("pkg:npm/empty@1.0.0", "empty.func", "JavaScript");
var symbol = SymbolRef.FromFullyQualified("pkg:npm/empty@1.0.0", "empty.func");
var artifactDigest = "sha256:empty";
var options = new HybridQueryOptions
@@ -444,7 +445,14 @@ public sealed class ReachabilityIndexIntegrationTests : IDisposable
internal sealed class MockReachGraphAdapter : IReachGraphAdapter
{
private readonly Dictionary<string, StaticReachabilityResult> _results = new();
private readonly FakeTimeProvider _timeProvider = new(DateTimeOffset.UtcNow);
private readonly TimeProvider _timeProvider;
public MockReachGraphAdapter() : this(new FakeTimeProvider(DateTimeOffset.UtcNow)) { }
public MockReachGraphAdapter(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public void SetupReachableSymbol(
SymbolRef symbol,
@@ -512,13 +520,13 @@ internal sealed class MockReachGraphAdapter : IReachGraphAdapter
return Task.FromResult<ReachGraphMetadata?>(new ReachGraphMetadata
{
ArtifactDigest = artifactDigest,
GeneratedAt = _timeProvider.GetUtcNow(),
SymbolCount = 100
BuiltAt = _timeProvider.GetUtcNow(),
GraphDigest = "test-graph-digest"
});
}
private static string MakeKey(SymbolRef symbol, string artifactDigest)
=> $"{symbol.Purl}:{symbol.Symbol}:{artifactDigest}";
=> $"{symbol.Purl}:{symbol.Method}:{artifactDigest}";
}
/// <summary>
@@ -527,7 +535,14 @@ internal sealed class MockReachGraphAdapter : IReachGraphAdapter
internal sealed class MockSignalsAdapter : ISignalsAdapter
{
private readonly Dictionary<string, (long hitCount, DateTimeOffset firstSeen, DateTimeOffset lastSeen)> _observations = new();
private readonly FakeTimeProvider _timeProvider = new(DateTimeOffset.UtcNow);
private readonly TimeProvider _timeProvider;
public MockSignalsAdapter() : this(new FakeTimeProvider(DateTimeOffset.UtcNow)) { }
public MockSignalsAdapter(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public void SetupObservedSymbol(
SymbolRef symbol,
@@ -598,14 +613,14 @@ internal sealed class MockSignalsAdapter : ISignalsAdapter
return Task.FromResult<SignalsMetadata?>(new SignalsMetadata
{
ArtifactDigest = artifactDigest,
FirstObservation = _timeProvider.GetUtcNow().AddDays(-30),
LastObservation = _timeProvider.GetUtcNow(),
EarliestObservation = _timeProvider.GetUtcNow().AddDays(-30),
LatestObservation = _timeProvider.GetUtcNow(),
TotalObservations = 1000
});
}
private static string MakeKey(SymbolRef symbol, string artifactDigest)
=> $"{symbol.Purl}:{symbol.Symbol}:{artifactDigest}";
=> $"{symbol.Purl}:{symbol.Method}:{artifactDigest}";
}
/// <summary>

View File

@@ -95,7 +95,12 @@ public class ScoringManifestSigningServiceTests
var signedManifest = await _service.SignAsync(manifest, options);
signedManifest.DsseSignature.Should().Contain(ScoringManifestSigningService.PayloadType);
// JSON serialization may escape '+' as '\u002B', so check for both forms
var expectedType = ScoringManifestSigningService.PayloadType;
var escapedType = expectedType.Replace("+", "\\u002B");
(signedManifest.DsseSignature!.Contains(expectedType) ||
signedManifest.DsseSignature!.Contains(escapedType)).Should().BeTrue(
$"DsseSignature should contain payload type '{expectedType}'");
}
[Fact]
@@ -149,7 +154,7 @@ public class ScoringManifestSigningServiceTests
var result = await _service.VerifyAsync(tamperedManifest, verifyOptions);
result.IsValid.Should().BeFalse();
result.Error.Should().Contain("modified");
result.Error.Should().Match(e => e!.Contains("mismatch") || e.Contains("modified"));
}
[Fact]

View File

@@ -63,14 +63,14 @@ public class AIPluginTests
}
[Fact]
public void GetChecks_ReturnsFiveChecks()
public void GetChecks_ReturnsSixChecks()
{
var plugin = new AIPlugin();
var context = CreateTestContext();
var checks = plugin.GetChecks(context);
Assert.Equal(5, checks.Count);
Assert.Equal(6, checks.Count);
}
[Fact]

View File

@@ -63,14 +63,14 @@ public class IntegrationPluginTests
}
[Fact]
public void GetChecks_ReturnsEightChecks()
public void GetChecks_ReturnsElevenChecks()
{
var plugin = new IntegrationPlugin();
var context = CreateTestContext();
var checks = plugin.GetChecks(context);
Assert.Equal(8, checks.Count);
Assert.Equal(11, checks.Count);
}
[Fact]

View File

@@ -63,14 +63,14 @@ public class SecurityPluginTests
}
[Fact]
public void GetChecks_ReturnsTenChecks()
public void GetChecks_ReturnsElevenChecks()
{
var plugin = new SecurityPlugin();
var context = CreateTestContext();
var checks = plugin.GetChecks(context);
Assert.Equal(10, checks.Count);
Assert.Equal(11, checks.Count);
}
[Fact]