Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Mongo.Repositories;
using StellaOps.Scheduler.Worker.Graph;
using StellaOps.Scheduler.Worker.Graph.Cartographer;
using StellaOps.Scheduler.Worker.Graph.Scheduler;
using StellaOps.Scheduler.Worker.Options;
using Xunit;
namespace StellaOps.Scheduler.Worker.Tests;
public sealed class GraphOverlayExecutionServiceTests
{
[Fact]
public async Task ExecuteAsync_Skips_WhenGraphDisabled()
{
var repository = new RecordingGraphJobRepository();
var cartographer = new StubCartographerOverlayClient();
var completion = new RecordingCompletionClient();
using var metrics = new SchedulerWorkerMetrics();
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
{
Graph = new SchedulerWorkerOptions.GraphOptions
{
Enabled = false
}
});
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
var job = CreateOverlayJob();
var result = await service.ExecuteAsync(job, CancellationToken.None);
Assert.Equal(GraphOverlayExecutionResultType.Skipped, result.Type);
Assert.Equal("graph_processing_disabled", result.Reason);
Assert.Empty(completion.Notifications);
Assert.Equal(0, cartographer.CallCount);
}
[Fact]
public async Task ExecuteAsync_CompletesJob_OnSuccess()
{
var repository = new RecordingGraphJobRepository();
var cartographer = new StubCartographerOverlayClient
{
Result = new CartographerOverlayResult(
GraphJobStatus.Completed,
GraphSnapshotId: "graph_snap_2",
ResultUri: "oras://graph/overlay",
Error: null)
};
var completion = new RecordingCompletionClient();
using var metrics = new SchedulerWorkerMetrics();
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
{
Graph = new SchedulerWorkerOptions.GraphOptions
{
Enabled = true,
MaxAttempts = 2,
RetryBackoff = TimeSpan.FromMilliseconds(5)
}
});
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
var job = CreateOverlayJob();
var result = await service.ExecuteAsync(job, CancellationToken.None);
Assert.Equal(GraphOverlayExecutionResultType.Completed, result.Type);
Assert.Single(completion.Notifications);
var notification = completion.Notifications[0];
Assert.Equal("Overlay", notification.JobType);
Assert.Equal(GraphJobStatus.Completed, notification.Status);
Assert.Equal("oras://graph/overlay", notification.ResultUri);
Assert.Equal("graph_snap_2", notification.GraphSnapshotId);
}
[Fact]
public async Task ExecuteAsync_Fails_AfterRetries()
{
var repository = new RecordingGraphJobRepository();
var cartographer = new StubCartographerOverlayClient
{
ExceptionToThrow = new InvalidOperationException("overlay failed")
};
var completion = new RecordingCompletionClient();
using var metrics = new SchedulerWorkerMetrics();
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
{
Graph = new SchedulerWorkerOptions.GraphOptions
{
Enabled = true,
MaxAttempts = 2,
RetryBackoff = TimeSpan.FromMilliseconds(1)
}
});
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
var job = CreateOverlayJob();
var result = await service.ExecuteAsync(job, CancellationToken.None);
Assert.Equal(GraphOverlayExecutionResultType.Failed, result.Type);
Assert.Single(completion.Notifications);
Assert.Equal(GraphJobStatus.Failed, completion.Notifications[0].Status);
Assert.Equal("overlay failed", completion.Notifications[0].Error);
}
[Fact]
public async Task ExecuteAsync_Skips_WhenConcurrencyConflict()
{
var repository = new RecordingGraphJobRepository
{
ShouldReplaceSucceed = false
};
var cartographer = new StubCartographerOverlayClient();
var completion = new RecordingCompletionClient();
using var metrics = new SchedulerWorkerMetrics();
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
{
Graph = new SchedulerWorkerOptions.GraphOptions
{
Enabled = true
}
});
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
var job = CreateOverlayJob();
var result = await service.ExecuteAsync(job, CancellationToken.None);
Assert.Equal(GraphOverlayExecutionResultType.Skipped, result.Type);
Assert.Equal("concurrency_conflict", result.Reason);
Assert.Empty(completion.Notifications);
Assert.Equal(0, cartographer.CallCount);
}
private static GraphOverlayJob CreateOverlayJob() => new(
id: "goj_1",
tenantId: "tenant-alpha",
graphSnapshotId: "snap-1",
overlayKind: GraphOverlayKind.Policy,
overlayKey: "policy@1",
status: GraphJobStatus.Pending,
trigger: GraphOverlayJobTrigger.Policy,
createdAt: DateTimeOffset.UtcNow,
subjects: Array.Empty<string>(),
attempts: 0,
metadata: Array.Empty<KeyValuePair<string, string>>());
private sealed class RecordingGraphJobRepository : IGraphJobRepository
{
public bool ShouldReplaceSucceed { get; set; } = true;
public int RunningReplacements { get; private set; }
public Task<bool> TryReplaceOverlayAsync(GraphOverlayJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken = default)
{
if (!ShouldReplaceSucceed)
{
return Task.FromResult(false);
}
RunningReplacements++;
return Task.FromResult(true);
}
public Task<bool> TryReplaceAsync(GraphBuildJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<GraphBuildJob> ReplaceAsync(GraphBuildJob job, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<GraphOverlayJob> ReplaceAsync(GraphOverlayJob job, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task InsertAsync(GraphBuildJob job, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task InsertAsync(GraphOverlayJob job, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<GraphBuildJob?> GetBuildJobAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<GraphOverlayJob?> GetOverlayJobAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<IReadOnlyList<GraphBuildJob>> ListBuildJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<IReadOnlyList<GraphBuildJob>> ListBuildJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<IReadOnlyList<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<IReadOnlyList<GraphOverlayJob>> ListOverlayJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
}
private sealed class StubCartographerOverlayClient : ICartographerOverlayClient
{
public CartographerOverlayResult Result { get; set; } = new(GraphJobStatus.Completed, null, null, null);
public Exception? ExceptionToThrow { get; set; }
public int CallCount { get; private set; }
public Task<CartographerOverlayResult> StartOverlayAsync(GraphOverlayJob job, CancellationToken cancellationToken)
{
CallCount++;
if (ExceptionToThrow is not null)
{
throw ExceptionToThrow;
}
return Task.FromResult(Result);
}
}
private sealed class RecordingCompletionClient : IGraphJobCompletionClient
{
public List<GraphJobCompletionRequestDto> Notifications { get; } = new();
public Task NotifyAsync(GraphJobCompletionRequestDto request, CancellationToken cancellationToken)
{
Notifications.Add(request);
return Task.CompletedTask;
}
}
}