audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories

This commit is contained in:
master
2026-01-07 18:49:59 +02:00
parent 04ec098046
commit 608a7f85c0
866 changed files with 56323 additions and 6231 deletions

View File

@@ -0,0 +1,22 @@
# Eventing Tests Charter
## Mission
- Verify eventing envelope contracts and deterministic serialization.
## Responsibilities
- Cover schema validation, parsing, and round-trip serialization.
- Exercise edge cases and determinism behavior.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- docs/modules/eventing/event-envelope-schema.md
## Working Agreement
- Use fixed times and IDs for deterministic tests.
- Avoid network dependencies in tests.
## Testing Strategy
- Unit tests for schema and serialization.
- Regression tests for envelope compatibility.

View File

@@ -0,0 +1,154 @@
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
using FluentAssertions;
using StellaOps.Eventing.Internal;
using StellaOps.HybridLogicalClock;
using Xunit;
namespace StellaOps.Eventing.Tests;
[Trait("Category", "Unit")]
public sealed class EventIdGeneratorTests
{
[Fact]
public void Generate_SameInputs_ProducesSameId()
{
// Arrange
var correlationId = "scan-abc123";
var tHlc = new HlcTimestamp(1704585600000, 0, "node1");
var service = "Scheduler";
var kind = "ENQUEUE";
// Act
var id1 = EventIdGenerator.Generate(correlationId, tHlc, service, kind);
var id2 = EventIdGenerator.Generate(correlationId, tHlc, service, kind);
// Assert
id1.Should().Be(id2);
}
[Fact]
public void Generate_DifferentCorrelationId_ProducesDifferentId()
{
// Arrange
var tHlc = new HlcTimestamp(1704585600000, 0, "node1");
var service = "Scheduler";
var kind = "ENQUEUE";
// Act
var id1 = EventIdGenerator.Generate("scan-abc123", tHlc, service, kind);
var id2 = EventIdGenerator.Generate("scan-xyz789", tHlc, service, kind);
// Assert
id1.Should().NotBe(id2);
}
[Fact]
public void Generate_DifferentHlc_ProducesDifferentId()
{
// Arrange
var correlationId = "scan-abc123";
var tHlc1 = new HlcTimestamp(1704585600000, 0, "node1");
var tHlc2 = new HlcTimestamp(1704585600000, 1, "node1");
var service = "Scheduler";
var kind = "ENQUEUE";
// Act
var id1 = EventIdGenerator.Generate(correlationId, tHlc1, service, kind);
var id2 = EventIdGenerator.Generate(correlationId, tHlc2, service, kind);
// Assert
id1.Should().NotBe(id2);
}
[Fact]
public void Generate_DifferentService_ProducesDifferentId()
{
// Arrange
var correlationId = "scan-abc123";
var tHlc = new HlcTimestamp(1704585600000, 0, "node1");
var kind = "ENQUEUE";
// Act
var id1 = EventIdGenerator.Generate(correlationId, tHlc, "Scheduler", kind);
var id2 = EventIdGenerator.Generate(correlationId, tHlc, "AirGap", kind);
// Assert
id1.Should().NotBe(id2);
}
[Fact]
public void Generate_DifferentKind_ProducesDifferentId()
{
// Arrange
var correlationId = "scan-abc123";
var tHlc = new HlcTimestamp(1704585600000, 0, "node1");
var service = "Scheduler";
// Act
var id1 = EventIdGenerator.Generate(correlationId, tHlc, service, "ENQUEUE");
var id2 = EventIdGenerator.Generate(correlationId, tHlc, service, "EXECUTE");
// Assert
id1.Should().NotBe(id2);
}
[Fact]
public void Generate_ReturnsLowercaseHex32Chars()
{
// Arrange
var correlationId = "scan-abc123";
var tHlc = new HlcTimestamp(1704585600000, 0, "node1");
var service = "Scheduler";
var kind = "ENQUEUE";
// Act
var id = EventIdGenerator.Generate(correlationId, tHlc, service, kind);
// Assert
id.Should().HaveLength(32);
id.Should().MatchRegex("^[a-f0-9]{32}$");
}
[Fact]
public void ComputePayloadDigest_SamePayload_ProducesSameDigest()
{
// Arrange
var payload = """{"key":"value"}""";
// Act
var digest1 = EventIdGenerator.ComputePayloadDigest(payload);
var digest2 = EventIdGenerator.ComputePayloadDigest(payload);
// Assert
digest1.Should().BeEquivalentTo(digest2);
}
[Fact]
public void ComputePayloadDigest_DifferentPayload_ProducesDifferentDigest()
{
// Arrange
var payload1 = """{"key":"value1"}""";
var payload2 = """{"key":"value2"}""";
// Act
var digest1 = EventIdGenerator.ComputePayloadDigest(payload1);
var digest2 = EventIdGenerator.ComputePayloadDigest(payload2);
// Assert
digest1.Should().NotBeEquivalentTo(digest2);
}
[Fact]
public void ComputePayloadDigest_Returns32Bytes()
{
// Arrange
var payload = """{"key":"value"}""";
// Act
var digest = EventIdGenerator.ComputePayloadDigest(payload);
// Assert
digest.Should().HaveCount(32); // SHA-256 = 256 bits = 32 bytes
}
}

