747 lines
24 KiB
C#
747 lines
24 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using StellaOps.TaskRunner.Core.Events;
|
|
using Xunit;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.TaskRunner.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests for pack run timeline event domain model, emitter, and sink.
|
|
/// Per TASKRUN-OBS-52-001.
|
|
/// </summary>
|
|
public sealed class PackRunTimelineEventTests
|
|
{
|
|
private const string TestTenantId = "test-tenant";
|
|
private const string TestRunId = "run-12345";
|
|
private const string TestPlanHash = "sha256:abc123";
|
|
private const string TestStepId = "step-001";
|
|
private const string TestProjectId = "project-xyz";
|
|
|
|
#region Domain Model Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Create_WithRequiredFields_GeneratesValidEvent()
|
|
{
|
|
// Arrange
|
|
var occurredAt = DateTimeOffset.UtcNow;
|
|
|
|
// Act
|
|
var evt = PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.PackStarted,
|
|
source: "taskrunner-worker",
|
|
occurredAt: occurredAt,
|
|
runId: TestRunId,
|
|
planHash: TestPlanHash);
|
|
|
|
// Assert
|
|
Assert.NotEqual(Guid.Empty, evt.EventId);
|
|
Assert.Equal(TestTenantId, evt.TenantId);
|
|
Assert.Equal(PackRunEventTypes.PackStarted, evt.EventType);
|
|
Assert.Equal("taskrunner-worker", evt.Source);
|
|
Assert.Equal(occurredAt, evt.OccurredAt);
|
|
Assert.Equal(TestRunId, evt.RunId);
|
|
Assert.Equal(TestPlanHash, evt.PlanHash);
|
|
Assert.Null(evt.ReceivedAt);
|
|
Assert.Null(evt.EventSeq);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Create_WithPayload_ComputesHashAndNormalizes()
|
|
{
|
|
// Arrange
|
|
var payload = new { stepId = "step-001", attempt = 1 };
|
|
|
|
// Act
|
|
var evt = PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.StepStarted,
|
|
source: "taskrunner-worker",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: TestRunId,
|
|
planHash: TestPlanHash,
|
|
payload: payload);
|
|
|
|
// Assert
|
|
Assert.NotNull(evt.RawPayloadJson);
|
|
Assert.NotNull(evt.NormalizedPayloadJson);
|
|
Assert.NotNull(evt.PayloadHash);
|
|
Assert.StartsWith("sha256:", evt.PayloadHash);
|
|
Assert.Equal(64 + 7, evt.PayloadHash.Length); // sha256: prefix + 64 hex chars
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Create_WithStepId_SetsStepId()
|
|
{
|
|
// Act
|
|
var evt = PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.StepCompleted,
|
|
source: "taskrunner-worker",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: TestRunId,
|
|
planHash: TestPlanHash,
|
|
stepId: TestStepId);
|
|
|
|
// Assert
|
|
Assert.Equal(TestStepId, evt.StepId);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Create_WithEvidencePointer_SetsPointer()
|
|
{
|
|
// Arrange
|
|
var evidence = PackRunEvidencePointer.Bundle(Guid.NewGuid(), "sha256:def456");
|
|
|
|
// Act
|
|
var evt = PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.PackCompleted,
|
|
source: "taskrunner-worker",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: TestRunId,
|
|
planHash: TestPlanHash,
|
|
evidencePointer: evidence);
|
|
|
|
// Assert
|
|
Assert.NotNull(evt.EvidencePointer);
|
|
Assert.Equal(PackRunEvidencePointerType.Bundle, evt.EvidencePointer.Type);
|
|
Assert.Equal("sha256:def456", evt.EvidencePointer.BundleDigest);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void WithReceivedAt_CreatesCopyWithTimestamp()
|
|
{
|
|
// Arrange
|
|
var evt = PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.PackStarted,
|
|
source: "taskrunner-worker",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: TestRunId,
|
|
planHash: TestPlanHash);
|
|
|
|
var receivedAt = DateTimeOffset.UtcNow.AddSeconds(1);
|
|
|
|
// Act
|
|
var updated = evt.WithReceivedAt(receivedAt);
|
|
|
|
// Assert
|
|
Assert.Null(evt.ReceivedAt);
|
|
Assert.Equal(receivedAt, updated.ReceivedAt);
|
|
Assert.Equal(evt.EventId, updated.EventId);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void WithSequence_CreatesCopyWithSequence()
|
|
{
|
|
// Arrange
|
|
var evt = PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.PackStarted,
|
|
source: "taskrunner-worker",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: TestRunId,
|
|
planHash: TestPlanHash);
|
|
|
|
// Act
|
|
var updated = evt.WithSequence(42);
|
|
|
|
// Assert
|
|
Assert.Null(evt.EventSeq);
|
|
Assert.Equal(42, updated.EventSeq);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void ToJson_SerializesEvent()
|
|
{
|
|
// Arrange
|
|
var evt = PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.StepCompleted,
|
|
source: "taskrunner-worker",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: TestRunId,
|
|
planHash: TestPlanHash,
|
|
stepId: TestStepId);
|
|
|
|
// Act
|
|
var json = evt.ToJson();
|
|
|
|
// Assert
|
|
Assert.Contains("\"tenantId\"", json);
|
|
Assert.Contains("\"eventType\"", json);
|
|
Assert.Contains("pack.step.completed", json);
|
|
Assert.Contains(TestStepId, json);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void FromJson_DeserializesEvent()
|
|
{
|
|
// Arrange
|
|
var original = PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.StepCompleted,
|
|
source: "taskrunner-worker",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: TestRunId,
|
|
planHash: TestPlanHash,
|
|
stepId: TestStepId);
|
|
var json = original.ToJson();
|
|
|
|
// Act
|
|
var deserialized = PackRunTimelineEvent.FromJson(json);
|
|
|
|
// Assert
|
|
Assert.NotNull(deserialized);
|
|
Assert.Equal(original.EventId, deserialized.EventId);
|
|
Assert.Equal(original.TenantId, deserialized.TenantId);
|
|
Assert.Equal(original.EventType, deserialized.EventType);
|
|
Assert.Equal(original.RunId, deserialized.RunId);
|
|
Assert.Equal(original.StepId, deserialized.StepId);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void GenerateIdempotencyKey_ReturnsConsistentKey()
|
|
{
|
|
// Arrange
|
|
var evt = PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.PackStarted,
|
|
source: "taskrunner-worker",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: TestRunId,
|
|
planHash: TestPlanHash);
|
|
|
|
// Act
|
|
var key1 = evt.GenerateIdempotencyKey();
|
|
var key2 = evt.GenerateIdempotencyKey();
|
|
|
|
// Assert
|
|
Assert.Equal(key1, key2);
|
|
Assert.Contains(TestTenantId, key1);
|
|
Assert.Contains(PackRunEventTypes.PackStarted, key1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Event Types Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void PackRunEventTypes_HasExpectedValues()
|
|
{
|
|
Assert.Equal("pack.started", PackRunEventTypes.PackStarted);
|
|
Assert.Equal("pack.completed", PackRunEventTypes.PackCompleted);
|
|
Assert.Equal("pack.failed", PackRunEventTypes.PackFailed);
|
|
Assert.Equal("pack.step.started", PackRunEventTypes.StepStarted);
|
|
Assert.Equal("pack.step.completed", PackRunEventTypes.StepCompleted);
|
|
Assert.Equal("pack.step.failed", PackRunEventTypes.StepFailed);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Theory]
|
|
[InlineData("pack.started", true)]
|
|
[InlineData("pack.step.completed", true)]
|
|
[InlineData("scan.completed", false)]
|
|
[InlineData("job.started", false)]
|
|
public void IsPackRunEvent_ReturnsCorrectly(string eventType, bool expected)
|
|
{
|
|
Assert.Equal(expected, PackRunEventTypes.IsPackRunEvent(eventType));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Evidence Pointer Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void EvidencePointer_Bundle_CreatesCorrectType()
|
|
{
|
|
var bundleId = Guid.NewGuid();
|
|
var pointer = PackRunEvidencePointer.Bundle(bundleId, "sha256:abc");
|
|
|
|
Assert.Equal(PackRunEvidencePointerType.Bundle, pointer.Type);
|
|
Assert.Equal(bundleId, pointer.BundleId);
|
|
Assert.Equal("sha256:abc", pointer.BundleDigest);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void EvidencePointer_Attestation_CreatesCorrectType()
|
|
{
|
|
var pointer = PackRunEvidencePointer.Attestation("subject:uri", "sha256:abc");
|
|
|
|
Assert.Equal(PackRunEvidencePointerType.Attestation, pointer.Type);
|
|
Assert.Equal("subject:uri", pointer.AttestationSubject);
|
|
Assert.Equal("sha256:abc", pointer.AttestationDigest);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void EvidencePointer_Manifest_CreatesCorrectType()
|
|
{
|
|
var pointer = PackRunEvidencePointer.Manifest("https://example.com/manifest", "/locker/path");
|
|
|
|
Assert.Equal(PackRunEvidencePointerType.Manifest, pointer.Type);
|
|
Assert.Equal("https://example.com/manifest", pointer.ManifestUri);
|
|
Assert.Equal("/locker/path", pointer.LockerPath);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region In-Memory Sink Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task InMemorySink_WriteAsync_StoresEvent()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
var evt = PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.PackStarted,
|
|
source: "taskrunner-worker",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: TestRunId,
|
|
planHash: TestPlanHash);
|
|
|
|
// Act
|
|
var result = await sink.WriteAsync(evt, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.NotNull(result.Sequence);
|
|
Assert.False(result.Deduplicated);
|
|
Assert.Single(sink);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task InMemorySink_WriteAsync_Deduplicates()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
var evt = PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.PackStarted,
|
|
source: "taskrunner-worker",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: TestRunId,
|
|
planHash: TestPlanHash);
|
|
var ct = CancellationToken.None;
|
|
|
|
// Act
|
|
await sink.WriteAsync(evt, ct);
|
|
var result = await sink.WriteAsync(evt, ct);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.True(result.Deduplicated);
|
|
Assert.Single(sink);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task InMemorySink_AssignsMonotonicSequence()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
var ct = CancellationToken.None;
|
|
|
|
// Act
|
|
var evt1 = PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.PackStarted,
|
|
source: "test",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: "run-1",
|
|
planHash: TestPlanHash);
|
|
|
|
var evt2 = PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.StepStarted,
|
|
source: "test",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: "run-1",
|
|
planHash: TestPlanHash);
|
|
|
|
var result1 = await sink.WriteAsync(evt1, ct);
|
|
var result2 = await sink.WriteAsync(evt2, ct);
|
|
|
|
// Assert
|
|
Assert.Equal(1, result1.Sequence);
|
|
Assert.Equal(2, result2.Sequence);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task InMemorySink_WriteBatchAsync_StoresMultiple()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
var events = Enumerable.Range(0, 3).Select(i =>
|
|
PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.StepStarted,
|
|
source: "test",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: TestRunId,
|
|
planHash: TestPlanHash,
|
|
stepId: $"step-{i}")).ToList();
|
|
|
|
// Act
|
|
var result = await sink.WriteBatchAsync(events, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.Equal(3, result.Written);
|
|
Assert.Equal(0, result.Deduplicated);
|
|
Assert.Equal(3, sink.Count);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task InMemorySink_GetEventsForRun_FiltersCorrectly()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
var ct = CancellationToken.None;
|
|
|
|
await sink.WriteAsync(PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.PackStarted,
|
|
source: "test",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: "run-1",
|
|
planHash: TestPlanHash), ct);
|
|
|
|
await sink.WriteAsync(PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.PackStarted,
|
|
source: "test",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: "run-2",
|
|
planHash: TestPlanHash), ct);
|
|
|
|
// Act
|
|
var run1Events = sink.GetEventsForRun("run-1");
|
|
var run2Events = sink.GetEventsForRun("run-2");
|
|
|
|
// Assert
|
|
Assert.Single(run1Events);
|
|
Assert.Single(run2Events);
|
|
Assert.Equal("run-1", run1Events[0].RunId);
|
|
Assert.Equal("run-2", run2Events[0].RunId);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task InMemorySink_Clear_RemovesAll()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
await sink.WriteAsync(PackRunTimelineEvent.Create(
|
|
tenantId: TestTenantId,
|
|
eventType: PackRunEventTypes.PackStarted,
|
|
source: "test",
|
|
occurredAt: DateTimeOffset.UtcNow,
|
|
runId: TestRunId,
|
|
planHash: TestPlanHash), CancellationToken.None);
|
|
|
|
// Act
|
|
sink.Clear();
|
|
|
|
// Assert
|
|
Assert.Empty(sink);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Emitter Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Emitter_EmitPackStartedAsync_CreatesEvent()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
|
var emitter = new PackRunTimelineEventEmitter(
|
|
sink,
|
|
timeProvider,
|
|
NullLogger<PackRunTimelineEventEmitter>.Instance);
|
|
|
|
// Act
|
|
var result = await emitter.EmitPackStartedAsync(
|
|
TestTenantId,
|
|
TestRunId,
|
|
TestPlanHash,
|
|
projectId: TestProjectId,
|
|
cancellationToken: CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.False(result.Deduplicated);
|
|
Assert.Equal(PackRunEventTypes.PackStarted, result.Event.EventType);
|
|
Assert.Equal(TestRunId, result.Event.RunId);
|
|
Assert.Single(sink);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Emitter_EmitPackCompletedAsync_CreatesEvent()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
|
var emitter = new PackRunTimelineEventEmitter(
|
|
sink,
|
|
timeProvider,
|
|
NullLogger<PackRunTimelineEventEmitter>.Instance);
|
|
|
|
// Act
|
|
var result = await emitter.EmitPackCompletedAsync(
|
|
TestTenantId,
|
|
TestRunId,
|
|
TestPlanHash,
|
|
cancellationToken: CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.Equal(PackRunEventTypes.PackCompleted, result.Event.EventType);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Emitter_EmitPackFailedAsync_CreatesEventWithError()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
|
var emitter = new PackRunTimelineEventEmitter(
|
|
sink,
|
|
timeProvider,
|
|
NullLogger<PackRunTimelineEventEmitter>.Instance);
|
|
|
|
// Act
|
|
var result = await emitter.EmitPackFailedAsync(
|
|
TestTenantId,
|
|
TestRunId,
|
|
TestPlanHash,
|
|
failureReason: "Step step-001 failed",
|
|
cancellationToken: CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.Equal(PackRunEventTypes.PackFailed, result.Event.EventType);
|
|
Assert.Equal(PackRunEventSeverity.Error, result.Event.Severity);
|
|
Assert.Contains("failureReason", result.Event.Attributes!.Keys);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Emitter_EmitStepStartedAsync_IncludesAttempt()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
|
var emitter = new PackRunTimelineEventEmitter(
|
|
sink,
|
|
timeProvider,
|
|
NullLogger<PackRunTimelineEventEmitter>.Instance);
|
|
|
|
// Act
|
|
var result = await emitter.EmitStepStartedAsync(
|
|
TestTenantId,
|
|
TestRunId,
|
|
TestPlanHash,
|
|
TestStepId,
|
|
attempt: 2,
|
|
cancellationToken: CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.Equal(PackRunEventTypes.StepStarted, result.Event.EventType);
|
|
Assert.Equal(TestStepId, result.Event.StepId);
|
|
Assert.Equal("2", result.Event.Attributes!["attempt"]);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Emitter_EmitStepCompletedAsync_IncludesDuration()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
|
var emitter = new PackRunTimelineEventEmitter(
|
|
sink,
|
|
timeProvider,
|
|
NullLogger<PackRunTimelineEventEmitter>.Instance);
|
|
|
|
// Act
|
|
var result = await emitter.EmitStepCompletedAsync(
|
|
TestTenantId,
|
|
TestRunId,
|
|
TestPlanHash,
|
|
TestStepId,
|
|
attempt: 1,
|
|
durationMs: 123.45,
|
|
cancellationToken: CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.Equal(PackRunEventTypes.StepCompleted, result.Event.EventType);
|
|
Assert.Contains("durationMs", result.Event.Attributes!.Keys);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Emitter_EmitStepFailedAsync_IncludesError()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
|
var emitter = new PackRunTimelineEventEmitter(
|
|
sink,
|
|
timeProvider,
|
|
NullLogger<PackRunTimelineEventEmitter>.Instance);
|
|
|
|
// Act
|
|
var result = await emitter.EmitStepFailedAsync(
|
|
TestTenantId,
|
|
TestRunId,
|
|
TestPlanHash,
|
|
TestStepId,
|
|
attempt: 3,
|
|
error: "Connection timeout",
|
|
cancellationToken: CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.Equal(PackRunEventTypes.StepFailed, result.Event.EventType);
|
|
Assert.Equal(PackRunEventSeverity.Error, result.Event.Severity);
|
|
Assert.Equal("Connection timeout", result.Event.Attributes!["error"]);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Emitter_EmitBatchAsync_OrdersEventsDeterministically()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
|
var emitter = new PackRunTimelineEventEmitter(
|
|
sink,
|
|
timeProvider,
|
|
NullLogger<PackRunTimelineEventEmitter>.Instance);
|
|
|
|
var now = DateTimeOffset.UtcNow;
|
|
var events = new[]
|
|
{
|
|
PackRunTimelineEvent.Create(TestTenantId, PackRunEventTypes.StepStarted, "test", now.AddSeconds(2), TestRunId, TestPlanHash),
|
|
PackRunTimelineEvent.Create(TestTenantId, PackRunEventTypes.PackStarted, "test", now, TestRunId, TestPlanHash),
|
|
PackRunTimelineEvent.Create(TestTenantId, PackRunEventTypes.StepCompleted, "test", now.AddSeconds(1), TestRunId, TestPlanHash),
|
|
};
|
|
|
|
// Act
|
|
var result = await emitter.EmitBatchAsync(events, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.Equal(3, result.Emitted);
|
|
Assert.Equal(0, result.Deduplicated);
|
|
|
|
var stored = sink.GetEvents();
|
|
Assert.Equal(PackRunEventTypes.PackStarted, stored[0].EventType);
|
|
Assert.Equal(PackRunEventTypes.StepCompleted, stored[1].EventType);
|
|
Assert.Equal(PackRunEventTypes.StepStarted, stored[2].EventType);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Emitter_EmitBatchAsync_HandlesDuplicates()
|
|
{
|
|
// Arrange
|
|
var sink = new InMemoryPackRunTimelineEventSink();
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
|
var emitter = new PackRunTimelineEventEmitter(
|
|
sink,
|
|
timeProvider,
|
|
NullLogger<PackRunTimelineEventEmitter>.Instance);
|
|
var ct = CancellationToken.None;
|
|
|
|
var evt = PackRunTimelineEvent.Create(
|
|
TestTenantId,
|
|
PackRunEventTypes.PackStarted,
|
|
"test",
|
|
DateTimeOffset.UtcNow,
|
|
TestRunId,
|
|
TestPlanHash);
|
|
|
|
// Emit once directly
|
|
await sink.WriteAsync(evt, ct);
|
|
|
|
// Act - emit batch with same event
|
|
var result = await emitter.EmitBatchAsync([evt], ct);
|
|
|
|
// Assert
|
|
Assert.Equal(0, result.Emitted);
|
|
Assert.Equal(1, result.Deduplicated);
|
|
Assert.Single(sink); // Only one event stored
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Null Sink Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task NullSink_WriteAsync_ReturnsSuccess()
|
|
{
|
|
// Arrange
|
|
var sink = NullPackRunTimelineEventSink.Instance;
|
|
var evt = PackRunTimelineEvent.Create(
|
|
TestTenantId,
|
|
PackRunEventTypes.PackStarted,
|
|
"test",
|
|
DateTimeOffset.UtcNow,
|
|
TestRunId,
|
|
TestPlanHash);
|
|
|
|
// Act
|
|
var result = await sink.WriteAsync(evt, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.False(result.Deduplicated);
|
|
Assert.Null(result.Sequence);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fake time provider for testing.
|
|
/// </summary>
|
|
internal sealed class FakeTimeProvider : TimeProvider
|
|
{
|
|
private DateTimeOffset _utcNow;
|
|
|
|
public FakeTimeProvider(DateTimeOffset utcNow)
|
|
{
|
|
_utcNow = utcNow;
|
|
}
|
|
|
|
public override DateTimeOffset GetUtcNow() => _utcNow;
|
|
|
|
public void Advance(TimeSpan duration) => _utcNow = _utcNow.Add(duration);
|
|
}
|