save checkpoint: save features

This commit is contained in:
master
2026-02-12 10:27:23 +02:00
parent dca86e1248
commit 5bca406787
8837 changed files with 1796879 additions and 5294 deletions

View File

@@ -0,0 +1,61 @@
using StellaOps.Unknowns.Core.Models;
namespace StellaOps.Unknowns.Services;
/// <summary>
/// Shared SLA band and timing calculations used by monitor and health-check services.
/// </summary>
public static class SlaCalculator
{
/// <summary>
/// Maps a queue score to SLA band.
/// </summary>
public static UnknownsBand GetBand(double score)
{
return score switch
{
>= 0.70 => UnknownsBand.Hot,
>= 0.40 => UnknownsBand.Warm,
_ => UnknownsBand.Cold
};
}
/// <summary>
/// Returns configured SLA duration for a band.
/// </summary>
public static TimeSpan GetSlaLimit(UnknownsBand band, UnknownsSlaOptions options)
{
ArgumentNullException.ThrowIfNull(options);
return band switch
{
UnknownsBand.Hot => options.HotSla,
UnknownsBand.Warm => options.WarmSla,
_ => options.ColdSla
};
}
/// <summary>
/// Computes remaining SLA time for a queue entry.
/// </summary>
public static TimeSpan CalculateRemaining(GreyQueueEntry entry, DateTimeOffset now, UnknownsSlaOptions options)
{
var band = GetBand(entry.Score);
var slaLimit = GetSlaLimit(band, options);
return slaLimit - (now - entry.CreatedAt);
}
/// <summary>
/// Computes elapsed SLA percentage for a queue entry.
/// </summary>
public static double CalculatePercentElapsed(GreyQueueEntry entry, DateTimeOffset now, UnknownsSlaOptions options)
{
var band = GetBand(entry.Score);
var slaLimit = GetSlaLimit(band, options);
if (slaLimit <= TimeSpan.Zero)
{
return double.PositiveInfinity;
}
return (now - entry.CreatedAt) / slaLimit;
}
}

View File

@@ -23,13 +23,10 @@
<ProjectReference Include="..\__Libraries\StellaOps.Unknowns.Persistence\StellaOps.Unknowns.Persistence.csproj" />
</ItemGroup>
<!-- TODO: Re-enable after aligning service APIs with IGreyQueueRepository -->
<!-- Keep legacy watchdog/lifecycle services excluded until their repository API alignment is completed. -->
<ItemGroup>
<Compile Remove="GreyQueueWatchdogService.cs" />
<Compile Remove="UnknownsLifecycleService.cs" />
<Compile Remove="UnknownsMetricsService.cs" />
<Compile Remove="UnknownsSlaHealthCheck.cs" />
<Compile Remove="UnknownsSlaMonitorService.cs" />
</ItemGroup>
</Project>

View File

