release orchestrator v1 draft and build fixes

This commit is contained in:
master
2026-01-12 12:24:17 +02:00
parent f3de858c59
commit 9873f80830
1598 changed files with 240385 additions and 5944 deletions

View File

@@ -90,17 +90,18 @@ public sealed record AuditEntry(
string? correlationId = null,
string? previousEntryHash = null,
long sequenceNumber = 0,
string? metadata = null)
string? metadata = null,
Guid? entryId = null)
{
ArgumentNullException.ThrowIfNull(hasher);
var entryId = Guid.NewGuid();
var actualEntryId = entryId ?? Guid.NewGuid();
// Compute canonical hash from immutable content
// Use the same property names and fields as VerifyIntegrity to keep the hash stable.
var contentHash = hasher.ComputeCanonicalHash(new
{
EntryId = entryId,
EntryId = actualEntryId,
TenantId = tenantId,
EventType = eventType,
ResourceType = resourceType,
@@ -115,7 +116,7 @@ public sealed record AuditEntry(
});
return new AuditEntry(
EntryId: entryId,
EntryId: actualEntryId,
TenantId: tenantId,
EventType: eventType,
ResourceType: resourceType,

View File

@@ -60,6 +60,12 @@ public sealed record SignedManifest(
/// </summary>
public const string CurrentSchemaVersion = "1.0.0";
/// <summary>
/// Indicates whether the manifest has expired.
/// A manifest is expired if it has an ExpiresAt value and that time has passed.
/// </summary>
public bool IsExpired => ExpiresAt.HasValue && ExpiresAt.Value < DateTimeOffset.UtcNow;
/// <summary>
/// Creates an unsigned manifest from a ledger entry.
/// The manifest must be signed separately using SigningService.

View File

@@ -196,6 +196,7 @@ public sealed class JobAttestationService : IJobAttestationService
private readonly IJobAttestationStore _store;
private readonly ITimelineEventEmitter _timelineEmitter;
private readonly TimeProvider _timeProvider;
private readonly StellaOps.Determinism.IGuidProvider _guidProvider;
private readonly ILogger<JobAttestationService> _logger;
public JobAttestationService(
@@ -203,13 +204,15 @@ public sealed class JobAttestationService : IJobAttestationService
IJobAttestationStore store,
ITimelineEventEmitter timelineEmitter,
TimeProvider timeProvider,
ILogger<JobAttestationService> logger)
ILogger<JobAttestationService> logger,
StellaOps.Determinism.IGuidProvider? guidProvider = null)
{
_signer = signer ?? throw new ArgumentNullException(nameof(signer));
_store = store ?? throw new ArgumentNullException(nameof(store));
_timelineEmitter = timelineEmitter ?? throw new ArgumentNullException(nameof(timelineEmitter));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance;
}
public async Task<JobAttestationResult> GenerateJobCompletionAttestationAsync(
@@ -547,7 +550,7 @@ public sealed class JobAttestationService : IJobAttestationService
cancellationToken);
// Create attestation record
var attestationId = Guid.NewGuid();
var attestationId = _guidProvider.NewGuid();
var payloadDigest = "sha256:" + Convert.ToHexString(SHA256.HashData(payloadBytes)).ToLowerInvariant();
return new JobAttestation(

View File

@@ -1,3 +1,4 @@
using StellaOps.Determinism;
using StellaOps.Orchestrator.Core.Domain;
using StellaOps.Orchestrator.Core.Domain.Export;
@@ -99,15 +100,18 @@ public sealed class ExportJobService : IExportJobService
private readonly IJobRepository _jobRepository;
private readonly IQuotaRepository _quotaRepository;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public ExportJobService(
IJobRepository jobRepository,
IQuotaRepository quotaRepository,
TimeProvider? timeProvider = null)
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null)
{
_jobRepository = jobRepository;
_quotaRepository = quotaRepository;
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
public async Task<Job> CreateExportJobAsync(
@@ -134,7 +138,7 @@ public sealed class ExportJobService : IExportJobService
var now = _timeProvider.GetUtcNow();
var job = new Job(
JobId: Guid.NewGuid(),
JobId: _guidProvider.NewGuid(),
TenantId: tenantId,
ProjectId: projectId,
RunId: null,

View File

@@ -19,6 +19,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@@ -56,7 +56,7 @@ public sealed class LedgerExporter : ILedgerExporter
export.ExportId, export.TenantId, export.Format);
// Mark export as started
export = export.Start();
export = export.Start(startTime);
export = await _exportRepository.UpdateAsync(export, cancellationToken);
// Fetch entries based on filters
@@ -83,7 +83,8 @@ public sealed class LedgerExporter : ILedgerExporter
var sizeBytes = Encoding.UTF8.GetByteCount(content);
// Complete the export
export = export.Complete(outputUri, digest, sizeBytes, entries.Count);
var completedAt = _timeProvider.GetUtcNow();
export = export.Complete(outputUri, digest, sizeBytes, entries.Count, completedAt);
export = await _exportRepository.UpdateAsync(export, cancellationToken);
var duration = _timeProvider.GetUtcNow() - startTime;
@@ -105,7 +106,8 @@ public sealed class LedgerExporter : ILedgerExporter
OrchestratorMetrics.LedgerExportFailed(export.TenantId, export.Format);
export = export.Fail(ex.Message);
var failedAt = _timeProvider.GetUtcNow();
export = export.Fail(ex.Message, failedAt);
export = await _exportRepository.UpdateAsync(export, cancellationToken);
throw;
@@ -123,7 +125,8 @@ public sealed class LedgerExporter : ILedgerExporter
"Generating manifest for ledger entry {LedgerId}, run {RunId}",
entry.LedgerId, entry.RunId);
var manifest = SignedManifest.CreateFromLedgerEntry(entry, buildInfo);
var createdAt = _timeProvider.GetUtcNow();
var manifest = SignedManifest.CreateFromLedgerEntry(entry, createdAt, buildInfo);
OrchestratorMetrics.ManifestCreated(entry.TenantId, "run");
@@ -140,7 +143,8 @@ public sealed class LedgerExporter : ILedgerExporter
"Generating manifest for export {ExportId} with {EntryCount} entries",
export.ExportId, entries.Count);
var manifest = SignedManifest.CreateFromExport(export, entries);
var createdAt = _timeProvider.GetUtcNow();
var manifest = SignedManifest.CreateFromExport(export, entries, createdAt);
OrchestratorMetrics.ManifestCreated(export.TenantId, "export");

View File

@@ -64,15 +64,18 @@ public sealed class PostgresAuditRepository : IAuditRepository
private readonly OrchestratorDataSource _dataSource;
private readonly CanonicalJsonHasher _hasher;
private readonly ILogger<PostgresAuditRepository> _logger;
private readonly TimeProvider _timeProvider;
public PostgresAuditRepository(
OrchestratorDataSource dataSource,
CanonicalJsonHasher hasher,
ILogger<PostgresAuditRepository> logger)
ILogger<PostgresAuditRepository> logger,
TimeProvider? timeProvider = null)
{
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
_hasher = hasher ?? throw new ArgumentNullException(nameof(hasher));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<AuditEntry> AppendAsync(
@@ -118,6 +121,7 @@ public sealed class PostgresAuditRepository : IAuditRepository
}
// Create the entry
var occurredAt = _timeProvider.GetUtcNow();
var entry = AuditEntry.Create(
hasher: _hasher,
tenantId: tenantId,
@@ -127,6 +131,7 @@ public sealed class PostgresAuditRepository : IAuditRepository
actorId: actorId,
actorType: actorType,
description: description,
occurredAt: occurredAt,
oldState: oldState,
newState: newState,
actorIp: actorIp,

View File

@@ -72,13 +72,16 @@ public sealed class PostgresLedgerRepository : ILedgerRepository
private readonly OrchestratorDataSource _dataSource;
private readonly ILogger<PostgresLedgerRepository> _logger;
private readonly TimeProvider _timeProvider;
public PostgresLedgerRepository(
OrchestratorDataSource dataSource,
ILogger<PostgresLedgerRepository> logger)
ILogger<PostgresLedgerRepository> logger,
TimeProvider? timeProvider = null)
{
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<RunLedgerEntry> AppendAsync(
@@ -118,12 +121,14 @@ public sealed class PostgresLedgerRepository : ILedgerRepository
}
// Create the ledger entry
var ledgerCreatedAt = _timeProvider.GetUtcNow();
var entry = RunLedgerEntry.FromCompletedRun(
run: run,
artifacts: artifacts,
inputDigest: inputDigest,
sequenceNumber: sequenceNumber,
previousEntryHash: previousEntryHash,
ledgerCreatedAt: ledgerCreatedAt,
metadata: metadata);
// Insert the entry

View File

@@ -153,10 +153,11 @@ public sealed record BackfillCheckpoint(
DateTimeOffset batchStart,
DateTimeOffset batchEnd,
int eventsInBatch,
DateTimeOffset startedAt)
DateTimeOffset startedAt,
Guid? checkpointId = null)
{
return new BackfillCheckpoint(
CheckpointId: Guid.NewGuid(),
CheckpointId: checkpointId ?? Guid.NewGuid(),
TenantId: tenantId,
BackfillId: backfillId,
BatchNumber: batchNumber,

View File

@@ -7,16 +7,18 @@ namespace StellaOps.Orchestrator.Tests.AuditLedger;
/// </summary>
public sealed class SignedManifestTests
{
private static readonly DateTimeOffset BaseTime = new(2024, 1, 1, 12, 0, 0, TimeSpan.Zero);
[Fact]
public void CreateFromLedgerEntry_WithValidEntry_CreatesManifest()
{
// Arrange
var run = CreateCompletedRun();
var artifacts = CreateArtifacts(run.RunId, 2);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, artifacts, "input-digest", 1, null);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, artifacts, "input-digest", 1, null, BaseTime);
// Act
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
// Assert
Assert.NotEqual(Guid.Empty, manifest.ManifestId);
@@ -43,7 +45,7 @@ public sealed class SignedManifestTests
var entries = CreateLedgerEntries(3);
// Act
var manifest = SignedManifest.CreateFromExport(export, entries);
var manifest = SignedManifest.CreateFromExport(export, entries, BaseTime);
// Assert
Assert.NotEqual(Guid.Empty, manifest.ManifestId);
@@ -60,11 +62,12 @@ public sealed class SignedManifestTests
var export = LedgerExport.CreateRequest(
tenantId: "test-tenant",
format: "json",
requestedBy: "user");
requestedBy: "user",
requestedAt: BaseTime);
// Act & Assert
Assert.Throws<InvalidOperationException>(() =>
SignedManifest.CreateFromExport(export, []));
SignedManifest.CreateFromExport(export, [], BaseTime));
}
[Fact]
@@ -72,8 +75,8 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
// Act
var signed = manifest.Sign(
@@ -96,8 +99,8 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
// Act & Assert
Assert.Throws<ArgumentException>(() =>
@@ -109,8 +112,8 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
// Act & Assert
Assert.Throws<ArgumentException>(() =>
@@ -122,8 +125,8 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
// Act & Assert
Assert.Throws<ArgumentException>(() =>
@@ -135,8 +138,8 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
// Assert
Assert.False(manifest.IsSigned);
@@ -147,8 +150,8 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
// Assert
Assert.False(manifest.IsExpired);
@@ -159,8 +162,8 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry)
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime)
.Sign("ES256", "sig", "key", DateTimeOffset.UtcNow.AddDays(30));
// Assert
@@ -172,8 +175,8 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry)
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime)
.Sign("ES256", "sig", "key", DateTimeOffset.UtcNow.AddDays(-1));
// Assert
@@ -185,8 +188,8 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
// Act
var isValid = manifest.VerifyPayloadIntegrity();
@@ -200,8 +203,8 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
// Tamper with the manifest
var tampered = manifest with { Statements = "[]" };
@@ -219,8 +222,8 @@ public sealed class SignedManifestTests
// Arrange
var run = CreateCompletedRun();
var artifacts = CreateArtifacts(run.RunId, 2);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, artifacts, "input", 1, null);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, artifacts, "input", 1, null, BaseTime);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
// Act
var references = manifest.GetArtifactReferences();
@@ -241,8 +244,8 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input-digest", 1, null);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input-digest", 1, null, BaseTime);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
// Act
var materials = manifest.GetMaterialReferences();
@@ -258,8 +261,8 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
// Act
var statements = manifest.GetStatements();
@@ -281,13 +284,13 @@ public sealed class SignedManifestTests
if (expectedType == ProvenanceType.Run)
{
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
}
else
{
var export = CreateCompletedExport();
manifest = SignedManifest.CreateFromExport(export, []);
manifest = SignedManifest.CreateFromExport(export, [], BaseTime);
}
// Assert
@@ -300,11 +303,11 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
var buildInfo = """{"version":"1.0.0","builder":"test"}""";
// Act
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, buildInfo);
var manifest = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime, buildInfo);
// Assert
Assert.Equal(buildInfo, manifest.BuildInfo);
@@ -315,11 +318,11 @@ public sealed class SignedManifestTests
{
// Arrange
var run = CreateCompletedRun();
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null);
var ledgerEntry = RunLedgerEntry.FromCompletedRun(run, [], "input", 1, null, BaseTime);
// Act
var manifest1 = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var manifest2 = SignedManifest.CreateFromLedgerEntry(ledgerEntry);
var manifest1 = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
var manifest2 = SignedManifest.CreateFromLedgerEntry(ledgerEntry, BaseTime);
// Note: ManifestId will differ, but the payload digest should be the same
// if the content (statements, artifacts, materials) is identical
@@ -352,11 +355,12 @@ public sealed class SignedManifestTests
var export = LedgerExport.CreateRequest(
tenantId: "test-tenant",
format: "json",
requestedBy: "user");
requestedBy: "user",
requestedAt: BaseTime);
return export
.Start()
.Complete("file:///exports/test.json", "sha256:abc123", 1024, 10);
.Start(BaseTime.AddMinutes(1))
.Complete("file:///exports/test.json", "sha256:abc123", 1024, 10, BaseTime.AddMinutes(2));
}
private static List<Artifact> CreateArtifacts(Guid runId, int count)
@@ -388,7 +392,7 @@ public sealed class SignedManifestTests
for (var i = 0; i < count; i++)
{
var run = CreateCompletedRun();
var entry = RunLedgerEntry.FromCompletedRun(run, [], $"input-{i}", i + 1, previousHash);
var entry = RunLedgerEntry.FromCompletedRun(run, [], $"input-{i}", i + 1, previousHash, BaseTime.AddMinutes(i));
entries.Add(entry);
previousHash = entry.ContentHash;
}
@@ -396,3 +400,4 @@ public sealed class SignedManifestTests
return entries;
}
}

