release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -637,6 +637,7 @@ public sealed class MirrorOperationRecorderTests
|
||||
_emitter,
|
||||
_capsuleGenerator,
|
||||
_evidenceStore,
|
||||
TimeProvider.System,
|
||||
NullLogger<MirrorOperationRecorder>.Instance);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user