Refactor code structure and optimize performance across multiple modules

This commit is contained in:
StellaOps Bot
2025-12-26 20:03:22 +02:00
parent c786faae84
commit f10d83c444
1385 changed files with 69732 additions and 10280 deletions

View File

@@ -120,6 +120,53 @@ public sealed class GraphJobRepository : IGraphJobRepository
public ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, CancellationToken cancellationToken)
=> ListOverlayJobsAsync(tenantId, status: null, limit: 50, cancellationToken);
// Cross-tenant overloads for background services - scans all tenants
public async ValueTask<IReadOnlyCollection<GraphBuildJob>> ListBuildJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken)
{
var sql = "SELECT payload FROM scheduler.graph_jobs WHERE type=@Type";
if (status is not null)
{
sql += " AND status=@Status";
}
sql += " ORDER BY created_at LIMIT @Limit";
await using var conn = await _dataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
var results = await conn.QueryAsync<string>(sql, new
{
Type = (short)GraphJobQueryType.Build,
Status = status is not null ? (short)status : (short?)null,
Limit = limit
});
return results
.Select(r => CanonicalJsonSerializer.Deserialize<GraphBuildJob>(r))
.Where(r => r is not null)!
.ToArray()!;
}
public async ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken)
{
var sql = "SELECT payload FROM scheduler.graph_jobs WHERE type=@Type";
if (status is not null)
{
sql += " AND status=@Status";
}
sql += " ORDER BY created_at LIMIT @Limit";
await using var conn = await _dataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
var results = await conn.QueryAsync<string>(sql, new
{
Type = (short)GraphJobQueryType.Overlay,
Status = status is not null ? (short)status : (short?)null,
Limit = limit
});
return results
.Select(r => CanonicalJsonSerializer.Deserialize<GraphOverlayJob>(r))
.Where(r => r is not null)!
.ToArray()!;
}
public async ValueTask<bool> TryReplaceAsync(GraphBuildJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken)
{
const string sql = @"UPDATE scheduler.graph_jobs

View File

@@ -19,4 +19,8 @@ public interface IGraphJobRepository
ValueTask<IReadOnlyCollection<GraphBuildJob>> ListBuildJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken);
ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken);
ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, CancellationToken cancellationToken);
// Cross-tenant overloads for background services
ValueTask<IReadOnlyCollection<GraphBuildJob>> ListBuildJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken);
ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken);
}

View File

@@ -58,4 +58,9 @@ public interface IJobHistoryRepository
/// Deletes old history entries.
/// </summary>
Task<int> DeleteOlderThanAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default);
/// <summary>
/// Gets recent failed jobs across all tenants for background indexing.
/// </summary>
Task<IReadOnlyList<JobHistoryEntity>> GetRecentFailedAsync(int limit, CancellationToken cancellationToken = default);
}

View File

@@ -210,6 +210,31 @@ public sealed class JobHistoryRepository : RepositoryBase<SchedulerDataSource>,
return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IReadOnlyList<JobHistoryEntity>> GetRecentFailedAsync(int limit, CancellationToken cancellationToken = default)
{
const string sql = """
SELECT id, job_id, tenant_id, project_id, job_type, status, attempt, payload_digest,
result, reason, worker_id, duration_ms, created_at, completed_at, archived_at
FROM scheduler.job_history
WHERE status = 'failed'::scheduler.job_status OR status = 'timed_out'::scheduler.job_status
ORDER BY completed_at DESC
LIMIT @limit
""";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "limit", limit);
var results = new List<JobHistoryEntity>();
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
results.Add(MapJobHistory(reader));
}
return results;
}
private static JobHistoryEntity MapJobHistory(NpgsqlDataReader reader) => new()
{
Id = reader.GetInt64(reader.GetOrdinal("id")),

View File

@@ -89,8 +89,7 @@ public sealed class PartitionMaintenanceWorker : BackgroundService
_logger.LogInformation("Starting partition maintenance cycle");
await using var conn = await _dataSource.GetConnectionAsync(ct);
await conn.OpenAsync(ct);
await using var conn = await _dataSource.OpenSystemConnectionAsync(ct);
foreach (var (schemaTable, _) in opts.ManagedTables)
{

View File

@@ -7,7 +7,6 @@ using Microsoft.Extensions.Logging;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Queue;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
using StellaOps.Scheduler.Worker.Events;
using StellaOps.Scheduler.Worker.Observability;

View File

@@ -1,3 +1,4 @@
using System.Text.Json;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -94,10 +95,13 @@ public sealed class FailureSignatureIndexer : BackgroundService
_logger.LogDebug("Starting failure indexing batch");
// Get recent failed jobs that haven't been indexed
var failedJobs = await _historyRepository.GetRecentFailedJobsAsync(
var historyEntries = await _historyRepository.GetRecentFailedAsync(
_options.Value.BatchSize,
ct);
// Convert history entries to failed job records
var failedJobs = historyEntries.Select(ConvertToFailedJobRecord).ToList();
var indexed = 0;
foreach (var job in failedJobs)
{
@@ -278,6 +282,58 @@ public sealed class FailureSignatureIndexer : BackgroundService
return (errorCode, ErrorCategory.Unknown);
}
private static FailedJobRecord ConvertToFailedJobRecord(JobHistoryEntity entity)
{
// Try to extract additional details from the result JSON
string? imageDigest = null;
string? artifactDigest = null;
string? repository = null;
string? errorCode = null;
string? scannerVersion = null;
string? runtimeVersion = null;
if (!string.IsNullOrWhiteSpace(entity.Result))
{
try
{
using var doc = JsonDocument.Parse(entity.Result);
var root = doc.RootElement;
if (root.TryGetProperty("imageDigest", out var imgProp))
imageDigest = imgProp.GetString();
if (root.TryGetProperty("artifactDigest", out var artProp))
artifactDigest = artProp.GetString();
if (root.TryGetProperty("repository", out var repoProp))
repository = repoProp.GetString();
if (root.TryGetProperty("errorCode", out var codeProp))
errorCode = codeProp.GetString();
if (root.TryGetProperty("scannerVersion", out var scanVerProp))
scannerVersion = scanVerProp.GetString();
if (root.TryGetProperty("runtimeVersion", out var rtVerProp))
runtimeVersion = rtVerProp.GetString();
}
catch (JsonException)
{
// Result is not valid JSON, ignore
}
}
return new FailedJobRecord
{
JobId = entity.JobId,
TenantId = entity.TenantId,
JobType = entity.JobType,
ImageDigest = imageDigest,
ArtifactDigest = artifactDigest,
Repository = repository,
Error = entity.Reason,
ErrorCode = errorCode,
ScannerVersion = scannerVersion,
RuntimeVersion = runtimeVersion,
FailedAt = entity.CompletedAt
};
}
}
/// <summary>

View File

@@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Queue;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
using StellaOps.Scheduler.Worker.Options;
using StellaOps.Scheduler.Worker.Observability;

View File

@@ -202,11 +202,11 @@ internal sealed class PlannerQueueDispatchService : IPlannerQueueDispatchService
return map;
}
private async Task<IReadOnlyDictionary<string, SurfaceManifestPointer>> PrefetchManifestsAsync(
private async Task<IReadOnlyDictionary<string, Queue.SurfaceManifestPointer>> PrefetchManifestsAsync(
IReadOnlyList<string> digests,
CancellationToken cancellationToken)
{
var results = new Dictionary<string, SurfaceManifestPointer>(StringComparer.Ordinal);
var results = new Dictionary<string, Queue.SurfaceManifestPointer>(StringComparer.Ordinal);
foreach (var digest in digests)
{
@@ -224,7 +224,7 @@ internal sealed class PlannerQueueDispatchService : IPlannerQueueDispatchService
continue;
}
var pointer = new SurfaceManifestPointer(digest, manifest.Tenant);
var pointer = new Queue.SurfaceManifestPointer(digest, manifest.Tenant);
results[digest] = pointer;
_metrics.RecordSurfaceManifestPrefetch(result: "hit");
}
@@ -239,9 +239,7 @@ internal sealed class PlannerQueueDispatchService : IPlannerQueueDispatchService
}
}
return results.Count == 0
? (IReadOnlyDictionary<string, SurfaceManifestPointer>)EmptyReadOnlyDictionary<string, SurfaceManifestPointer>.Instance
: results;
return results;
}
}