View File

@@ -22,7 +22,8 @@ public class BackfillRequestTests
windowStart: windowStart,
windowEnd: windowEnd,
reason: "Reprocess after bug fix",
createdBy: "admin");
createdBy: "admin",
timestamp: BaseTime);
Assert.NotEqual(Guid.Empty, request.BackfillId);
Assert.Equal(TenantId, request.TenantId);
@@ -48,7 +49,7 @@ public class BackfillRequestTests
{
var request = BackfillRequest.Create(
TenantId, SourceId, null, BaseTime, BaseTime.AddDays(1),
"Test", "admin", dryRun: true, forceReprocess: true);
"Test", "admin", BaseTime, dryRun: true, forceReprocess: true);
Assert.True(request.DryRun);
Assert.True(request.ForceReprocess);
@@ -59,7 +60,7 @@ public class BackfillRequestTests
{
var request = BackfillRequest.Create(
TenantId, SourceId, null, BaseTime, BaseTime.AddDays(1),
"Test", "admin", batchSize: 500);
"Test", "admin", BaseTime, batchSize: 500);
Assert.Equal(500, request.BatchSize);
}
@@ -69,11 +70,11 @@ public class BackfillRequestTests
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
BackfillRequest.Create(TenantId, SourceId, null, BaseTime, BaseTime.AddDays(1),
"Test", "admin", batchSize: 0));
"Test", "admin", BaseTime, batchSize: 0));
Assert.Throws<ArgumentOutOfRangeException>(() =>
BackfillRequest.Create(TenantId, SourceId, null, BaseTime, BaseTime.AddDays(1),
"Test", "admin", batchSize: 10001));
"Test", "admin", BaseTime, batchSize: 10001));
}
[Fact]
@@ -84,7 +85,8 @@ public class BackfillRequestTests
windowStart: BaseTime.AddDays(1),
windowEnd: BaseTime,
reason: "Test",
createdBy: "admin"));
createdBy: "admin",
timestamp: BaseTime));
}
[Fact]
@@ -92,14 +94,14 @@ public class BackfillRequestTests
{
Assert.Throws<ArgumentException>(() =>
BackfillRequest.Create(TenantId, null, null, BaseTime, BaseTime.AddDays(1),
"Test", "admin"));
"Test", "admin", BaseTime));
}
[Fact]
public void WindowDuration_ReturnsCorrectDuration()
{
var request = BackfillRequest.Create(TenantId, SourceId, null,
BaseTime, BaseTime.AddDays(7), "Test", "admin");
BaseTime, BaseTime.AddDays(7), "Test", "admin", BaseTime);
Assert.Equal(TimeSpan.FromDays(7), request.WindowDuration);
}
@@ -108,7 +110,7 @@ public class BackfillRequestTests
public void StartValidation_TransitionsToPending()
{
var request = BackfillRequest.Create(TenantId, SourceId, null,
BaseTime, BaseTime.AddDays(1), "Test", "admin");
BaseTime, BaseTime.AddDays(1), "Test", "admin", BaseTime);
var validating = request.StartValidation("validator");
@@ -120,7 +122,7 @@ public class BackfillRequestTests
public void StartValidation_FromNonPending_Throws()
{
var request = BackfillRequest.Create(TenantId, SourceId, null,
BaseTime, BaseTime.AddDays(1), "Test", "admin");
BaseTime, BaseTime.AddDays(1), "Test", "admin", BaseTime);
var validating = request.StartValidation("v");
Assert.Throws<InvalidOperationException>(() =>
@@ -131,7 +133,7 @@ public class BackfillRequestTests
public void WithSafetyChecks_RecordsSafetyResults()
{
var request = BackfillRequest.Create(TenantId, SourceId, null,
BaseTime, BaseTime.AddDays(1), "Test", "admin")
BaseTime, BaseTime.AddDays(1), "Test", "admin", BaseTime)
.StartValidation("v");
var checks = BackfillSafetyChecks.AllPassed();
@@ -146,11 +148,11 @@ public class BackfillRequestTests
public void Start_TransitionsToRunning()
{
var request = BackfillRequest.Create(TenantId, SourceId, null,
BaseTime, BaseTime.AddDays(1), "Test", "admin")
BaseTime, BaseTime.AddDays(1), "Test", "admin", BaseTime)
.StartValidation("v")
.WithSafetyChecks(BackfillSafetyChecks.AllPassed(), 1000, TimeSpan.FromMinutes(10), "v");
var running = request.Start("worker");
var running = request.Start("worker", BaseTime.AddMinutes(1));
Assert.Equal(BackfillStatus.Running, running.Status);
Assert.NotNull(running.StartedAt);
@@ -172,21 +174,21 @@ public class BackfillRequestTests
Errors: ["Source not found"]);
var request = BackfillRequest.Create(TenantId, SourceId, null,
BaseTime, BaseTime.AddDays(1), "Test", "admin")
BaseTime, BaseTime.AddDays(1), "Test", "admin", BaseTime)
.StartValidation("v")
.WithSafetyChecks(checks, 1000, TimeSpan.FromMinutes(10), "v");
Assert.Throws<InvalidOperationException>(() => request.Start("worker"));
Assert.Throws<InvalidOperationException>(() => request.Start("worker", BaseTime));
}
[Fact]
public void UpdateProgress_UpdatesCounters()
{
var request = BackfillRequest.Create(TenantId, SourceId, null,
BaseTime, BaseTime.AddDays(1), "Test", "admin")
BaseTime, BaseTime.AddDays(1), "Test", "admin", BaseTime)
.StartValidation("v")
.WithSafetyChecks(BackfillSafetyChecks.AllPassed(), 1000, TimeSpan.FromMinutes(10), "v")
.Start("worker");
.Start("worker", BaseTime.AddMinutes(1));
var newPosition = BaseTime.AddHours(6);
var updated = request.UpdateProgress(newPosition, processed: 500, skipped: 50, failed: 5, "worker");
@@ -201,10 +203,10 @@ public class BackfillRequestTests
public void UpdateProgress_AccumulatesCounts()
{
var request = BackfillRequest.Create(TenantId, SourceId, null,
BaseTime, BaseTime.AddDays(1), "Test", "admin")
BaseTime, BaseTime.AddDays(1), "Test", "admin", BaseTime)
.StartValidation("v")
.WithSafetyChecks(BackfillSafetyChecks.AllPassed(), 1000, TimeSpan.FromMinutes(10), "v")
.Start("worker");
.Start("worker", BaseTime.AddMinutes(1));
var after1 = request.UpdateProgress(BaseTime.AddHours(1), 100, 10, 1, "w");
var after2 = after1.UpdateProgress(BaseTime.AddHours(2), 200, 20, 2, "w");
@@ -218,10 +220,10 @@ public class BackfillRequestTests
public void ProgressPercent_CalculatesCorrectly()
{
var request = BackfillRequest.Create(TenantId, SourceId, null,
BaseTime, BaseTime.AddDays(1), "Test", "admin")
BaseTime, BaseTime.AddDays(1), "Test", "admin", BaseTime)
.StartValidation("v")
.WithSafetyChecks(BackfillSafetyChecks.AllPassed(), 1000, TimeSpan.FromMinutes(10), "v")
.Start("worker")
.Start("worker", BaseTime.AddMinutes(1))
.UpdateProgress(BaseTime.AddHours(12), 400, 50, 50, "w");
Assert.Equal(50.0, request.ProgressPercent);
@@ -231,10 +233,10 @@ public class BackfillRequestTests
public void Pause_TransitionsToPaused()
{
var request = BackfillRequest.Create(TenantId, SourceId, null,
BaseTime, BaseTime.AddDays(1), "Test", "admin")
BaseTime, BaseTime.AddDays(1), "Test", "admin", BaseTime)
.StartValidation("v")
.WithSafetyChecks(BackfillSafetyChecks.AllPassed(), 1000, TimeSpan.FromMinutes(10), "v")
.Start("worker");
.Start("worker", BaseTime.AddMinutes(1));
var paused = request.Pause("admin");
@@ -245,10 +247,10 @@ public class BackfillRequestTests
public void Resume_TransitionsToRunning()
{
var request = BackfillRequest.Create(TenantId, SourceId, null,
BaseTime, BaseTime.AddDays(1), "Test", "admin")
BaseTime, BaseTime.AddDays(1), "Test", "admin", BaseTime)
.StartValidation("v")
.WithSafetyChecks(BackfillSafetyChecks.AllPassed(), 1000, TimeSpan.FromMinutes(10), "v")
.Start("worker")
.Start("worker", BaseTime.AddMinutes(1))
.Pause("admin");
var resumed = request.Resume("admin");