@@ -96,7 +96,7 @@ public sealed class UnknownsSlaMonitorService : BackgroundService
await _notificationPublisher.PublishAsync(new SlaWarningNotification
{
EntryId = entry.Id,
BomRef = entry.BomRef,
BomRef = entry.BomRef ?? string.Empty,
Band = band,
PercentElapsed = percentElapsed * 100,
RemainingTime = remaining
@@ -110,7 +110,7 @@ public sealed class UnknownsSlaMonitorService : BackgroundService
await _notificationPublisher.PublishAsync(new SlaBreachNotification
{
EntryId = entry.Id,
BomRef = entry.BomRef,
BomRef = entry.BomRef ?? string.Empty,
Band = band,
OverdueBy = elapsed - slaLimit
}, ct);
@@ -170,21 +170,6 @@ public sealed record UnknownsSlaOptions
public double WarningThreshold { get; init; } = 0.80;
}
/// <summary>
/// Unknowns band classification.
/// </summary>
public enum UnknownsBand
{
/// <summary>High priority (score >= 0.70).</summary>
Hot,
/// <summary>Medium priority (score 0.40-0.69).</summary>
Warm,
/// <summary>Low priority (score < 0.40).</summary>
Cold
}
/// <summary>
/// SLA warning notification.
/// </summary>

View File

@@ -0,0 +1,91 @@
using FluentAssertions;
using StellaOps.Policy.Scoring;
using StellaOps.Unknowns.Core.Models;
using StellaOps.Unknowns.Core.Services;
using Xunit;
namespace StellaOps.Unknowns.Core.Tests.Services;
public sealed class UnknownProofEmitterTests
{
[Fact]
public void EmitProof_WithContainmentDeduction_EmitsContainmentAndFinalScoreNodes()
{
var unknown = CreateUnknown();
var blast = new BlastRadius(120, true, "root");
var pressure = new ExploitPressure(0.82, true);
var containment = ContainmentSignals.WellSandboxed; // non-zero deduction
var ranker = new UnknownRanker();
var finalScore = ranker.Rank(blast, unknown.UncertaintyScore, pressure, containment);
var emitter = new UnknownProofEmitter();
var proof = emitter.EmitProof(unknown, blast, pressure, containment, finalScore);
proof.Count.Should().Be(6);
proof.Nodes[0].RuleId.Should().Be("unknown.input");
proof.Nodes[0].Kind.Should().Be(ProofNodeKind.Input);
proof.Nodes[4].RuleId.Should().Be("unknown.containment");
proof.Nodes[4].Delta.Should().BeLessThan(0);
proof.Nodes[5].RuleId.Should().Be("unknown.final_score");
proof.Nodes[5].Kind.Should().Be(ProofNodeKind.Score);
proof.Nodes[5].Total.Should().Be(finalScore);
proof.VerifyIntegrity().Should().BeTrue();
}
[Fact]
public void EmitProof_WithoutContainmentDeduction_SkipsContainmentNode()
{
var unknown = CreateUnknown();
var blast = new BlastRadius(10, false, "none");
var pressure = new ExploitPressure(0.1, false);
var containment = new ContainmentSignals("disabled", "rw", "disabled", 0); // zero deduction
var ranker = new UnknownRanker();
var finalScore = ranker.Rank(blast, unknown.UncertaintyScore, pressure, containment);
var emitter = new UnknownProofEmitter();
var proof = emitter.EmitProof(unknown, blast, pressure, containment, finalScore);
proof.Count.Should().Be(5);
proof.Nodes.Select(n => n.RuleId).Should().NotContain("unknown.containment");
proof.Nodes[^1].RuleId.Should().Be("unknown.final_score");
proof.Nodes[^1].ParentIds.Should().ContainSingle().Which.Should().EndWith("-pressure");
}
[Fact]
public void RankWithProof_ReturnsItemAndProofWithMatchingFinalScore()
{
var unknown = CreateUnknown();
var blast = new BlastRadius(42, true, "admin");
var pressure = new ExploitPressure(0.6, false);
var containment = new ContainmentSignals("audit", "rw", "audit", 5);
IUnknownRanker ranker = new UnknownRanker();
IUnknownProofEmitter emitter = new UnknownProofEmitter();
var result = ranker.RankWithProof(emitter, unknown, blast, pressure, containment);
result.Item.Id.Should().Be(unknown.Id.ToString());
result.Proof.Count.Should().BeGreaterThan(0);
result.Proof.Nodes[^1].RuleId.Should().Be("unknown.final_score");
result.Proof.Nodes[^1].Total.Should().Be(result.Item.Score);
}
private static Unknown CreateUnknown()
{
var now = new DateTimeOffset(2026, 2, 11, 10, 0, 0, TimeSpan.Zero);
return new Unknown
{
Id = Guid.Parse("d20da73d-a148-48cd-8fd5-f721bd6b78ed"),
TenantId = "tenant-qa",
SubjectHash = "sha256:unknown-hash",
SubjectType = UnknownSubjectType.Binary,
SubjectRef = "/usr/lib/libssl.so.1.1",
Kind = UnknownKind.MissingBuildId,
Severity = UnknownSeverity.Medium,
ValidFrom = now,
SysFrom = now,
CreatedAt = now,
CreatedBy = "qa-test",
UncertaintyScore = 0.65
};
}
}

View File

@@ -0,0 +1,144 @@
using FluentAssertions;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using StellaOps.Unknowns.Core.Models;
using StellaOps.Unknowns.Core.Repositories;
using StellaOps.Unknowns.Services;
using System.Reflection;
using Xunit;
namespace StellaOps.Unknowns.Core.Tests.Services;
public sealed class UnknownsSlaBehaviorTests
{
private static readonly UnknownsSlaOptions DefaultOptions = new()
{
PollingInterval = TimeSpan.FromMilliseconds(50),
HotSla = TimeSpan.FromHours(24),
WarmSla = TimeSpan.FromDays(7),
ColdSla = TimeSpan.FromDays(30),
WarningThreshold = 0.80
};
[Fact]
public async Task HealthCheck_ReturnsHealthy_WhenAllPendingEntriesWithinSla()
{
var now = new DateTimeOffset(2026, 2, 11, 11, 0, 0, TimeSpan.Zero);
var repository = Substitute.For<IGreyQueueRepository>();
repository.GetPendingAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<GreyQueueEntry>>([CreateEntry(now.AddHours(-2), 0.80)]));
var sut = new UnknownsSlaHealthCheck(repository, Options.Create(DefaultOptions), new FrozenTimeProvider(now));
var result = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None);
result.Status.Should().Be(HealthStatus.Healthy);
}
[Fact]
public async Task HealthCheck_ReturnsDegraded_WhenPendingEntryIsNearSlaLimit()
{
var now = new DateTimeOffset(2026, 2, 11, 11, 0, 0, TimeSpan.Zero);
var repository = Substitute.For<IGreyQueueRepository>();
repository.GetPendingAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<GreyQueueEntry>>([CreateEntry(now.AddHours(-20), 0.80)]));
var sut = new UnknownsSlaHealthCheck(repository, Options.Create(DefaultOptions), new FrozenTimeProvider(now));
var result = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None);
result.Status.Should().Be(HealthStatus.Degraded);
}
[Fact]
public async Task HealthCheck_ReturnsUnhealthy_WhenPendingEntryBreachesSla()
{
var now = new DateTimeOffset(2026, 2, 11, 11, 0, 0, TimeSpan.Zero);
var repository = Substitute.For<IGreyQueueRepository>();
repository.GetPendingAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<GreyQueueEntry>>([CreateEntry(now.AddHours(-30), 0.80)]));
var sut = new UnknownsSlaHealthCheck(repository, Options.Create(DefaultOptions), new FrozenTimeProvider(now));
var result = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None);
result.Status.Should().Be(HealthStatus.Unhealthy);
}
[Fact]
public async Task SlaMonitor_PublishesWarningAndBreachNotifications()
{
var now = new DateTimeOffset(2026, 2, 11, 11, 0, 0, TimeSpan.Zero);
var repository = Substitute.For<IGreyQueueRepository>();
var notifications = new CaptureNotificationPublisher();
var warning = CreateEntry(now.AddHours(-20), 0.80); // 83% of 24h
var breach = CreateEntry(now.AddHours(-30), 0.80); // 125% of 24h
repository.GetPendingAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<GreyQueueEntry>>([warning, breach]));
var sut = new UnknownsSlaMonitorService(
repository,
notifications,
Options.Create(DefaultOptions),
new FrozenTimeProvider(now),
new UnknownsMetrics(),
NullLogger<UnknownsSlaMonitorService>.Instance);
var privateMethod = typeof(UnknownsSlaMonitorService).GetMethod(
"CheckSlaBoundsAsync",
BindingFlags.Instance | BindingFlags.NonPublic);
privateMethod.Should().NotBeNull();
var task = (Task?)privateMethod!.Invoke(sut, [CancellationToken.None]);
task.Should().NotBeNull();
await task!;
notifications.Warnings.Should().ContainSingle(x => x.EntryId == warning.Id);
notifications.Breaches.Should().ContainSingle(x => x.EntryId == breach.Id);
}
private static GreyQueueEntry CreateEntry(DateTimeOffset createdAt, double score)
{
return new GreyQueueEntry
{
Id = Guid.NewGuid(),
TenantId = "test-tenant",
UnknownId = Guid.NewGuid(),
BomRef = "pkg:npm/example@1.0.0",
Fingerprint = $"sha256:{Guid.NewGuid():N}",
Status = GreyQueueStatus.Pending,
Priority = 100,
Score = score,
Reason = GreyQueueReason.InsufficientVex,
CreatedAt = createdAt,
CreatedBy = "qa-test"
};
}
private sealed class FrozenTimeProvider(DateTimeOffset now) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => now;
}
private sealed class CaptureNotificationPublisher : INotificationPublisher
{
public List<SlaWarningNotification> Warnings { get; } = [];
public List<SlaBreachNotification> Breaches { get; } = [];
public Task PublishAsync<T>(T notification, CancellationToken ct = default)
{
if (notification is SlaWarningNotification warning)
{
Warnings.Add(warning);
}
else if (notification is SlaBreachNotification breach)
{
Breaches.Add(breach);
}
return Task.CompletedTask;
}
}
}