View File

@@ -6,6 +6,7 @@
// -----------------------------------------------------------------------------
using System.Diagnostics;
using System.Net.Http.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

View File

@@ -3,11 +3,13 @@ using Scheduler.Backfill;
using StellaOps.Scheduler.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Backfill.Tests;
public class BackfillMappingsTests
{
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(ScheduleMode.AnalysisOnly, "analysisonly")]
[InlineData(ScheduleMode.ContentRefresh, "contentrefresh")]
public void ScheduleMode_is_lower_snake(ScheduleMode mode, string expected)
@@ -15,7 +17,8 @@ public class BackfillMappingsTests
BackfillMappings.ToScheduleMode(mode).Should().Be(expected);
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(RunState.Planning, "planning")]
[InlineData(RunState.Completed, "completed")]
[InlineData(RunState.Cancelled, "cancelled")]
@@ -24,7 +27,8 @@ public class BackfillMappingsTests
BackfillMappings.ToRunState(state).Should().Be(expected);
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(RunTrigger.Cron, "cron")]
[InlineData(RunTrigger.Manual, "manual")]
public void RunTrigger_is_lower(RunTrigger trigger, string expected)

View File

@@ -12,7 +12,8 @@ namespace StellaOps.Scheduler.ImpactIndex.Tests;
public sealed class FixtureImpactIndexTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveByPurls_UsesEmbeddedFixtures()
{
var selector = new Selector(SelectorScope.AllImages);
@@ -38,7 +39,8 @@ public sealed class FixtureImpactIndexTests
result.SchemaVersion.Should().Be(SchedulerSchemaVersions.ImpactSet);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveByPurls_UsageOnlyFiltersInventoryOnlyComponents()
{
var selector = new Selector(SelectorScope.AllImages);
@@ -63,7 +65,8 @@ public sealed class FixtureImpactIndexTests
inventoryResult.Images.Single().UsedByEntrypoint.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAll_ReturnsDeterministicFixtureSet()
{
var selector = new Selector(SelectorScope.AllImages);
@@ -78,7 +81,8 @@ public sealed class FixtureImpactIndexTests
second.Images.Should().Equal(first.Images);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveByVulnerabilities_ReturnsEmptySet()
{
var selector = new Selector(SelectorScope.AllImages);
@@ -93,7 +97,8 @@ public sealed class FixtureImpactIndexTests
result.Images.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FixtureDirectoryOption_LoadsFromFileSystem()
{
var selector = new Selector(SelectorScope.AllImages);
@@ -104,6 +109,7 @@ public sealed class FixtureImpactIndexTests
});
using var _ = loggerFactory;
using StellaOps.TestKit;
var result = await impactIndex.ResolveAllAsync(selector, usageOnly: false);
result.Images.Should().HaveCount(6);

View File

@@ -9,11 +9,13 @@ using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Emit.Index;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.ImpactIndex.Tests;
public sealed class RoaringImpactIndexTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task IngestAsync_RegistersComponentsAndUsage()
{
var (stream, digest) = CreateBomIndex(
@@ -50,7 +52,8 @@ public sealed class RoaringImpactIndexTests
usageOnly.Images.Should().HaveCount(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task IngestAsync_ReplacesExistingImageData()
{
var component = ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0");
@@ -86,7 +89,8 @@ public sealed class RoaringImpactIndexTests
impactSet.Images[0].UsedByEntrypoint.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveByPurlsAsync_RespectsTenantNamespaceAndTagFilters()
{
var component = ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0");
@@ -129,7 +133,8 @@ public sealed class RoaringImpactIndexTests
result.Images[0].Tags.Should().Contain("prod-eu");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAllAsync_UsageOnlyFiltersEntrypointImages()
{
var component = ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0");
@@ -166,7 +171,8 @@ public sealed class RoaringImpactIndexTests
allImages.Images.Should().HaveCount(2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RemoveAsync_RemovesImageAndComponents()
{
var component = ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0");
@@ -201,7 +207,8 @@ public sealed class RoaringImpactIndexTests
impact.Images.Should().ContainSingle(img => img.ImageDigest == digest2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CreateSnapshotAsync_CompactsIdsAndRestores()
{
var component = ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0");

View File

@@ -2,7 +2,8 @@ namespace StellaOps.Scheduler.Models.Tests;
public sealed class AuditRecordTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AuditRecordNormalizesMetadataAndIdentifiers()
{
var actor = new AuditActor(actorId: "user_admin", displayName: "Cluster Admin", kind: "user");

View File

@@ -2,7 +2,8 @@ namespace StellaOps.Scheduler.Models.Tests;
public sealed class GraphJobStateMachineTests
{
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(GraphJobStatus.Pending, GraphJobStatus.Pending, true)]
[InlineData(GraphJobStatus.Pending, GraphJobStatus.Queued, true)]
[InlineData(GraphJobStatus.Pending, GraphJobStatus.Running, true)]
@@ -17,7 +18,8 @@ public sealed class GraphJobStateMachineTests
Assert.Equal(expected, GraphJobStateMachine.CanTransition(from, to));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void EnsureTransition_UpdatesBuildJobLifecycle()
{
var createdAt = new DateTimeOffset(2025, 10, 26, 12, 0, 0, TimeSpan.Zero);
@@ -51,7 +53,8 @@ public sealed class GraphJobStateMachineTests
Assert.Null(job.Error);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void EnsureTransition_ToFailedRequiresError()
{
var job = new GraphBuildJob(
@@ -71,7 +74,8 @@ public sealed class GraphJobStateMachineTests
DateTimeOffset.UtcNow));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void EnsureTransition_ToFailedSetsError()
{
var job = new GraphOverlayJob(
@@ -96,7 +100,8 @@ public sealed class GraphJobStateMachineTests
Assert.Equal("cartographer timeout", failed.Error);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_RequiresCompletedAtForTerminalState()
{
var job = new GraphOverlayJob(
@@ -112,7 +117,8 @@ public sealed class GraphJobStateMachineTests
Assert.Throws<InvalidOperationException>(() => GraphJobStateMachine.Validate(job));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphOverlayJob_NormalizesSubjectsAndMetadata()
{
var createdAt = DateTimeOffset.UtcNow;
@@ -147,7 +153,8 @@ public sealed class GraphJobStateMachineTests
Assert.Equal("run-123", job.Metadata["policyrunid"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphBuildJob_NormalizesDigestAndMetadata()
{
var job = new GraphBuildJob(

View File

@@ -1,10 +1,12 @@
using StellaOps.Scheduler.Models;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Models.Tests;
public sealed class ImpactSetTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ImpactSetSortsImagesByDigest()
{
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
@@ -47,7 +49,8 @@ public sealed class ImpactSetTests
Assert.Contains("\"snapshotId\":\"snap-001\"", json, StringComparison.Ordinal);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ImpactImageRejectsInvalidDigest()
{
Assert.Throws<ArgumentException>(() => new ImpactImage("sha1:not-supported", "registry", "repo"));

View File

@@ -2,11 +2,13 @@ using System.Collections.Immutable;
using System.Text.Json;
using StellaOps.Scheduler.Models;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Models.Tests;
public sealed class PolicyRunModelsTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PolicyRunInputs_NormalizesEnvironmentKeys()
{
var inputs = new PolicyRunInputs(
@@ -28,7 +30,8 @@ public sealed class PolicyRunModelsTests
Assert.Equal("global", inputs.Environment["region"].GetString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PolicySimulationWebhookPayloadFactory_ComputesSucceeded()
{
var now = DateTimeOffset.UtcNow;
@@ -43,7 +46,8 @@ public sealed class PolicyRunModelsTests
Assert.NotNull(payload.LatencySeconds);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PolicySimulationWebhookPayloadFactory_ComputesFailureReason()
{
var now = DateTimeOffset.UtcNow;
@@ -90,7 +94,8 @@ public sealed class PolicyRunModelsTests
CancelledAt: status == PolicyRunJobStatus.Cancelled ? timestamp : null);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PolicyRunStatus_ThrowsOnNegativeAttempts()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new PolicyRunStatus(
@@ -105,7 +110,8 @@ public sealed class PolicyRunModelsTests
attempts: -1));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PolicyDiffSummary_NormalizesSeverityKeys()
{
var summary = new PolicyDiffSummary(
@@ -122,7 +128,8 @@ public sealed class PolicyRunModelsTests
Assert.True(summary.BySeverity.ContainsKey("High"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PolicyExplainTrace_LowercasesMetadataKeys()
{
var trace = new PolicyExplainTrace(

View File

@@ -4,13 +4,15 @@ using System.Text.Json;
using System.Text.Json.Nodes;
using StellaOps.Notify.Models;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Models.Tests;
public sealed class RescanDeltaEventSampleTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RescanDeltaEventSampleAlignsWithContracts()
{
const string fileName = "scheduler.rescan.delta@1.sample.json";

View File

@@ -1,10 +1,12 @@
using StellaOps.Scheduler.Models;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Models.Tests;
public sealed class RunStateMachineTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void EnsureTransition_FromQueuedToRunningSetsStartedAt()
{
var run = new Run(
@@ -29,7 +31,8 @@ public sealed class RunStateMachineTests
Assert.Null(updated.Error);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void EnsureTransition_ToCompletedPopulatesFinishedAt()
{
var run = new Run(
@@ -58,7 +61,8 @@ public sealed class RunStateMachineTests
Assert.Equal(1, updated.Stats.Completed);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void EnsureTransition_ErrorRequiresMessage()
{
var run = new Run(
@@ -78,7 +82,8 @@ public sealed class RunStateMachineTests
Assert.Contains("requires a non-empty error message", ex.Message, StringComparison.Ordinal);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_ThrowsWhenTerminalWithoutFinishedAt()
{
var run = new Run(
@@ -93,7 +98,8 @@ public sealed class RunStateMachineTests
Assert.Throws<InvalidOperationException>(() => RunStateMachine.Validate(run));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RunReasonExtension_NormalizesImpactWindow()
{
var reason = new RunReason(manualReason: "delta");

View File

@@ -1,10 +1,12 @@
using StellaOps.Scheduler.Models;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Models.Tests;
public sealed class RunValidationTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RunStatsRejectsNegativeValues()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new RunStats(candidates: -1));
@@ -18,7 +20,8 @@ public sealed class RunValidationTests
Assert.Throws<ArgumentOutOfRangeException>(() => new RunStats(newLow: -1));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSummarySortsTopFindingsBySeverityThenId()
{
var summary = new DeltaSummary(
@@ -43,7 +46,8 @@ public sealed class RunValidationTests
Assert.Equal(new[] { "CVE-2025-0001", "CVE-2025-0002" }, summary.KevHits);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RunSerializationIncludesDeterministicOrdering()
{
var stats = new RunStats(candidates: 10, deduped: 8, queued: 8, completed: 5, deltas: 3, newCriticals: 2);

View File

@@ -8,7 +8,8 @@ public sealed class SamplePayloadTests
{
private static readonly string SamplesRoot = LocateSamplesRoot();
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ScheduleSample_RoundtripsThroughCanonicalSerializer()
{
var json = ReadSample("schedule.json");
@@ -21,7 +22,8 @@ public sealed class SamplePayloadTests
AssertJsonEquivalent(json, canonical);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RunSample_RoundtripsThroughCanonicalSerializer()
{
var json = ReadSample("run.json");
@@ -34,7 +36,8 @@ public sealed class SamplePayloadTests
AssertJsonEquivalent(json, canonical);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ImpactSetSample_RoundtripsThroughCanonicalSerializer()
{
var json = ReadSample("impact-set.json");
@@ -47,7 +50,8 @@ public sealed class SamplePayloadTests
AssertJsonEquivalent(json, canonical);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AuditSample_RoundtripsThroughCanonicalSerializer()
{
var json = ReadSample("audit.json");
@@ -60,7 +64,8 @@ public sealed class SamplePayloadTests
AssertJsonEquivalent(json, canonical);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphBuildJobSample_RoundtripsThroughCanonicalSerializer()
{
var json = ReadSample("graph-build-job.json");
@@ -73,7 +78,8 @@ public sealed class SamplePayloadTests
AssertJsonEquivalent(json, canonical);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GraphOverlayJobSample_RoundtripsThroughCanonicalSerializer()
{
var json = ReadSample("graph-overlay-job.json");
@@ -87,7 +93,8 @@ public sealed class SamplePayloadTests
AssertJsonEquivalent(json, canonical);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PolicyRunRequestSample_RoundtripsThroughCanonicalSerializer()
{
var json = ReadSample("policy-run-request.json");
@@ -103,7 +110,8 @@ public sealed class SamplePayloadTests
AssertJsonEquivalent(json, canonical);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PolicyRunStatusSample_RoundtripsThroughCanonicalSerializer()
{
var json = ReadSample("policy-run-status.json");
@@ -117,7 +125,8 @@ public sealed class SamplePayloadTests
AssertJsonEquivalent(json, canonical);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PolicyDiffSummarySample_RoundtripsThroughCanonicalSerializer()
{
var json = ReadSample("policy-diff-summary.json");
@@ -131,7 +140,8 @@ public sealed class SamplePayloadTests
AssertJsonEquivalent(json, canonical);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PolicyExplainTraceSample_RoundtripsThroughCanonicalSerializer()
{
var json = ReadSample("policy-explain-trace.json");
@@ -145,7 +155,8 @@ public sealed class SamplePayloadTests
var canonical = CanonicalJsonSerializer.Serialize(trace);
AssertJsonEquivalent(json, canonical);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PolicyRunJob_RoundtripsThroughCanonicalSerializer()
{
var metadata = ImmutableSortedDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
@@ -237,6 +248,7 @@ public sealed class SamplePayloadTests
private static string NormalizeJson(string json)
{
using var document = JsonDocument.Parse(json);
using StellaOps.TestKit;
return JsonSerializer.Serialize(document.RootElement, new JsonSerializerOptions
{
WriteIndented = false

View File

@@ -5,7 +5,8 @@ namespace StellaOps.Scheduler.Models.Tests;
public sealed class ScheduleSerializationTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ScheduleSerialization_IsDeterministicRegardlessOfInputOrdering()
{
var selectionA = new Selector(
@@ -77,6 +78,7 @@ public sealed class ScheduleSerializationTests
Assert.Equal(jsonA, jsonB);
using var doc = JsonDocument.Parse(jsonA);
using StellaOps.TestKit;
var root = doc.RootElement;
Assert.Equal(SchedulerSchemaVersions.Schedule, root.GetProperty("schemaVersion").GetString());
Assert.Equal("analysis-only", root.GetProperty("mode").GetString());
@@ -86,7 +88,8 @@ public sealed class ScheduleSerializationTests
Assert.Equal(new[] { "team-a", "team-b" }, namespaces);
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("")]
[InlineData("not-a-timezone")]
public void Schedule_ThrowsWhenTimezoneInvalid(string timezone)

View File

@@ -1,11 +1,13 @@
using System.Text.Json.Nodes;
using StellaOps.Scheduler.Models;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Models.Tests;
public sealed class SchedulerSchemaMigrationTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void UpgradeSchedule_DefaultsSchemaVersionWhenMissing()
{
var schedule = new Schedule(
@@ -35,7 +37,8 @@ public sealed class SchedulerSchemaMigrationTests
Assert.Empty(result.Warnings);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void UpgradeRun_StrictModeRemovesUnknownProperties()
{
var run = new Run(
@@ -54,7 +57,8 @@ public sealed class SchedulerSchemaMigrationTests
Assert.Contains(result.Warnings, warning => warning.Contains("extraField", StringComparison.Ordinal));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void UpgradeImpactSet_ThrowsForUnsupportedVersion()
{
var impactSet = new ImpactSet(
@@ -70,7 +74,8 @@ public sealed class SchedulerSchemaMigrationTests
Assert.Contains("Unsupported scheduler schema version", ex.Message, StringComparison.Ordinal);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void UpgradeSchedule_Legacy0_UpgradesToLatestVersion()
{
var legacy = new JsonObject
@@ -116,7 +121,8 @@ public sealed class SchedulerSchemaMigrationTests
Assert.Contains(result.Warnings, warning => warning.Contains("schedule.subscribers", StringComparison.Ordinal));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void UpgradeRun_Legacy0_BackfillsMissingStats()
{
var legacy = new JsonObject
@@ -146,7 +152,8 @@ public sealed class SchedulerSchemaMigrationTests
Assert.Contains(result.Warnings, warning => warning.Contains("run.stats.newMedium", StringComparison.Ordinal));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void UpgradeImpactSet_Legacy0_ComputesTotal()
{
var legacy = new JsonObject

View File

@@ -5,11 +5,13 @@ using FluentAssertions;
using StellaOps.Scheduler.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Queue.Tests;
public sealed class PlannerAndRunnerMessageTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PlannerMessage_CanonicalSerialization_RoundTrips()
{
var schedule = new Schedule(
@@ -68,7 +70,8 @@ public sealed class PlannerAndRunnerMessageTests
roundTrip.Should().BeEquivalentTo(message, options => options.WithStrictOrdering());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RunnerSegmentMessage_RequiresAtLeastOneDigest()
{
var act = () => new RunnerSegmentQueueMessage(
@@ -80,7 +83,8 @@ public sealed class PlannerAndRunnerMessageTests
act.Should().Throw<ArgumentException>();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RunnerSegmentMessage_CanonicalSerialization_RoundTrips()
{
var message = new RunnerSegmentQueueMessage(

View File

@@ -2,30 +2,24 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using DotNet.Testcontainers.Configurations;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StackExchange.Redis;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Queue.Redis;
using Testcontainers.Redis;
using Xunit;
namespace StellaOps.Scheduler.Queue.Tests;
public sealed class RedisSchedulerQueueTests : IAsyncLifetime
{
private readonly RedisTestcontainer _redis;
private readonly RedisContainer _redis;
private string? _skipReason;
public RedisSchedulerQueueTests()
{
var configuration = new RedisTestcontainerConfiguration();
_redis = new TestcontainersBuilder<RedisTestcontainer>()
.WithDatabase(configuration)
.Build();
_redis = new RedisBuilder().Build();
}
public async Task InitializeAsync()
@@ -50,7 +44,8 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
await _redis.DisposeAsync().AsTask();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PlannerQueue_EnqueueLeaseAck_RemovesMessage()
{
if (SkipIfUnavailable())
@@ -86,7 +81,8 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
afterAck.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RunnerQueue_Retry_IncrementsDeliveryAttempt()
{
if (SkipIfUnavailable())
@@ -122,7 +118,8 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
secondLease[0].Attempt.Should().Be(2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PlannerQueue_ClaimExpired_ReassignsLease()
{
if (SkipIfUnavailable())
@@ -155,7 +152,8 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
await reclaimed[0].AcknowledgeAsync();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PlannerQueue_RecordsDepthMetrics()
{
if (SkipIfUnavailable())
@@ -190,7 +188,8 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
plannerDepth.Should().Be(0);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RunnerQueue_DropWhenDeadLetterDisabled()
{
if (SkipIfUnavailable())
@@ -209,6 +208,7 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
TimeProvider.System,
async config => (IConnectionMultiplexer)await ConnectionMultiplexer.ConnectAsync(config).ConfigureAwait(false));
using StellaOps.TestKit;
var message = TestData.CreateRunnerMessage();
await queue.EnqueueAsync(message);
@@ -234,7 +234,7 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
RetryMaxBackoff = TimeSpan.FromMilliseconds(50),
Redis = new SchedulerRedisQueueOptions
{
ConnectionString = _redis.ConnectionString,
ConnectionString = _redis.GetConnectionString(),
Database = 0,
InitializationTimeout = TimeSpan.FromSeconds(10),
Planner = new RedisSchedulerStreamOptions

View File

@@ -18,7 +18,8 @@ namespace StellaOps.Scheduler.Queue.Tests;
public sealed class SchedulerQueueServiceCollectionExtensionsTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AddSchedulerQueues_RegistersNatsTransport()
{
var services = new ServiceCollection();
@@ -32,6 +33,7 @@ public sealed class SchedulerQueueServiceCollectionExtensionsTests
await using var provider = services.BuildServiceProvider();
using StellaOps.TestKit;
var plannerQueue = provider.GetRequiredService<ISchedulerPlannerQueue>();
var runnerQueue = provider.GetRequiredService<ISchedulerRunnerQueue>();
@@ -39,7 +41,8 @@ public sealed class SchedulerQueueServiceCollectionExtensionsTests
runnerQueue.Should().BeOfType<NatsSchedulerRunnerQueue>();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SchedulerQueueHealthCheck_ReturnsHealthy_WhenTransportsReachable()
{
var healthCheck = new SchedulerQueueHealthCheck(
@@ -57,7 +60,8 @@ public sealed class SchedulerQueueServiceCollectionExtensionsTests
result.Status.Should().Be(HealthStatus.Healthy);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SchedulerQueueHealthCheck_ReturnsUnhealthy_WhenRunnerPingFails()
{
var healthCheck = new SchedulerQueueHealthCheck(

View File

@@ -9,7 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="DotNet.Testcontainers" Version="1.7.0-beta.2269" />
<PackageReference Include="Testcontainers" Version="4.4.0" />
<PackageReference Include="Testcontainers.Redis" Version="4.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />

View File

@@ -4,6 +4,7 @@ using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Storage.Postgres.Tests;
[Collection(SchedulerPostgresCollection.Name)]
@@ -26,7 +27,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TryAcquire_SucceedsOnFirstAttempt()
{
// Arrange
@@ -39,7 +41,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
acquired.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TryAcquire_FailsWhenAlreadyHeld()
{
// Arrange
@@ -53,7 +56,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
secondAcquire.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Release_AllowsReacquisition()
{
// Arrange
@@ -68,7 +72,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
reacquired.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Extend_ExtendsLockDuration()
{
// Arrange
@@ -82,7 +87,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
extended.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Extend_FailsForDifferentHolder()
{
// Arrange
@@ -96,7 +102,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
extended.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Get_ReturnsLockInfo()
{
// Arrange
@@ -111,7 +118,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
lockInfo!.HolderId.Should().Be("worker-1");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ListByTenant_ReturnsTenantsLocks()
{
// Arrange
@@ -127,7 +135,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
locks.Should().HaveCount(2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TryAcquire_IsExclusiveAcrossConcurrentCallers()
{
// Arrange
@@ -156,7 +165,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
persisted!.HolderId.Should().Be(winningHolder);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TryAcquire_AllowsReacquireAfterExpiration()
{
// Arrange

View File

@@ -10,6 +10,7 @@ using StellaOps.Scheduler.Storage.Postgres;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Storage.Postgres.Tests;
[Collection(SchedulerPostgresCollection.Name)]
@@ -36,7 +37,8 @@ public sealed class GraphJobRepositoryTests : IAsyncLifetime
trigger: GraphBuildJobTrigger.SbomVersion,
createdAt: DateTimeOffset.UtcNow);
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task InsertAndGetBuildJob()
{
var dataSource = CreateDataSource();
@@ -52,7 +54,8 @@ public sealed class GraphJobRepositoryTests : IAsyncLifetime
fetched.Status.Should().Be(GraphJobStatus.Pending);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TryReplaceSucceedsWithExpectedStatus()
{
var dataSource = CreateDataSource();
@@ -70,7 +73,8 @@ public sealed class GraphJobRepositoryTests : IAsyncLifetime
fetched!.Status.Should().Be(GraphJobStatus.Running);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TryReplaceFailsOnUnexpectedStatus()
{
var dataSource = CreateDataSource();
@@ -85,7 +89,8 @@ public sealed class GraphJobRepositoryTests : IAsyncLifetime
updated.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ListBuildJobsHonorsStatusAndLimit()
{
var dataSource = CreateDataSource();

View File

@@ -5,6 +5,7 @@ using StellaOps.Scheduler.Storage.Postgres.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Storage.Postgres.Tests;
[Collection(SchedulerPostgresCollection.Name)]
@@ -27,7 +28,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CreateAndGet_RoundTripsTrigger()
{
// Arrange
@@ -57,7 +59,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
fetched.CronExpression.Should().Be("0 0 * * *");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetByName_ReturnsCorrectTrigger()
{
// Arrange
@@ -72,7 +75,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
fetched!.Id.Should().Be(trigger.Id);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_ReturnsAllTriggersForTenant()
{
// Arrange
@@ -89,7 +93,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
triggers.Select(t => t.Name).Should().Contain(["trigger1", "trigger2"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetDueTriggers_ReturnsTriggersReadyToFire()
{
// Arrange - One due trigger, one future trigger
@@ -128,7 +133,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
dueTriggers[0].Name.Should().Be("due");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RecordFire_UpdatesTriggerState()
{
// Arrange
@@ -148,7 +154,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
fetched.FireCount.Should().Be(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SetEnabled_TogglesEnableState()
{
// Arrange
@@ -170,7 +177,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
enabled!.Enabled.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Delete_RemovesTrigger()
{
// Arrange
@@ -185,7 +193,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
fetched.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetDueTriggers_IsDeterministicForEqualNextFire()
{
// Arrange

View File

@@ -5,6 +5,7 @@ using StellaOps.Scheduler.Storage.Postgres.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Storage.Postgres.Tests;
[Collection(SchedulerPostgresCollection.Name)]
@@ -26,7 +27,8 @@ public sealed class WorkerRepositoryTests : IAsyncLifetime
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UpsertAndGet_RoundTripsWorker()
{
// Arrange
@@ -50,7 +52,8 @@ public sealed class WorkerRepositoryTests : IAsyncLifetime
fetched.JobTypes.Should().BeEquivalentTo(["scan", "sbom"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Heartbeat_UpdatesLastHeartbeat()
{
// Arrange
@@ -67,7 +70,8 @@ public sealed class WorkerRepositoryTests : IAsyncLifetime
fetched.CurrentJobs.Should().Be(2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ListByStatus_ReturnsWorkersWithStatus()
{
// Arrange
@@ -91,7 +95,8 @@ public sealed class WorkerRepositoryTests : IAsyncLifetime
activeWorkers[0].Id.Should().Be(activeWorker.Id);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SetStatus_ChangesWorkerStatus()
{
// Arrange
@@ -106,7 +111,8 @@ public sealed class WorkerRepositoryTests : IAsyncLifetime
fetched!.Status.Should().Be(WorkerStatus.Draining);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Delete_RemovesWorker()
{
// Arrange
@@ -121,7 +127,8 @@ public sealed class WorkerRepositoryTests : IAsyncLifetime
fetched.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_ReturnsAllWorkers()
{
// Arrange

View File

@@ -12,7 +12,8 @@ namespace StellaOps.Scheduler.WebService.Tests;
public sealed class CartographerWebhookClientTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task NotifyAsync_PostsPayload_WhenEnabled()
{
var handler = new RecordingHandler();
@@ -68,13 +69,15 @@ public sealed class CartographerWebhookClientTests
Assert.Equal("tenant-alpha", json.GetProperty("tenantId").GetString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task NotifyAsync_Skips_WhenDisabled()
{
var handler = new RecordingHandler();
var httpClient = new HttpClient(handler);
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerCartographerOptions());
using var loggerFactory = LoggerFactory.Create(builder => builder.AddDebug());
using StellaOps.TestKit;
var client = new CartographerWebhookClient(httpClient, new OptionsMonitorStub<SchedulerCartographerOptions>(options), loggerFactory.CreateLogger<CartographerWebhookClient>());
var job = new GraphOverlayJob(

View File

@@ -30,7 +30,8 @@ public sealed class EventWebhookEndpointTests : IClassFixture<WebApplicationFact
_factory = factory;
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ConselierWebhook_AcceptsValidSignature()
{
using var client = _factory.CreateClient();
@@ -53,7 +54,8 @@ public sealed class EventWebhookEndpointTests : IClassFixture<WebApplicationFact
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ConselierWebhook_RejectsInvalidSignature()
{
using var client = _factory.CreateClient();
@@ -74,7 +76,8 @@ public sealed class EventWebhookEndpointTests : IClassFixture<WebApplicationFact
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExcitorWebhook_HonoursRateLimit()
{
using var restrictedFactory = _factory.WithWebHostBuilder(builder =>
@@ -122,6 +125,7 @@ public sealed class EventWebhookEndpointTests : IClassFixture<WebApplicationFact
private static string ComputeSignature(string secret, string payload)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
using StellaOps.TestKit;
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
return "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();
}

View File

@@ -16,7 +16,8 @@ public sealed class FailureSignatureEndpointTests : IClassFixture<SchedulerWebAp
_factory = factory;
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task BestMatch_WhenMissing_ReturnsNoContent()
{
var repository = new StubFailureSignatureRepository(match: null);
@@ -41,7 +42,8 @@ public sealed class FailureSignatureEndpointTests : IClassFixture<SchedulerWebAp
Assert.Equal("tch_123", repository.LastCall!.Value.ToolchainHash);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task BestMatch_WhenPresent_ReturnsPayload()
{
var signatureId = Guid.Parse("e22132b0-2aa7-4cde-94a9-0b335d321c61");
@@ -74,6 +76,7 @@ public sealed class FailureSignatureEndpointTests : IClassFixture<SchedulerWebAp
}));
using var client = factory.CreateClient();
using StellaOps.TestKit;
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-failure-signatures");
client.DefaultRequestHeaders.Add("X-Scopes", "scheduler.runs.read");

View File

@@ -11,7 +11,8 @@ public sealed class GraphJobEndpointTests : IClassFixture<SchedulerWebApplicatio
_factory = factory;
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CreateGraphBuildJob_RequiresGraphWriteScope()
{
using var client = _factory.CreateClient();
@@ -27,7 +28,8 @@ public sealed class GraphJobEndpointTests : IClassFixture<SchedulerWebApplicatio
Assert.Equal(System.Net.HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CreateGraphBuildJob_AndList()
{
using var client = _factory.CreateClient();
@@ -58,7 +60,8 @@ public sealed class GraphJobEndpointTests : IClassFixture<SchedulerWebApplicatio
Assert.Equal("pending", first.GetProperty("status").GetString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CompleteOverlayJob_UpdatesStatusAndMetrics()
{
using var client = _factory.CreateClient();
@@ -98,6 +101,7 @@ public sealed class GraphJobEndpointTests : IClassFixture<SchedulerWebApplicatio
metricsResponse.EnsureSuccessStatusCode();
var metricsJson = await metricsResponse.Content.ReadAsStringAsync();
using var metricsDoc = JsonDocument.Parse(metricsJson);
using StellaOps.TestKit;
var metricsRoot = metricsDoc.RootElement;
Assert.Equal("tenant-bravo", metricsRoot.GetProperty("tenantId").GetString());
Assert.True(metricsRoot.GetProperty("completed").GetInt32() >= 1);

View File

@@ -11,7 +11,8 @@ namespace StellaOps.Scheduler.WebService.Tests;
public sealed class GraphJobEventPublisherTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PublishAsync_LogsEvent_WhenDriverUnsupported()
{
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerEventsOptions
@@ -63,12 +64,14 @@ public sealed class GraphJobEventPublisherTests
Assert.Contains("\"resultUri\":\"oras://result\"", eventPayload);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PublishAsync_Suppressed_WhenDisabled()
{
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerEventsOptions());
var loggerProvider = new ListLoggerProvider();
using var loggerFactory = LoggerFactory.Create(builder => builder.AddProvider(loggerProvider));
using StellaOps.TestKit;
var publisher = new GraphJobEventPublisher(new OptionsMonitorStub<SchedulerEventsOptions>(options), new ThrowingRedisConnectionFactory(), loggerFactory.CreateLogger<GraphJobEventPublisher>());
var overlayJob = new GraphOverlayJob(

View File

@@ -6,13 +6,15 @@ using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.WebService.GraphJobs;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.WebService.Tests;
public sealed class GraphJobServiceTests
{
private static readonly DateTimeOffset FixedTime = new(2025, 11, 4, 12, 0, 0, TimeSpan.Zero);
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CompleteBuildJob_PersistsMetadataAndPublishesOnce()
{
var store = new TrackingGraphJobStore();
@@ -50,7 +52,8 @@ public sealed class GraphJobServiceTests
Assert.Equal("oras://cartographer/bundle", resultUri);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CompleteBuildJob_IsIdempotentWhenAlreadyCompleted()
{
var store = new TrackingGraphJobStore();
@@ -84,7 +87,8 @@ public sealed class GraphJobServiceTests
Assert.Single(webhook.Notifications);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CompleteBuildJob_UpdatesResultUriWithoutReemittingEvent()
{
var store = new TrackingGraphJobStore();
@@ -131,7 +135,8 @@ public sealed class GraphJobServiceTests
Assert.Equal("oras://cartographer/bundle-v2", resultUri);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CreateBuildJob_NormalizesSbomDigest()
{
var store = new TrackingGraphJobStore();
@@ -151,7 +156,8 @@ public sealed class GraphJobServiceTests
Assert.Equal("sha256:" + new string('a', 64), created.SbomDigest);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CreateBuildJob_RejectsDigestWithoutPrefix()
{
var store = new TrackingGraphJobStore();

View File

@@ -6,11 +6,13 @@ using StellaOps.Scheduler.ImpactIndex;
using StellaOps.Scheduler.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.WebService.Tests;
public sealed class ImpactIndexFixtureTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FixtureDirectoryExists()
{
var fixtureDirectory = GetFixtureDirectory();
@@ -23,7 +25,8 @@ public sealed class ImpactIndexFixtureTests
Assert.Contains(sampleFile, files);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FixtureImpactIndexLoadsSampleImage()
{
var fixtureDirectory = GetFixtureDirectory();

View File

@@ -11,7 +11,8 @@ public sealed class PolicyRunEndpointTests : IClassFixture<WebApplicationFactory
_factory = factory;
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CreateListGetPolicyRun()
{
using var client = _factory.CreateClient();
@@ -58,10 +59,12 @@ public sealed class PolicyRunEndpointTests : IClassFixture<WebApplicationFactory
Assert.Equal(runId, retrieved.GetProperty("run").GetProperty("runId").GetString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task MissingScopeReturnsForbidden()
{
using var client = _factory.CreateClient();
using StellaOps.TestKit;
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-policy");
var response = await client.GetAsync("/api/v1/scheduler/policy/runs");

View File

@@ -19,7 +19,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
_factory = factory;
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CreateListGetSimulation()
{
using var client = _factory.CreateClient();
@@ -56,7 +57,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
Assert.Equal(runId, simulation.GetProperty("simulation").GetProperty("runId").GetString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task MetricsEndpointWithoutProviderReturns501()
{
using var client = _factory.CreateClient();
@@ -67,7 +69,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task MetricsEndpointReturnsSummary()
{
var stub = new StubPolicySimulationMetricsProvider
@@ -111,7 +114,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
Assert.Equal(2.0, payload.GetProperty("policy_simulation_latency").GetProperty("mean_seconds").GetDouble());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CreateSimulationRequiresScopeHeader()
{
using var client = _factory.CreateClient();
@@ -126,7 +130,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CreateSimulationRequiresPolicySimulateScope()
{
using var client = _factory.CreateClient();
@@ -142,7 +147,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CancelSimulationMarksStatus()
{
using var client = _factory.CreateClient();
@@ -167,7 +173,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
Assert.True(cancelled.GetProperty("simulation").GetProperty("cancellationRequested").GetBoolean());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RetrySimulationCreatesNewRun()
{
using var client = _factory.CreateClient();
@@ -197,7 +204,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
Assert.Equal(runId, retryOf.GetString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task StreamSimulationEmitsCoreEvents()
{
using var client = _factory.CreateClient();
@@ -227,6 +235,7 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
var seenHeartbeat = false;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
using StellaOps.TestKit;
while (!cts.Token.IsCancellationRequested && !(seenRetry && seenInitial && seenQueueLag && seenHeartbeat))
{
var readTask = reader.ReadLineAsync();

View File

@@ -15,7 +15,8 @@ namespace StellaOps.Scheduler.WebService.Tests;
public sealed class PolicySimulationMetricsProviderTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CaptureAsync_ComputesQueueDepthAndLatency()
{
var now = DateTimeOffset.UtcNow;
@@ -61,7 +62,8 @@ public sealed class PolicySimulationMetricsProviderTests
Assert.Equal(29.8, response.Latency.P99.Value, 1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CaptureAsync_UpdatesSnapshotAndEmitsTenantTaggedGauge()
{
var repository = new StubPolicyRunJobRepository();
@@ -133,7 +135,8 @@ public sealed class PolicySimulationMetricsProviderTests
listener.Dispose();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RecordLatency_EmitsHistogramMeasurement()
{
var repository = new StubPolicyRunJobRepository();
@@ -149,6 +152,7 @@ public sealed class PolicySimulationMetricsProviderTests
instrument.Name == "policy_simulation_latency_seconds")
{
meterListener.EnableMeasurementEvents(instrument);
using StellaOps.TestKit;
}
}
};

View File

@@ -23,7 +23,8 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
_factory = factory;
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CreateListCancelRun()
{
using var client = _factory.CreateClient();
@@ -76,7 +77,8 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
Assert.Equal("cancelled", runDetail.GetProperty("run").GetProperty("state").GetString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PreviewImpactForSchedule()
{
using var client = _factory.CreateClient();
@@ -113,7 +115,8 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
Assert.True(preview.GetProperty("sample").GetArrayLength() <= 3);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RetryRunCreatesNewRun()
{
using var client = _factory.CreateClient();
@@ -148,7 +151,8 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
Assert.Contains("retry-of:", retryRun.GetProperty("reason").GetProperty("manualReason").GetString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetRunDeltasReturnsMetadata()
{
using var client = _factory.CreateClient();
@@ -207,7 +211,8 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
Assert.Equal(1, deltasJson.GetProperty("deltas").GetArrayLength());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task QueueLagSummaryReturnsDepth()
{
SchedulerQueueMetrics.RecordDepth("redis", "scheduler-runner", 7);
@@ -231,7 +236,8 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task StreamRunEmitsInitialEvent()
{
using var client = _factory.CreateClient();
@@ -261,6 +267,7 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
using var reader = new StreamReader(stream);
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
using StellaOps.TestKit;
var seenRetry = false;
var seenInitial = false;
var seenQueueLag = false;

View File

@@ -9,7 +9,8 @@ public sealed class ScheduleEndpointTests : IClassFixture<WebApplicationFactory<
_factory = factory;
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CreateListAndRetrieveSchedule()
{
using var client = _factory.CreateClient();
@@ -51,10 +52,12 @@ public sealed class ScheduleEndpointTests : IClassFixture<WebApplicationFactory<
Assert.Equal("Nightly", scheduleJson.GetProperty("schedule").GetProperty("name").GetString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PauseAndResumeSchedule()
{
using var client = _factory.CreateClient();
using StellaOps.TestKit;
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-controls");
client.DefaultRequestHeaders.Add("X-Scopes", "scheduler.schedules.write scheduler.schedules.read");

View File

@@ -5,11 +5,13 @@ using StellaOps.Scheduler.WebService.Hosting;
using StellaOps.Scheduler.WebService.Options;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.WebService.Tests;
public class SchedulerPluginHostFactoryTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_usesDefaults_whenOptionsEmpty()
{
var options = new SchedulerOptions.PluginOptions();
@@ -36,7 +38,8 @@ public class SchedulerPluginHostFactoryTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_respectsConfiguredValues()
{
var options = new SchedulerOptions.PluginOptions

View File

@@ -17,7 +17,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
public sealed class GraphBuildExecutionServiceTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_Skips_WhenGraphDisabled()
{
var repository = new RecordingGraphJobRepository();
@@ -43,7 +44,8 @@ public sealed class GraphBuildExecutionServiceTests
Assert.Empty(completion.Notifications);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_CompletesJob_OnSuccess()
{
var repository = new RecordingGraphJobRepository();
@@ -85,7 +87,8 @@ public sealed class GraphBuildExecutionServiceTests
Assert.True(repository.ReplaceCalls >= 1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_Fails_AfterMaxAttempts()
{
var repository = new RecordingGraphJobRepository();
@@ -116,7 +119,8 @@ public sealed class GraphBuildExecutionServiceTests
Assert.Equal("network", completion.Notifications[0].Error);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_Skips_WhenConcurrencyConflict()
{
var repository = new RecordingGraphJobRepository
@@ -126,6 +130,7 @@ public sealed class GraphBuildExecutionServiceTests
var cartographer = new StubCartographerBuildClient();
var completion = new RecordingCompletionClient();
using var metrics = new SchedulerWorkerMetrics();
using StellaOps.TestKit;
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
{
Graph = new SchedulerWorkerOptions.GraphOptions

View File

@@ -17,7 +17,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
public sealed class GraphOverlayExecutionServiceTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_Skips_WhenGraphDisabled()
{
var repository = new RecordingGraphJobRepository();
@@ -42,7 +43,8 @@ public sealed class GraphOverlayExecutionServiceTests
Assert.Equal(0, cartographer.CallCount);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_CompletesJob_OnSuccess()
{
var repository = new RecordingGraphJobRepository();
@@ -79,7 +81,8 @@ public sealed class GraphOverlayExecutionServiceTests
Assert.Equal("graph_snap_2", notification.GraphSnapshotId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_Fails_AfterRetries()
{
var repository = new RecordingGraphJobRepository();
@@ -109,7 +112,8 @@ public sealed class GraphOverlayExecutionServiceTests
Assert.Equal("overlay failed", completion.Notifications[0].Error);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_Skips_WhenConcurrencyConflict()
{
var repository = new RecordingGraphJobRepository
@@ -119,6 +123,7 @@ public sealed class GraphOverlayExecutionServiceTests
var cartographer = new StubCartographerOverlayClient();
var completion = new RecordingCompletionClient();
using var metrics = new SchedulerWorkerMetrics();
using StellaOps.TestKit;
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
{
Graph = new SchedulerWorkerOptions.GraphOptions

View File

@@ -9,11 +9,13 @@ using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Worker.Execution;
using StellaOps.Scheduler.Worker.Options;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Worker.Tests;
public sealed class HttpScannerReportClientTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_WhenReportReturnsFindings_ProducesDeltaSummary()
{
var handler = new StubHttpMessageHandler(request =>
@@ -75,7 +77,8 @@ public sealed class HttpScannerReportClientTests
Assert.NotNull(result.Dsse);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_WhenReportFails_RetriesAndThrows()
{
var callCount = 0;

View File

@@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Worker.Tests;
public sealed class ImpactShardPlannerTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PlanShards_ReturnsSingleShardWhenParallelismNotSpecified()
{
var impactSet = CreateImpactSet(count: 3);
@@ -19,7 +21,8 @@ public sealed class ImpactShardPlannerTests
Assert.Equal(3, shards[0].Count);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PlanShards_RespectsMaxJobsLimit()
{
var impactSet = CreateImpactSet(count: 5);
@@ -31,7 +34,8 @@ public sealed class ImpactShardPlannerTests
Assert.True(shards.All(shard => shard.Count <= 1));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PlanShards_DistributesImagesEvenly()
{
var impactSet = CreateImpactSet(count: 10);

View File

@@ -5,11 +5,13 @@ using System.Threading;
using System.Threading.Tasks;
using StellaOps.Scheduler.ImpactIndex;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Worker.Tests;
public sealed class ImpactTargetingServiceTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveByPurlsAsync_DeduplicatesKeysAndInvokesIndex()
{
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
@@ -38,7 +40,8 @@ public sealed class ImpactTargetingServiceTests
Assert.Equal(new[] { "pkg:npm/a", "pkg:npm/b" }, capturedKeys);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveByVulnerabilitiesAsync_ReturnsEmptyWhenNoIds()
{
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
@@ -52,7 +55,8 @@ public sealed class ImpactTargetingServiceTests
Assert.Null(index.LastVulnerabilityIds);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveAllAsync_DelegatesToIndex()
{
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
@@ -74,7 +78,8 @@ public sealed class ImpactTargetingServiceTests
Assert.Equal(expected, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveByPurlsAsync_DeduplicatesImpactImagesByDigest()
{
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
@@ -130,7 +135,8 @@ public sealed class ImpactTargetingServiceTests
Assert.Equal("api", image.Labels["component"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ResolveByPurlsAsync_FiltersImagesBySelectorConstraints()
{
var selector = new Selector(

View File

@@ -16,7 +16,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
public sealed class PlannerBackgroundServiceTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_RespectsTenantFairnessCap()
{
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-27T12:00:00Z"));
@@ -71,7 +72,8 @@ public sealed class PlannerBackgroundServiceTests
Assert.Equal(new[] { "run-a1", "run-b1" }, processedIds);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_PrioritizesManualAndEventTriggers()
{
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-27T18:00:00Z"));
@@ -93,6 +95,7 @@ public sealed class PlannerBackgroundServiceTests
var targetingService = new StubImpactTargetingService(timeProvider);
using var metrics = new SchedulerWorkerMetrics();
using StellaOps.TestKit;
var executionService = new PlannerExecutionService(
scheduleRepository,
repository,

View File

@@ -15,7 +15,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
public sealed class PlannerExecutionServiceTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ProcessAsync_WithImpactedImages_QueuesPlannerMessage()
{
var schedule = CreateSchedule();
@@ -52,7 +53,8 @@ public sealed class PlannerExecutionServiceTests
Assert.Equal(impactSet.Images.Length, result.UpdatedRun.Stats.Queued);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ProcessAsync_WithNoImpactedImages_CompletesWithoutWork()
{
var schedule = CreateSchedule();
@@ -68,7 +70,8 @@ public sealed class PlannerExecutionServiceTests
Assert.Empty(plannerQueue.Messages);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ProcessAsync_WhenScheduleMissing_MarksRunAsFailed()
{
var run = CreateRun(scheduleId: "missing");
@@ -81,6 +84,7 @@ public sealed class PlannerExecutionServiceTests
using var metrics = new SchedulerWorkerMetrics();
using StellaOps.TestKit;
var service = new PlannerExecutionService(
scheduleRepository,
runRepository,

View File

@@ -9,11 +9,13 @@ using StellaOps.Scheduler.Queue;
using StellaOps.Scheduler.Worker.Planning;
using StellaOps.Scheduler.Worker.Options;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Worker.Tests;
public sealed class PlannerQueueDispatchServiceTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DispatchAsync_EnqueuesRunnerSegmentsDeterministically()
{
var run = CreateRun();
@@ -54,7 +56,8 @@ public sealed class PlannerQueueDispatchServiceTests
});
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DispatchAsync_NoImages_ReturnsNoWork()
{
var run = CreateRun();

View File

@@ -14,7 +14,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
public sealed class PolicyRunDispatchBackgroundServiceTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_DoesNotLease_WhenPolicyDispatchDisabled()
{
var repository = new RecordingPolicyRunJobRepository();
@@ -23,6 +24,7 @@ public sealed class PolicyRunDispatchBackgroundServiceTests
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
using StellaOps.TestKit;
await service.StartAsync(cts.Token);
await service.StopAsync(CancellationToken.None);

View File

@@ -38,7 +38,8 @@ public sealed class PolicyRunExecutionServiceTests
}
};
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_CancelsJob_WhenCancellationRequested()
{
var repository = new RecordingPolicyRunJobRepository();
@@ -70,7 +71,8 @@ public sealed class PolicyRunExecutionServiceTests
Assert.Equal("cancelled", webhook.Payloads[0].Result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_SubmitsJob_OnSuccess()
{
var repository = new RecordingPolicyRunJobRepository();
@@ -105,7 +107,8 @@ public sealed class PolicyRunExecutionServiceTests
Assert.Empty(webhook.Payloads);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_RetriesJob_OnFailure()
{
var repository = new RecordingPolicyRunJobRepository();
@@ -139,7 +142,8 @@ public sealed class PolicyRunExecutionServiceTests
Assert.Empty(webhook.Payloads);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_MarksJobFailed_WhenAttemptsExceeded()
{
var repository = new RecordingPolicyRunJobRepository();
@@ -174,7 +178,8 @@ public sealed class PolicyRunExecutionServiceTests
Assert.Equal("failed", webhook.Payloads[0].Result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_NoWork_CompletesJob()
{
var repository = new RecordingPolicyRunJobRepository();
@@ -182,6 +187,7 @@ public sealed class PolicyRunExecutionServiceTests
var options = Microsoft.Extensions.Options.Options.Create(CloneOptions());
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
using var metrics = new SchedulerWorkerMetrics();
using StellaOps.TestKit;
var targeting = new StubPolicyRunTargetingService
{
OnEnsureTargets = job => PolicyRunTargetingResult.NoWork(job, "empty")

View File

@@ -11,11 +11,13 @@ using StellaOps.Scheduler.Worker.Options;
using StellaOps.Scheduler.Worker.Policy;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Worker.Tests;
public sealed class PolicyRunTargetingServiceTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EnsureTargetsAsync_ReturnsUnchanged_ForNonIncrementalJob()
{
var service = CreateService();
@@ -27,7 +29,8 @@ public sealed class PolicyRunTargetingServiceTests
Assert.Equal(job, result.Job);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EnsureTargetsAsync_ReturnsUnchanged_WhenSbomSetAlreadyPresent()
{
var service = CreateService();
@@ -39,7 +42,8 @@ public sealed class PolicyRunTargetingServiceTests
Assert.Equal(PolicyRunTargetingStatus.Unchanged, result.Status);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EnsureTargetsAsync_ReturnsNoWork_WhenNoCandidatesResolved()
{
var impact = new StubImpactTargetingService();
@@ -53,7 +57,8 @@ public sealed class PolicyRunTargetingServiceTests
Assert.Equal("no_matches", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EnsureTargetsAsync_TargetsDirectSboms()
{
var service = CreateService();
@@ -66,7 +71,8 @@ public sealed class PolicyRunTargetingServiceTests
Assert.Equal(new[] { "sbom:S-1", "sbom:S-2" }, result.Job.Inputs.SbomSet);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EnsureTargetsAsync_TargetsUsingImpactIndex()
{
var impact = new StubImpactTargetingService
@@ -100,7 +106,8 @@ public sealed class PolicyRunTargetingServiceTests
Assert.Equal(new[] { "sbom:S-42" }, result.Job.Inputs.SbomSet);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EnsureTargetsAsync_FallsBack_WhenLimitExceeded()
{
var service = CreateService(configure: options => options.MaxSboms = 1);
@@ -112,7 +119,8 @@ public sealed class PolicyRunTargetingServiceTests
Assert.Equal(PolicyRunTargetingStatus.Unchanged, result.Status);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EnsureTargetsAsync_FallbacksToDigest_WhenLabelMissing()
{
var impact = new StubImpactTargetingService

View File

@@ -15,7 +15,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
public sealed class PolicySimulationWebhookClientTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task NotifyAsync_Disabled_DoesNotInvokeEndpoint()
{
var handler = new RecordingHandler();
@@ -29,11 +30,13 @@ public sealed class PolicySimulationWebhookClientTests
Assert.False(handler.WasInvoked);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task NotifyAsync_SendsPayload_WhenEnabled()
{
var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.Accepted));
using var httpClient = new HttpClient(handler);
using StellaOps.TestKit;
var options = CreateOptions(o =>
{
o.Policy.Webhook.Enabled = true;

View File

@@ -19,7 +19,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
public sealed class RunnerExecutionServiceTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_UpdatesRunStatsAndDeltas()
{
var run = CreateRun();
@@ -102,7 +103,8 @@ public sealed class RunnerExecutionServiceTests
Assert.Single(eventPublisher.RescanDeltaPayloads);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_WhenRunMissing_ReturnsRunMissing()
{
var repository = new InMemoryRunRepository();
@@ -110,6 +112,7 @@ public sealed class RunnerExecutionServiceTests
var eventPublisher = new RecordingSchedulerEventPublisher();
using var metrics = new SchedulerWorkerMetrics();
using StellaOps.TestKit;
var service = new RunnerExecutionService(
repository,
new RecordingRunSummaryService(),

View File

@@ -12,11 +12,13 @@ using StellaOps.Scheduler.Worker.Events;
using StellaOps.Scheduler.Worker.Execution;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scheduler.Worker.Tests;
public sealed class SchedulerEventPublisherTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PublishReportReadyAsync_EnqueuesNotifyEvent()
{
var queue = new RecordingNotifyEventQueue();
@@ -50,7 +52,8 @@ public sealed class SchedulerEventPublisherTests
Assert.Equal(1, deltaNode["newCritical"]!.GetValue<int>());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PublishRescanDeltaAsync_EnqueuesDeltaEvent()
{
var queue = new RecordingNotifyEventQueue();