View File

@@ -637,6 +637,7 @@ public sealed class MirrorOperationRecorderTests
_emitter,
_capsuleGenerator,
_evidenceStore,
TimeProvider.System,
NullLogger<MirrorOperationRecorder>.Instance);
}

View File

@@ -495,7 +495,7 @@ public sealed class ExportAlertTests
exportType: "export.sbom",
severity: ExportAlertSeverity.Error,
failedJobIds: failedJobs,
consecutiveFailures: 3);
consecutiveFailures: 3, timestamp: DateTimeOffset.UtcNow);
var after = DateTimeOffset.UtcNow;
@@ -527,7 +527,7 @@ public sealed class ExportAlertTests
exportType: "export.report",
severity: ExportAlertSeverity.Warning,
failureRate: 75.5,
recentFailedJobIds: failedJobs);
recentFailedJobIds: failedJobs, timestamp: DateTimeOffset.UtcNow);
Assert.Contains("failure rate is 75.5%", alert.Message);
Assert.Equal(0, alert.ConsecutiveFailures);
@@ -543,10 +543,10 @@ public sealed class ExportAlertTests
exportType: "export.sbom",
severity: ExportAlertSeverity.Error,
failedJobIds: [Guid.NewGuid()],
consecutiveFailures: 1);
consecutiveFailures: 1, timestamp: DateTimeOffset.UtcNow);
var before = DateTimeOffset.UtcNow;
var acknowledged = alert.Acknowledge("operator@example.com");
var acknowledged = alert.Acknowledge("operator@example.com", DateTimeOffset.UtcNow);
var after = DateTimeOffset.UtcNow;
Assert.NotNull(acknowledged.AcknowledgedAt);
@@ -564,10 +564,10 @@ public sealed class ExportAlertTests
exportType: "export.sbom",
severity: ExportAlertSeverity.Error,
failedJobIds: [Guid.NewGuid()],
consecutiveFailures: 1);
consecutiveFailures: 1, timestamp: DateTimeOffset.UtcNow);
var before = DateTimeOffset.UtcNow;
var resolved = alert.Resolve("Fixed database connection issue");
var resolved = alert.Resolve(DateTimeOffset.UtcNow, "Fixed database connection issue");
var after = DateTimeOffset.UtcNow;
Assert.NotNull(resolved.ResolvedAt);
@@ -587,9 +587,9 @@ public sealed class ExportAlertTests
exportType: "export.sbom",
severity: ExportAlertSeverity.Error,
failedJobIds: [Guid.NewGuid()],
consecutiveFailures: 1);
consecutiveFailures: 1, timestamp: DateTimeOffset.UtcNow);
var resolved = alert.Resolve();
var resolved = alert.Resolve(DateTimeOffset.UtcNow);
Assert.NotNull(resolved.ResolvedAt);
Assert.Null(resolved.ResolutionNotes);
@@ -605,11 +605,11 @@ public sealed class ExportAlertTests
exportType: "export.sbom",
severity: ExportAlertSeverity.Error,
failedJobIds: [Guid.NewGuid()],
consecutiveFailures: 1);
consecutiveFailures: 1, timestamp: DateTimeOffset.UtcNow);
Assert.True(alert.IsActive);
var acknowledged = alert.Acknowledge("user@example.com");
var acknowledged = alert.Acknowledge("user@example.com", DateTimeOffset.UtcNow);
Assert.True(acknowledged.IsActive);
}
@@ -622,9 +622,9 @@ public sealed class ExportAlertTests
exportType: "export.sbom",
severity: ExportAlertSeverity.Error,
failedJobIds: [Guid.NewGuid()],
consecutiveFailures: 1);
consecutiveFailures: 1, timestamp: DateTimeOffset.UtcNow);
var resolved = alert.Resolve();
var resolved = alert.Resolve(DateTimeOffset.UtcNow);
Assert.False(resolved.IsActive);
}
}