View File

@@ -0,0 +1,212 @@
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
using FluentAssertions;
using StellaOps.Eventing.Models;
using StellaOps.Eventing.Storage;
using StellaOps.HybridLogicalClock;
using Xunit;
namespace StellaOps.Eventing.Tests;
[Trait("Category", "Unit")]
public sealed class InMemoryTimelineEventStoreTests
{
private readonly InMemoryTimelineEventStore _store;
public InMemoryTimelineEventStoreTests()
{
_store = new InMemoryTimelineEventStore();
}
private static TimelineEvent CreateEvent(
string correlationId,
string kind,
HlcTimestamp hlc,
string service = "TestService")
{
return new TimelineEvent
{
EventId = $"{correlationId}-{kind}-{hlc.LogicalCounter}",
CorrelationId = correlationId,
Kind = kind,
THlc = hlc,
TsWall = DateTimeOffset.UtcNow,
Service = service,
Payload = "{}",
PayloadDigest = new byte[32],
EngineVersion = new EngineVersionRef("Test", "1.0.0", "test"),
SchemaVersion = 1
};
}
[Fact]
public async Task AppendAsync_StoresEvent()
{
// Arrange
var e = CreateEvent("corr-1", "ENQUEUE", new HlcTimestamp(1000, 0, "n1"));
// Act
await _store.AppendAsync(e);
// Assert
var retrieved = await _store.GetByIdAsync(e.EventId);
retrieved.Should().NotBeNull();
retrieved!.EventId.Should().Be(e.EventId);
}
[Fact]
public async Task AppendAsync_Idempotent_DoesNotDuplicate()
{
// Arrange
var e = CreateEvent("corr-1", "ENQUEUE", new HlcTimestamp(1000, 0, "n1"));
// Act
await _store.AppendAsync(e);
await _store.AppendAsync(e); // Duplicate
// Assert
var count = await _store.CountByCorrelationIdAsync("corr-1");
count.Should().Be(1);
}
[Fact]
public async Task GetByCorrelationIdAsync_ReturnsOrderedByHlc()
{
// Arrange
var hlc1 = new HlcTimestamp(1000, 0, "n1");
var hlc2 = new HlcTimestamp(1000, 1, "n1");
var hlc3 = new HlcTimestamp(2000, 0, "n1");
// Insert out of order
await _store.AppendAsync(CreateEvent("corr-1", "C", hlc3));
await _store.AppendAsync(CreateEvent("corr-1", "A", hlc1));
await _store.AppendAsync(CreateEvent("corr-1", "B", hlc2));
// Act
var events = await _store.GetByCorrelationIdAsync("corr-1");
// Assert
events.Should().HaveCount(3);
events[0].Kind.Should().Be("A");
events[1].Kind.Should().Be("B");
events[2].Kind.Should().Be("C");
}
[Fact]
public async Task GetByCorrelationIdAsync_Pagination_Works()
{
// Arrange
for (int i = 0; i < 10; i++)
{
await _store.AppendAsync(CreateEvent("corr-1", $"E{i}", new HlcTimestamp(1000 + i, 0, "n1")));
}
// Act
var page1 = await _store.GetByCorrelationIdAsync("corr-1", limit: 3, offset: 0);
var page2 = await _store.GetByCorrelationIdAsync("corr-1", limit: 3, offset: 3);
// Assert
page1.Should().HaveCount(3);
page2.Should().HaveCount(3);
page1[0].Kind.Should().Be("E0");
page2[0].Kind.Should().Be("E3");
}
[Fact]
public async Task GetByHlcRangeAsync_FiltersCorrectly()
{
// Arrange
await _store.AppendAsync(CreateEvent("corr-1", "A", new HlcTimestamp(1000, 0, "n1")));
await _store.AppendAsync(CreateEvent("corr-1", "B", new HlcTimestamp(2000, 0, "n1")));
await _store.AppendAsync(CreateEvent("corr-1", "C", new HlcTimestamp(3000, 0, "n1")));
await _store.AppendAsync(CreateEvent("corr-1", "D", new HlcTimestamp(4000, 0, "n1")));
// Act
var events = await _store.GetByHlcRangeAsync(
"corr-1",
new HlcTimestamp(2000, 0, "n1"),
new HlcTimestamp(3000, 0, "n1"));
// Assert
events.Should().HaveCount(2);
events[0].Kind.Should().Be("B");
events[1].Kind.Should().Be("C");
}
[Fact]
public async Task GetByServiceAsync_FiltersCorrectly()
{
// Arrange
await _store.AppendAsync(CreateEvent("corr-1", "A", new HlcTimestamp(1000, 0, "n1"), "Scheduler"));
await _store.AppendAsync(CreateEvent("corr-2", "B", new HlcTimestamp(2000, 0, "n1"), "AirGap"));
await _store.AppendAsync(CreateEvent("corr-3", "C", new HlcTimestamp(3000, 0, "n1"), "Scheduler"));
// Act
var events = await _store.GetByServiceAsync("Scheduler");
// Assert
events.Should().HaveCount(2);
events.All(e => e.Service == "Scheduler").Should().BeTrue();
}
[Fact]
public async Task GetByIdAsync_NotFound_ReturnsNull()
{
// Act
var result = await _store.GetByIdAsync("nonexistent");
// Assert
result.Should().BeNull();
}
[Fact]
public async Task CountByCorrelationIdAsync_ReturnsCorrectCount()
{
// Arrange
await _store.AppendAsync(CreateEvent("corr-1", "A", new HlcTimestamp(1000, 0, "n1")));
await _store.AppendAsync(CreateEvent("corr-1", "B", new HlcTimestamp(2000, 0, "n1")));
await _store.AppendAsync(CreateEvent("corr-2", "C", new HlcTimestamp(3000, 0, "n1")));
// Act
var count1 = await _store.CountByCorrelationIdAsync("corr-1");
var count2 = await _store.CountByCorrelationIdAsync("corr-2");
var count3 = await _store.CountByCorrelationIdAsync("corr-3");
// Assert
count1.Should().Be(2);
count2.Should().Be(1);
count3.Should().Be(0);
}
[Fact]
public async Task AppendBatchAsync_StoresAllEvents()
{
// Arrange
var events = new[]
{
CreateEvent("corr-1", "A", new HlcTimestamp(1000, 0, "n1")),
CreateEvent("corr-1", "B", new HlcTimestamp(2000, 0, "n1")),
CreateEvent("corr-1", "C", new HlcTimestamp(3000, 0, "n1"))
};
// Act
await _store.AppendBatchAsync(events);
// Assert
var count = await _store.CountByCorrelationIdAsync("corr-1");
count.Should().Be(3);
}
[Fact]
public void Clear_RemovesAllEvents()
{
// Arrange
_store.AppendAsync(CreateEvent("corr-1", "A", new HlcTimestamp(1000, 0, "n1"))).Wait();
// Act
_store.Clear();
// Assert
_store.GetAll().Should().BeEmpty();
}
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<RootNamespace>StellaOps.Eventing.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Eventing\StellaOps.Eventing.csproj" />
<ProjectReference Include="..\..\StellaOps.HybridLogicalClock\StellaOps.HybridLogicalClock.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Moq" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,183 @@
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using Moq;
using StellaOps.Eventing.Models;
using StellaOps.Eventing.Storage;
using StellaOps.HybridLogicalClock;
using Xunit;
namespace StellaOps.Eventing.Tests;
[Trait("Category", "Unit")]
public sealed class TimelineEventEmitterTests
{
private readonly FakeTimeProvider _timeProvider;
private readonly Mock<IHybridLogicalClock> _hlcMock;
private readonly InMemoryTimelineEventStore _eventStore;
private readonly IOptions<EventingOptions> _options;
private readonly TimelineEventEmitter _emitter;
public TimelineEventEmitterTests()
{
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 7, 12, 0, 0, TimeSpan.Zero));
_hlcMock = new Mock<IHybridLogicalClock>();
_eventStore = new InMemoryTimelineEventStore();
_options = Options.Create(new EventingOptions
{
ServiceName = "TestService",
EngineVersion = new EngineVersionRef("TestEngine", "1.0.0", "sha256:test")
});
_emitter = new TimelineEventEmitter(
_hlcMock.Object,
_timeProvider,
_eventStore,
_options,
NullLogger<TimelineEventEmitter>.Instance);
}
[Fact]
public async Task EmitAsync_StoresEventWithCorrectFields()
{
// Arrange
var correlationId = "scan-abc123";
var kind = EventKinds.Enqueue;
var payload = new { JobId = "job-1", Status = "pending" };
var expectedHlc = new HlcTimestamp(1704585600000, 0, "node1");
_hlcMock.Setup(h => h.Tick()).Returns(expectedHlc);
// Act
var result = await _emitter.EmitAsync(correlationId, kind, payload);
// Assert
result.Should().NotBeNull();
result.CorrelationId.Should().Be(correlationId);
result.Kind.Should().Be(kind);
result.Service.Should().Be("TestService");
result.THlc.Should().Be(expectedHlc);
result.TsWall.Should().Be(_timeProvider.GetUtcNow());
result.SchemaVersion.Should().Be(1);
result.EngineVersion.EngineName.Should().Be("TestEngine");
result.EngineVersion.Version.Should().Be("1.0.0");
result.PayloadDigest.Should().NotBeEmpty();
result.EventId.Should().HaveLength(32);
}
[Fact]
public async Task EmitAsync_GeneratesDeterministicEventId()
{
// Arrange
var correlationId = "scan-abc123";
var kind = EventKinds.Execute;
var payload = new { Step = 1 };
var hlc = new HlcTimestamp(1704585600000, 0, "node1");
_hlcMock.Setup(h => h.Tick()).Returns(hlc);
// Act
var result1 = await _emitter.EmitAsync(correlationId, kind, payload);
// Create a second emitter with same config
var emitter2 = new TimelineEventEmitter(
_hlcMock.Object,
_timeProvider,
new InMemoryTimelineEventStore(),
_options,
NullLogger<TimelineEventEmitter>.Instance);
var result2 = await emitter2.EmitAsync(correlationId, kind, payload);
// Assert - Same inputs should produce same EventId
result1.EventId.Should().Be(result2.EventId);
}
[Fact]
public async Task EmitAsync_StoresEventInStore()
{
// Arrange
var correlationId = "scan-abc123";
var hlc = new HlcTimestamp(1704585600000, 0, "node1");
_hlcMock.Setup(h => h.Tick()).Returns(hlc);
// Act
var emitted = await _emitter.EmitAsync(correlationId, EventKinds.Enqueue, new { Test = true });
// Assert
var stored = await _eventStore.GetByIdAsync(emitted.EventId);
stored.Should().NotBeNull();
stored!.EventId.Should().Be(emitted.EventId);
}
[Fact]
public async Task EmitBatchAsync_StoresAllEvents()
{
// Arrange
var hlcCounter = 0L;
_hlcMock.Setup(h => h.Tick())
.Returns(() => new HlcTimestamp(1704585600000, hlcCounter++, "node1"));
var pendingEvents = new[]
{
new PendingEvent("scan-1", EventKinds.Enqueue, new { Step = 1 }),
new PendingEvent("scan-1", EventKinds.Execute, new { Step = 2 }),
new PendingEvent("scan-1", EventKinds.Complete, new { Step = 3 })
};
// Act
var results = await _emitter.EmitBatchAsync(pendingEvents);
// Assert
results.Should().HaveCount(3);
results.Select(r => r.Kind).Should().BeEquivalentTo(
new[] { EventKinds.Enqueue, EventKinds.Execute, EventKinds.Complete });
var stored = _eventStore.GetAll();
stored.Should().HaveCount(3);
}
[Fact]
public async Task EmitBatchAsync_EmptyBatch_ReturnsEmptyList()
{
// Act
var results = await _emitter.EmitBatchAsync(Array.Empty<PendingEvent>());
// Assert
results.Should().BeEmpty();
}
[Fact]
public async Task EmitAsync_IncludesPayloadDigest()
{
// Arrange
var hlc = new HlcTimestamp(1704585600000, 0, "node1");
_hlcMock.Setup(h => h.Tick()).Returns(hlc);
// Act
var result = await _emitter.EmitAsync("corr-1", EventKinds.Emit, new { Data = "test" });
// Assert
result.PayloadDigest.Should().NotBeNull();
result.PayloadDigest.Should().HaveCount(32); // SHA-256
}
[Fact]
public async Task EmitAsync_DifferentPayloads_DifferentDigests()
{
// Arrange
var hlcCounter = 0L;
_hlcMock.Setup(h => h.Tick())
.Returns(() => new HlcTimestamp(1704585600000, hlcCounter++, "node1"));
// Act
var result1 = await _emitter.EmitAsync("corr-1", EventKinds.Emit, new { Value = 1 });
var result2 = await _emitter.EmitAsync("corr-1", EventKinds.Emit, new { Value = 2 });
// Assert
result1.PayloadDigest.Should().NotBeEquivalentTo(result2.PayloadDigest);
}
}