View File

@@ -14,12 +14,14 @@ public class SloTests
[Fact]
public void CreateAvailability_SetsCorrectProperties()
{
var now = DateTimeOffset.UtcNow;
var slo = Slo.CreateAvailability(
TenantId,
"API Availability",
target: 0.999,
window: SloWindow.ThirtyDays,
createdBy: "admin",
createdAt: now,
description: "99.9% uptime target");
Assert.NotEqual(Guid.Empty, slo.SloId);
@@ -38,12 +40,14 @@ public class SloTests
[Fact]
public void CreateAvailability_WithJobType_SetsJobType()
{
var now = DateTimeOffset.UtcNow;
var slo = Slo.CreateAvailability(
TenantId,
"Scan Availability",
0.99,
SloWindow.SevenDays,
"admin",
now,
jobType: "scan.image");
Assert.Equal("scan.image", slo.JobType);
@@ -52,6 +56,7 @@ public class SloTests
[Fact]
public void CreateAvailability_WithSourceId_SetsSourceId()
{
var now = DateTimeOffset.UtcNow;
var sourceId = Guid.NewGuid();
var slo = Slo.CreateAvailability(
TenantId,
@@ -59,6 +64,7 @@ public class SloTests
0.995,
SloWindow.OneDay,
"admin",
now,
sourceId: sourceId);
Assert.Equal(sourceId, slo.SourceId);
@@ -67,6 +73,7 @@ public class SloTests
[Fact]
public void CreateLatency_SetsCorrectProperties()
{
var now = DateTimeOffset.UtcNow;
var slo = Slo.CreateLatency(
TenantId,
"API Latency P95",
@@ -74,7 +81,8 @@ public class SloTests
targetSeconds: 0.5,
target: 0.99,
window: SloWindow.OneDay,
createdBy: "admin");
createdBy: "admin",
createdAt: now);
Assert.Equal(SloType.Latency, slo.Type);
Assert.Equal(0.95, slo.LatencyPercentile);
@@ -85,13 +93,15 @@ public class SloTests
[Fact]
public void CreateThroughput_SetsCorrectProperties()
{
var now = DateTimeOffset.UtcNow;
var slo = Slo.CreateThroughput(
TenantId,
"Scan Throughput",
minimum: 1000,
target: 0.95,
window: SloWindow.OneHour,
createdBy: "admin");
createdBy: "admin",
createdAt: now);
Assert.Equal(SloType.Throughput, slo.Type);
Assert.Equal(1000, slo.ThroughputMinimum);
@@ -108,8 +118,9 @@ public class SloTests
[InlineData(1.1)]
public void CreateAvailability_WithInvalidTarget_Throws(double target)
{
var now = DateTimeOffset.UtcNow;
Assert.Throws<ArgumentOutOfRangeException>(() =>
Slo.CreateAvailability(TenantId, "Test", target, SloWindow.OneDay, "admin"));
Slo.CreateAvailability(TenantId, "Test", target, SloWindow.OneDay, "admin", now));
}
[Theory]
@@ -117,8 +128,9 @@ public class SloTests
[InlineData(1.1)]
public void CreateLatency_WithInvalidPercentile_Throws(double percentile)
{
var now = DateTimeOffset.UtcNow;
Assert.Throws<ArgumentOutOfRangeException>(() =>
Slo.CreateLatency(TenantId, "Test", percentile, 1.0, 0.99, SloWindow.OneDay, "admin"));
Slo.CreateLatency(TenantId, "Test", percentile, 1.0, 0.99, SloWindow.OneDay, "admin", now));
}
[Theory]
@@ -126,8 +138,9 @@ public class SloTests
[InlineData(-1.0)]
public void CreateLatency_WithInvalidTargetSeconds_Throws(double targetSeconds)
{
var now = DateTimeOffset.UtcNow;
Assert.Throws<ArgumentOutOfRangeException>(() =>
Slo.CreateLatency(TenantId, "Test", 0.95, targetSeconds, 0.99, SloWindow.OneDay, "admin"));
Slo.CreateLatency(TenantId, "Test", 0.95, targetSeconds, 0.99, SloWindow.OneDay, "admin", now));
}
[Theory]
@@ -135,8 +148,9 @@ public class SloTests
[InlineData(-1)]
public void CreateThroughput_WithInvalidMinimum_Throws(int minimum)
{
var now = DateTimeOffset.UtcNow;
Assert.Throws<ArgumentOutOfRangeException>(() =>
Slo.CreateThroughput(TenantId, "Test", minimum, 0.99, SloWindow.OneDay, "admin"));
Slo.CreateThroughput(TenantId, "Test", minimum, 0.99, SloWindow.OneDay, "admin", now));
}
// =========================================================================
@@ -150,7 +164,8 @@ public class SloTests
[InlineData(0.9, 0.1)]
public void ErrorBudget_CalculatesCorrectly(double target, double expectedBudget)
{
var slo = Slo.CreateAvailability(TenantId, "Test", target, SloWindow.OneDay, "admin");
var now = DateTimeOffset.UtcNow;
var slo = Slo.CreateAvailability(TenantId, "Test", target, SloWindow.OneDay, "admin", now);
Assert.Equal(expectedBudget, slo.ErrorBudget, precision: 10);
}
@@ -166,7 +181,8 @@ public class SloTests
[InlineData(SloWindow.ThirtyDays, 720)]
public void GetWindowDuration_ReturnsCorrectHours(SloWindow window, int expectedHours)
{
var slo = Slo.CreateAvailability(TenantId, "Test", 0.99, window, "admin");
var now = DateTimeOffset.UtcNow;
var slo = Slo.CreateAvailability(TenantId, "Test", 0.99, window, "admin", now);
Assert.Equal(TimeSpan.FromHours(expectedHours), slo.GetWindowDuration());
}
@@ -178,9 +194,10 @@ public class SloTests
[Fact]
public void Update_UpdatesOnlySpecifiedFields()
{
var slo = Slo.CreateAvailability(TenantId, "Original", 0.99, SloWindow.OneDay, "admin");
var now = DateTimeOffset.UtcNow;
var slo = Slo.CreateAvailability(TenantId, "Original", 0.99, SloWindow.OneDay, "admin", now);
var updated = slo.Update(name: "Updated", updatedBy: "operator");
var updated = slo.Update(updatedAt: now, name: "Updated", updatedBy: "operator");
Assert.Equal("Updated", updated.Name);
Assert.Equal(0.99, updated.Target); // Unchanged
@@ -191,9 +208,10 @@ public class SloTests
[Fact]
public void Update_WithNewTarget_UpdatesTarget()
{
var slo = Slo.CreateAvailability(TenantId, "Test", 0.99, SloWindow.OneDay, "admin");
var now = DateTimeOffset.UtcNow;
var slo = Slo.CreateAvailability(TenantId, "Test", 0.99, SloWindow.OneDay, "admin", now);
var updated = slo.Update(target: 0.999, updatedBy: "operator");
var updated = slo.Update(updatedAt: now, target: 0.999, updatedBy: "operator");
Assert.Equal(0.999, updated.Target);
}
@@ -201,10 +219,11 @@ public class SloTests
[Fact]
public void Update_WithInvalidTarget_Throws()
{
var slo = Slo.CreateAvailability(TenantId, "Test", 0.99, SloWindow.OneDay, "admin");
var now = DateTimeOffset.UtcNow;
var slo = Slo.CreateAvailability(TenantId, "Test", 0.99, SloWindow.OneDay, "admin", now);
Assert.Throws<ArgumentOutOfRangeException>(() =>
slo.Update(target: 1.5, updatedBy: "operator"));
slo.Update(updatedAt: now, target: 1.5, updatedBy: "operator"));
}
// =========================================================================
@@ -214,9 +233,10 @@ public class SloTests
[Fact]
public void Disable_SetsEnabledToFalse()
{
var slo = Slo.CreateAvailability(TenantId, "Test", 0.99, SloWindow.OneDay, "admin");
var now = DateTimeOffset.UtcNow;
var slo = Slo.CreateAvailability(TenantId, "Test", 0.99, SloWindow.OneDay, "admin", now);
var disabled = slo.Disable("operator");
var disabled = slo.Disable("operator", now);
Assert.False(disabled.Enabled);
Assert.Equal("operator", disabled.UpdatedBy);
@@ -225,10 +245,11 @@ public class SloTests
[Fact]
public void Enable_SetsEnabledToTrue()
{
var slo = Slo.CreateAvailability(TenantId, "Test", 0.99, SloWindow.OneDay, "admin")
.Disable("operator");
var now = DateTimeOffset.UtcNow;
var slo = Slo.CreateAvailability(TenantId, "Test", 0.99, SloWindow.OneDay, "admin", now)
.Disable("operator", now);
var enabled = slo.Enable("operator");
var enabled = slo.Enable("operator", now);
Assert.True(enabled.Enabled);
}

View File

@@ -379,12 +379,14 @@ public static class LedgerEndpoints
[FromBody] CreateLedgerExportRequest request,
[FromServices] TenantResolver tenantResolver,
[FromServices] ILedgerExportRepository repository,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var actorId = context.User?.Identity?.Name ?? "system";
var now = timeProvider.GetUtcNow();
// Validate format
var validFormats = new[] { "json", "ndjson", "csv" };
@@ -403,6 +405,7 @@ public static class LedgerEndpoints
tenantId: tenantId,
format: request.Format!,
requestedBy: actorId,
requestedAt: now,
startTime: request.StartTime,
endTime: request.EndTime,
runTypeFilter: request.RunTypeFilter,

View File

@@ -212,6 +212,7 @@ public static class PackRunEndpoints
eventType: OrchestratorEventType.PackRunCreated,
tenantId: tenantId,
actor: EventActor.User(context.User?.Identity?.Name ?? "system", "webservice"),
occurredAt: now,
correlationId: request.CorrelationId,
projectId: request.ProjectId,
payload: ToPayload(new { packRunId, packId = request.PackId, packVersion = request.PackVersion }));
@@ -483,6 +484,7 @@ public static class PackRunEndpoints
eventType: OrchestratorEventType.PackRunStarted,
tenantId: tenantId,
actor: EventActor.System("task-runner", packRun.TaskRunnerId ?? "unknown"),
occurredAt: now,
correlationId: packRun.CorrelationId,
projectId: packRun.ProjectId,
payload: ToPayload(new { packRunId, packId = packRun.PackId, packVersion = packRun.PackVersion }));
@@ -619,6 +621,7 @@ public static class PackRunEndpoints
eventType: eventType,
tenantId: tenantId,
actor: EventActor.System("task-runner", packRun.TaskRunnerId ?? "unknown"),
occurredAt: now,
correlationId: packRun.CorrelationId,
projectId: packRun.ProjectId,
payload: ToPayload(new
@@ -707,6 +710,7 @@ public static class PackRunEndpoints
eventType: OrchestratorEventType.PackRunLog,
tenantId: tenantId,
actor: EventActor.System("task-runner", packRun.TaskRunnerId ?? "unknown"),
occurredAt: now,
correlationId: packRun.CorrelationId,
projectId: packRun.ProjectId,
payload: ToPayload(new { packRunId, logCount = logs.Count, latestSequence = seq }));
@@ -829,6 +833,7 @@ public static class PackRunEndpoints
eventType: OrchestratorEventType.PackRunFailed, // Use Failed for canceled
tenantId: tenantId,
actor: EventActor.User(context.User?.Identity?.Name ?? "system", "webservice"),
occurredAt: now,
correlationId: packRun.CorrelationId,
projectId: packRun.ProjectId,
payload: ToPayload(new { packRunId, packId = packRun.PackId, status = "canceled", reason = request.Reason }));
@@ -902,6 +907,7 @@ public static class PackRunEndpoints
eventType: OrchestratorEventType.PackRunCreated,
tenantId: tenantId,
actor: EventActor.User(context.User?.Identity?.Name ?? "system", "webservice"),
occurredAt: now,
correlationId: packRun.CorrelationId,
projectId: packRun.ProjectId,
payload: ToPayload(new { packRunId = newPackRunId, packId = packRun.PackId, retriedFrom = packRunId }));

View File

@@ -161,12 +161,14 @@ public static class SloEndpoints
[FromBody] CreateSloRequest request,
[FromServices] TenantResolver tenantResolver,
[FromServices] ISloRepository repository,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var actorId = context.User?.Identity?.Name ?? "system";
var now = timeProvider.GetUtcNow();
// Parse and validate type
if (!TryParseSloType(request.Type, out var sloType))
@@ -184,20 +186,20 @@ public static class SloEndpoints
Slo slo = sloType switch
{
SloType.Availability => Slo.CreateAvailability(
tenantId, request.Name, request.Target, window, actorId,
tenantId, request.Name, request.Target, window, actorId, now,
request.Description, request.JobType, request.SourceId),
SloType.Latency => Slo.CreateLatency(
tenantId, request.Name,
request.LatencyPercentile ?? 0.95,
request.LatencyTargetSeconds ?? 1.0,
request.Target, window, actorId,
request.Target, window, actorId, now,
request.Description, request.JobType, request.SourceId),
SloType.Throughput => Slo.CreateThroughput(
tenantId, request.Name,
request.ThroughputMinimum ?? 1,
request.Target, window, actorId,
request.Target, window, actorId, now,
request.Description, request.JobType, request.SourceId),
_ => throw new InvalidOperationException($"Unknown SLO type: {sloType}")
@@ -223,12 +225,14 @@ public static class SloEndpoints
[FromBody] UpdateSloRequest request,
[FromServices] TenantResolver tenantResolver,
[FromServices] ISloRepository repository,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var actorId = context.User?.Identity?.Name ?? "system";
var now = timeProvider.GetUtcNow();
var slo = await repository.GetByIdAsync(tenantId, sloId, cancellationToken).ConfigureAwait(false);
if (slo is null)
@@ -237,6 +241,7 @@ public static class SloEndpoints
}
var updated = slo.Update(
updatedAt: now,
name: request.Name,
description: request.Description,
target: request.Target,
@@ -348,12 +353,14 @@ public static class SloEndpoints
[FromRoute] Guid sloId,
[FromServices] TenantResolver tenantResolver,
[FromServices] ISloRepository repository,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var actorId = context.User?.Identity?.Name ?? "system";
var now = timeProvider.GetUtcNow();
var slo = await repository.GetByIdAsync(tenantId, sloId, cancellationToken).ConfigureAwait(false);
if (slo is null)
@@ -361,7 +368,7 @@ public static class SloEndpoints
return Results.NotFound();
}
var enabled = slo.Enable(actorId);
var enabled = slo.Enable(actorId, now);
await repository.UpdateAsync(enabled, cancellationToken).ConfigureAwait(false);
return Results.Ok(SloResponse.FromDomain(enabled));
@@ -377,12 +384,14 @@ public static class SloEndpoints
[FromRoute] Guid sloId,
[FromServices] TenantResolver tenantResolver,
[FromServices] ISloRepository repository,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var actorId = context.User?.Identity?.Name ?? "system";
var now = timeProvider.GetUtcNow();
var slo = await repository.GetByIdAsync(tenantId, sloId, cancellationToken).ConfigureAwait(false);
if (slo is null)
@@ -390,7 +399,7 @@ public static class SloEndpoints
return Results.NotFound();
}
var disabled = slo.Disable(actorId);
var disabled = slo.Disable(actorId, now);
await repository.UpdateAsync(disabled, cancellationToken).ConfigureAwait(false);
return Results.Ok(SloResponse.FromDomain(disabled));
@@ -437,12 +446,14 @@ public static class SloEndpoints
[FromServices] TenantResolver tenantResolver,
[FromServices] ISloRepository sloRepository,
[FromServices] IAlertThresholdRepository repository,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken = default)
{
try
{
var tenantId = tenantResolver.Resolve(context);
var actorId = context.User?.Identity?.Name ?? "system";
var now = timeProvider.GetUtcNow();
var slo = await sloRepository.GetByIdAsync(tenantId, sloId, cancellationToken).ConfigureAwait(false);
if (slo is null)
@@ -461,6 +472,7 @@ public static class SloEndpoints
budgetConsumedThreshold: request.BudgetConsumedThreshold,
severity: severity,
createdBy: actorId,
createdAt: now,
burnRateThreshold: request.BurnRateThreshold,
notificationChannel: request.NotificationChannel,
notificationEndpoint: request.NotificationEndpoint,