more audit work
This commit is contained in:
@@ -5,6 +5,9 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0016-M | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0016-T | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0016-A | DONE | Waived (test project). |
|
||||
| AUDIT-0076-M | DONE | Revalidated 2026-01-06. |
|
||||
| AUDIT-0076-T | DONE | Revalidated 2026-01-06. |
|
||||
| AUDIT-0076-A | DONE | Waived (test project; revalidated 2026-01-06). |
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
@@ -190,11 +191,23 @@ public class DpopProofValidatorTests
|
||||
var securityKey = new ECDsaSecurityKey(ecdsa) { KeyId = Guid.NewGuid().ToString("N") };
|
||||
var jwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(securityKey);
|
||||
|
||||
var jwkHeader = new Dictionary<string, object>
|
||||
{
|
||||
["kty"] = jwk.Kty,
|
||||
["crv"] = jwk.Crv,
|
||||
["x"] = jwk.X,
|
||||
["y"] = jwk.Y
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(jwk.Kid))
|
||||
{
|
||||
jwkHeader["kid"] = jwk.Kid;
|
||||
}
|
||||
|
||||
var header = new JwtHeader(new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256))
|
||||
{
|
||||
{ "typ", "dpop+jwt" },
|
||||
{ "jwk", jwk }
|
||||
{ "jwk", jwkHeader }
|
||||
};
|
||||
header["typ"] = "dpop+jwt";
|
||||
headerMutator?.Invoke(header);
|
||||
|
||||
var payload = new JwtPayload
|
||||
@@ -217,6 +230,7 @@ public class DpopProofValidatorTests
|
||||
return (handler.WriteToken(token), jwk);
|
||||
}
|
||||
|
||||
|
||||
private static string BuildUnsignedToken(object header, object payload)
|
||||
{
|
||||
var headerJson = JsonSerializer.Serialize(header);
|
||||
|
||||
@@ -5,6 +5,9 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0785-M | TODO | Maintainability audit for StellaOps.Auth.Security.Tests (pending revalidation). |
|
||||
| AUDIT-0785-T | TODO | Test coverage audit for StellaOps.Auth.Security.Tests (pending revalidation). |
|
||||
| AUDIT-0785-A | DONE | Waived (test project). |
|
||||
| AUDIT-0017-M | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0017-T | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0017-A | DONE | Waived (test project). |
|
||||
| AUDIT-0785-M | DONE | Revalidated 2026-01-07 (test project). |
|
||||
| AUDIT-0785-T | DONE | Revalidated 2026-01-07. |
|
||||
| AUDIT-0785-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
|
||||
@@ -5,6 +5,9 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0018-M | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0018-T | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0018-A | DONE | Waived (test project). |
|
||||
| AUDIT-0133-M | DONE | Maintainability audit for StellaOps.Canonicalization.Tests; revalidated 2026-01-06. |
|
||||
| AUDIT-0133-T | DONE | Test coverage audit for StellaOps.Canonicalization.Tests; revalidated 2026-01-06. |
|
||||
| AUDIT-0133-A | DONE | Waived (test project; revalidated 2026-01-06). |
|
||||
|
||||
@@ -5,6 +5,9 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0019-M | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0019-T | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0019-A | DONE | Waived (test project). |
|
||||
| AUDIT-0245-M | DONE | Revalidated 2026-01-07. |
|
||||
| AUDIT-0245-T | DONE | Revalidated 2026-01-07. |
|
||||
| AUDIT-0245-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
|
||||
@@ -5,6 +5,9 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0020-M | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0020-T | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0020-A | DONE | Waived (test project). |
|
||||
| AUDIT-0250-M | DONE | Revalidated 2026-01-07. |
|
||||
| AUDIT-0250-T | DONE | Revalidated 2026-01-07. |
|
||||
| AUDIT-0250-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
|
||||
@@ -5,6 +5,9 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0021-M | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0021-T | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0021-A | DONE | Waived (test project). |
|
||||
| AUDIT-0256-M | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
|
||||
| AUDIT-0256-T | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
|
||||
| AUDIT-0256-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
|
||||
@@ -175,7 +175,6 @@ public sealed class BouncyCastleErrorClassificationTests
|
||||
#region Invalid Key Material Errors
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(16)]
|
||||
[InlineData(31)]
|
||||
[InlineData(33)]
|
||||
@@ -203,6 +202,25 @@ public sealed class BouncyCastleErrorClassificationTests
|
||||
_output.WriteLine(" Error code mapping: CRYPTO_INVALID_KEY_MATERIAL");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpsertSigningKey_EmptyPrivateKey_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new BouncyCastleEd25519CryptoProvider();
|
||||
var keyReference = new CryptoKeyReference("test-key", provider.Name);
|
||||
|
||||
// Act
|
||||
Action act = () => _ = new CryptoSigningKey(
|
||||
keyReference,
|
||||
SignatureAlgorithms.Ed25519,
|
||||
Array.Empty<byte>(),
|
||||
createdAt: DateTimeOffset.UtcNow);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("*Private key material must be provided*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpsertSigningKey_InvalidPublicKeyLength_ThrowsInvalidOperationException()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,9 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0022-M | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0022-T | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0022-A | DONE | Waived (test project). |
|
||||
| AUDIT-0271-M | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
|
||||
| AUDIT-0271-T | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
|
||||
| AUDIT-0271-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
|
||||
@@ -46,7 +46,7 @@ public class DeltaVerdictTests
|
||||
var delta = engine.ComputeDelta(baseVerdict, headVerdict);
|
||||
|
||||
delta.AddedComponents.Should().Contain(c => c.Purl == "pkg:apk/zlib@2.0");
|
||||
delta.RemovedComponents.Should().Contain(c => c.Purl == "pkg:apk/openssl@1.0");
|
||||
delta.RemovedComponents.Should().NotContain(c => c.Purl == "pkg:apk/openssl@1.0");
|
||||
delta.ChangedComponents.Should().Contain(c => c.Purl == "pkg:apk/openssl@1.0");
|
||||
delta.AddedVulnerabilities.Should().Contain(v => v.VulnerabilityId == "CVE-2");
|
||||
delta.RemovedVulnerabilities.Should().Contain(v => v.VulnerabilityId == "CVE-1");
|
||||
|
||||
@@ -5,6 +5,9 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0023-M | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0023-T | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0023-A | DONE | Waived (test project). |
|
||||
| AUDIT-0274-M | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
|
||||
| AUDIT-0274-T | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
|
||||
| AUDIT-0274-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
|
||||
@@ -15,7 +15,12 @@ public sealed class EventIdGeneratorTests
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = "scan-abc123";
|
||||
var tHlc = new HlcTimestamp(1704585600000, 0, "node1");
|
||||
var tHlc = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 0,
|
||||
NodeId = "node1"
|
||||
};
|
||||
var service = "Scheduler";
|
||||
var kind = "ENQUEUE";
|
||||
|
||||
@@ -31,7 +36,12 @@ public sealed class EventIdGeneratorTests
|
||||
public void Generate_DifferentCorrelationId_ProducesDifferentId()
|
||||
{
|
||||
// Arrange
|
||||
var tHlc = new HlcTimestamp(1704585600000, 0, "node1");
|
||||
var tHlc = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 0,
|
||||
NodeId = "node1"
|
||||
};
|
||||
var service = "Scheduler";
|
||||
var kind = "ENQUEUE";
|
||||
|
||||
@@ -48,8 +58,18 @@ public sealed class EventIdGeneratorTests
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = "scan-abc123";
|
||||
var tHlc1 = new HlcTimestamp(1704585600000, 0, "node1");
|
||||
var tHlc2 = new HlcTimestamp(1704585600000, 1, "node1");
|
||||
var tHlc1 = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 0,
|
||||
NodeId = "node1"
|
||||
};
|
||||
var tHlc2 = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 1,
|
||||
NodeId = "node1"
|
||||
};
|
||||
var service = "Scheduler";
|
||||
var kind = "ENQUEUE";
|
||||
|
||||
@@ -66,7 +86,12 @@ public sealed class EventIdGeneratorTests
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = "scan-abc123";
|
||||
var tHlc = new HlcTimestamp(1704585600000, 0, "node1");
|
||||
var tHlc = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 0,
|
||||
NodeId = "node1"
|
||||
};
|
||||
var kind = "ENQUEUE";
|
||||
|
||||
// Act
|
||||
@@ -82,7 +107,12 @@ public sealed class EventIdGeneratorTests
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = "scan-abc123";
|
||||
var tHlc = new HlcTimestamp(1704585600000, 0, "node1");
|
||||
var tHlc = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 0,
|
||||
NodeId = "node1"
|
||||
};
|
||||
var service = "Scheduler";
|
||||
|
||||
// Act
|
||||
@@ -98,7 +128,12 @@ public sealed class EventIdGeneratorTests
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = "scan-abc123";
|
||||
var tHlc = new HlcTimestamp(1704585600000, 0, "node1");
|
||||
var tHlc = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 0,
|
||||
NodeId = "node1"
|
||||
};
|
||||
var service = "Scheduler";
|
||||
var kind = "ENQUEUE";
|
||||
|
||||
|
||||
@@ -39,17 +39,28 @@ public sealed class InMemoryTimelineEventStoreTests
|
||||
};
|
||||
}
|
||||
|
||||
private static HlcTimestamp CreateHlc(long physicalTime, int logicalCounter, string nodeId)
|
||||
{
|
||||
return new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = physicalTime,
|
||||
LogicalCounter = logicalCounter,
|
||||
NodeId = nodeId
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AppendAsync_StoresEvent()
|
||||
{
|
||||
// Arrange
|
||||
var e = CreateEvent("corr-1", "ENQUEUE", new HlcTimestamp(1000, 0, "n1"));
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var e = CreateEvent("corr-1", "ENQUEUE", CreateHlc(1000, 0, "n1"));
|
||||
|
||||
// Act
|
||||
await _store.AppendAsync(e);
|
||||
await _store.AppendAsync(e, ct);
|
||||
|
||||
// Assert
|
||||
var retrieved = await _store.GetByIdAsync(e.EventId);
|
||||
var retrieved = await _store.GetByIdAsync(e.EventId, ct);
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.EventId.Should().Be(e.EventId);
|
||||
}
|
||||
@@ -58,14 +69,15 @@ public sealed class InMemoryTimelineEventStoreTests
|
||||
public async Task AppendAsync_Idempotent_DoesNotDuplicate()
|
||||
{
|
||||
// Arrange
|
||||
var e = CreateEvent("corr-1", "ENQUEUE", new HlcTimestamp(1000, 0, "n1"));
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var e = CreateEvent("corr-1", "ENQUEUE", CreateHlc(1000, 0, "n1"));
|
||||
|
||||
// Act
|
||||
await _store.AppendAsync(e);
|
||||
await _store.AppendAsync(e); // Duplicate
|
||||
await _store.AppendAsync(e, ct);
|
||||
await _store.AppendAsync(e, ct); // Duplicate
|
||||
|
||||
// Assert
|
||||
var count = await _store.CountByCorrelationIdAsync("corr-1");
|
||||
var count = await _store.CountByCorrelationIdAsync("corr-1", ct);
|
||||
count.Should().Be(1);
|
||||
}
|
||||
|
||||
@@ -73,17 +85,18 @@ public sealed class InMemoryTimelineEventStoreTests
|
||||
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");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var hlc1 = CreateHlc(1000, 0, "n1");
|
||||
var hlc2 = CreateHlc(1000, 1, "n1");
|
||||
var hlc3 = CreateHlc(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));
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "C", hlc3), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", hlc1), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "B", hlc2), ct);
|
||||
|
||||
// Act
|
||||
var events = await _store.GetByCorrelationIdAsync("corr-1");
|
||||
var events = await _store.GetByCorrelationIdAsync("corr-1", cancellationToken: ct);
|
||||
|
||||
// Assert
|
||||
events.Should().HaveCount(3);
|
||||
@@ -96,14 +109,15 @@ public sealed class InMemoryTimelineEventStoreTests
|
||||
public async Task GetByCorrelationIdAsync_Pagination_Works()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
await _store.AppendAsync(CreateEvent("corr-1", $"E{i}", new HlcTimestamp(1000 + i, 0, "n1")));
|
||||
await _store.AppendAsync(CreateEvent("corr-1", $"E{i}", CreateHlc(1000 + i, 0, "n1")), ct);
|
||||
}
|
||||
|
||||
// Act
|
||||
var page1 = await _store.GetByCorrelationIdAsync("corr-1", limit: 3, offset: 0);
|
||||
var page2 = await _store.GetByCorrelationIdAsync("corr-1", limit: 3, offset: 3);
|
||||
var page1 = await _store.GetByCorrelationIdAsync("corr-1", limit: 3, offset: 0, cancellationToken: ct);
|
||||
var page2 = await _store.GetByCorrelationIdAsync("corr-1", limit: 3, offset: 3, cancellationToken: ct);
|
||||
|
||||
// Assert
|
||||
page1.Should().HaveCount(3);
|
||||
@@ -116,16 +130,18 @@ public sealed class InMemoryTimelineEventStoreTests
|
||||
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")));
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "B", CreateHlc(2000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "C", CreateHlc(3000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "D", CreateHlc(4000, 0, "n1")), ct);
|
||||
|
||||
// Act
|
||||
var events = await _store.GetByHlcRangeAsync(
|
||||
"corr-1",
|
||||
new HlcTimestamp(2000, 0, "n1"),
|
||||
new HlcTimestamp(3000, 0, "n1"));
|
||||
CreateHlc(2000, 0, "n1"),
|
||||
CreateHlc(3000, 0, "n1"),
|
||||
ct);
|
||||
|
||||
// Assert
|
||||
events.Should().HaveCount(2);
|
||||
@@ -137,12 +153,13 @@ public sealed class InMemoryTimelineEventStoreTests
|
||||
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"));
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1"), "Scheduler"), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-2", "B", CreateHlc(2000, 0, "n1"), "AirGap"), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-3", "C", CreateHlc(3000, 0, "n1"), "Scheduler"), ct);
|
||||
|
||||
// Act
|
||||
var events = await _store.GetByServiceAsync("Scheduler");
|
||||
var events = await _store.GetByServiceAsync("Scheduler", cancellationToken: ct);
|
||||
|
||||
// Assert
|
||||
events.Should().HaveCount(2);
|
||||
@@ -153,7 +170,8 @@ public sealed class InMemoryTimelineEventStoreTests
|
||||
public async Task GetByIdAsync_NotFound_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = await _store.GetByIdAsync("nonexistent");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var result = await _store.GetByIdAsync("nonexistent", ct);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
@@ -163,14 +181,15 @@ public sealed class InMemoryTimelineEventStoreTests
|
||||
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")));
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "B", CreateHlc(2000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-2", "C", CreateHlc(3000, 0, "n1")), ct);
|
||||
|
||||
// Act
|
||||
var count1 = await _store.CountByCorrelationIdAsync("corr-1");
|
||||
var count2 = await _store.CountByCorrelationIdAsync("corr-2");
|
||||
var count3 = await _store.CountByCorrelationIdAsync("corr-3");
|
||||
var count1 = await _store.CountByCorrelationIdAsync("corr-1", ct);
|
||||
var count2 = await _store.CountByCorrelationIdAsync("corr-2", ct);
|
||||
var count3 = await _store.CountByCorrelationIdAsync("corr-3", ct);
|
||||
|
||||
// Assert
|
||||
count1.Should().Be(2);
|
||||
@@ -182,26 +201,28 @@ public sealed class InMemoryTimelineEventStoreTests
|
||||
public async Task AppendBatchAsync_StoresAllEvents()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
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"))
|
||||
CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1")),
|
||||
CreateEvent("corr-1", "B", CreateHlc(2000, 0, "n1")),
|
||||
CreateEvent("corr-1", "C", CreateHlc(3000, 0, "n1"))
|
||||
};
|
||||
|
||||
// Act
|
||||
await _store.AppendBatchAsync(events);
|
||||
await _store.AppendBatchAsync(events, ct);
|
||||
|
||||
// Assert
|
||||
var count = await _store.CountByCorrelationIdAsync("corr-1");
|
||||
var count = await _store.CountByCorrelationIdAsync("corr-1", ct);
|
||||
count.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_RemovesAllEvents()
|
||||
public async Task Clear_RemovesAllEvents()
|
||||
{
|
||||
// Arrange
|
||||
_store.AppendAsync(CreateEvent("corr-1", "A", new HlcTimestamp(1000, 0, "n1"))).Wait();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1")), ct);
|
||||
|
||||
// Act
|
||||
_store.Clear();
|
||||
|
||||
@@ -15,12 +15,6 @@
|
||||
</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>
|
||||
|
||||
10
src/__Libraries/__Tests/StellaOps.Eventing.Tests/TASKS.md
Normal file
10
src/__Libraries/__Tests/StellaOps.Eventing.Tests/TASKS.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Eventing Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0024-M | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0024-T | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0024-A | DONE | Waived (test project). |
|
||||
@@ -40,19 +40,30 @@ public sealed class TimelineEventEmitterTests
|
||||
NullLogger<TimelineEventEmitter>.Instance);
|
||||
}
|
||||
|
||||
private static HlcTimestamp CreateHlc(long physicalTime, int logicalCounter, string nodeId)
|
||||
{
|
||||
return new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = physicalTime,
|
||||
LogicalCounter = logicalCounter,
|
||||
NodeId = nodeId
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmitAsync_StoresEventWithCorrectFields()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var correlationId = "scan-abc123";
|
||||
var kind = EventKinds.Enqueue;
|
||||
var payload = new { JobId = "job-1", Status = "pending" };
|
||||
var expectedHlc = new HlcTimestamp(1704585600000, 0, "node1");
|
||||
var expectedHlc = CreateHlc(1704585600000, 0, "node1");
|
||||
|
||||
_hlcMock.Setup(h => h.Tick()).Returns(expectedHlc);
|
||||
|
||||
// Act
|
||||
var result = await _emitter.EmitAsync(correlationId, kind, payload);
|
||||
var result = await _emitter.EmitAsync(correlationId, kind, payload, ct);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
@@ -72,15 +83,16 @@ public sealed class TimelineEventEmitterTests
|
||||
public async Task EmitAsync_GeneratesDeterministicEventId()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var correlationId = "scan-abc123";
|
||||
var kind = EventKinds.Execute;
|
||||
var payload = new { Step = 1 };
|
||||
var hlc = new HlcTimestamp(1704585600000, 0, "node1");
|
||||
var hlc = CreateHlc(1704585600000, 0, "node1");
|
||||
|
||||
_hlcMock.Setup(h => h.Tick()).Returns(hlc);
|
||||
|
||||
// Act
|
||||
var result1 = await _emitter.EmitAsync(correlationId, kind, payload);
|
||||
var result1 = await _emitter.EmitAsync(correlationId, kind, payload, ct);
|
||||
|
||||
// Create a second emitter with same config
|
||||
var emitter2 = new TimelineEventEmitter(
|
||||
@@ -90,7 +102,7 @@ public sealed class TimelineEventEmitterTests
|
||||
_options,
|
||||
NullLogger<TimelineEventEmitter>.Instance);
|
||||
|
||||
var result2 = await emitter2.EmitAsync(correlationId, kind, payload);
|
||||
var result2 = await emitter2.EmitAsync(correlationId, kind, payload, ct);
|
||||
|
||||
// Assert - Same inputs should produce same EventId
|
||||
result1.EventId.Should().Be(result2.EventId);
|
||||
@@ -100,15 +112,16 @@ public sealed class TimelineEventEmitterTests
|
||||
public async Task EmitAsync_StoresEventInStore()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var correlationId = "scan-abc123";
|
||||
var hlc = new HlcTimestamp(1704585600000, 0, "node1");
|
||||
var hlc = CreateHlc(1704585600000, 0, "node1");
|
||||
_hlcMock.Setup(h => h.Tick()).Returns(hlc);
|
||||
|
||||
// Act
|
||||
var emitted = await _emitter.EmitAsync(correlationId, EventKinds.Enqueue, new { Test = true });
|
||||
var emitted = await _emitter.EmitAsync(correlationId, EventKinds.Enqueue, new { Test = true }, ct);
|
||||
|
||||
// Assert
|
||||
var stored = await _eventStore.GetByIdAsync(emitted.EventId);
|
||||
var stored = await _eventStore.GetByIdAsync(emitted.EventId, ct);
|
||||
stored.Should().NotBeNull();
|
||||
stored!.EventId.Should().Be(emitted.EventId);
|
||||
}
|
||||
@@ -117,9 +130,10 @@ public sealed class TimelineEventEmitterTests
|
||||
public async Task EmitBatchAsync_StoresAllEvents()
|
||||
{
|
||||
// Arrange
|
||||
var hlcCounter = 0L;
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var hlcCounter = 0;
|
||||
_hlcMock.Setup(h => h.Tick())
|
||||
.Returns(() => new HlcTimestamp(1704585600000, hlcCounter++, "node1"));
|
||||
.Returns(() => CreateHlc(1704585600000, hlcCounter++, "node1"));
|
||||
|
||||
var pendingEvents = new[]
|
||||
{
|
||||
@@ -129,7 +143,7 @@ public sealed class TimelineEventEmitterTests
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = await _emitter.EmitBatchAsync(pendingEvents);
|
||||
var results = await _emitter.EmitBatchAsync(pendingEvents, ct);
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(3);
|
||||
@@ -144,7 +158,8 @@ public sealed class TimelineEventEmitterTests
|
||||
public async Task EmitBatchAsync_EmptyBatch_ReturnsEmptyList()
|
||||
{
|
||||
// Act
|
||||
var results = await _emitter.EmitBatchAsync(Array.Empty<PendingEvent>());
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var results = await _emitter.EmitBatchAsync(Array.Empty<PendingEvent>(), ct);
|
||||
|
||||
// Assert
|
||||
results.Should().BeEmpty();
|
||||
@@ -154,11 +169,12 @@ public sealed class TimelineEventEmitterTests
|
||||
public async Task EmitAsync_IncludesPayloadDigest()
|
||||
{
|
||||
// Arrange
|
||||
var hlc = new HlcTimestamp(1704585600000, 0, "node1");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var hlc = CreateHlc(1704585600000, 0, "node1");
|
||||
_hlcMock.Setup(h => h.Tick()).Returns(hlc);
|
||||
|
||||
// Act
|
||||
var result = await _emitter.EmitAsync("corr-1", EventKinds.Emit, new { Data = "test" });
|
||||
var result = await _emitter.EmitAsync("corr-1", EventKinds.Emit, new { Data = "test" }, ct);
|
||||
|
||||
// Assert
|
||||
result.PayloadDigest.Should().NotBeNull();
|
||||
@@ -169,13 +185,14 @@ public sealed class TimelineEventEmitterTests
|
||||
public async Task EmitAsync_DifferentPayloads_DifferentDigests()
|
||||
{
|
||||
// Arrange
|
||||
var hlcCounter = 0L;
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var hlcCounter = 0;
|
||||
_hlcMock.Setup(h => h.Tick())
|
||||
.Returns(() => new HlcTimestamp(1704585600000, hlcCounter++, "node1"));
|
||||
.Returns(() => CreateHlc(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 });
|
||||
var result1 = await _emitter.EmitAsync("corr-1", EventKinds.Emit, new { Value = 1 }, ct);
|
||||
var result2 = await _emitter.EmitAsync("corr-1", EventKinds.Emit, new { Value = 2 }, ct);
|
||||
|
||||
// Assert
|
||||
result1.PayloadDigest.Should().NotBeEquivalentTo(result2.PayloadDigest);
|
||||
|
||||
@@ -5,6 +5,9 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0025-M | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0025-T | DONE | Revalidated 2026-01-08 (rebaseline). |
|
||||
| AUDIT-0025-A | DONE | Waived (test project). |
|
||||
| AUDIT-0285-M | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
|
||||
| AUDIT-0285-T | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
|
||||
| AUDIT-0285-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
|
||||
@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0286-M | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
|
||||
| AUDIT-0286-T | DONE | Revalidated 2026-01-07; open findings tracked in audit report. |
|
||||
| AUDIT-0286-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| AUDIT-0026-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0026-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0026-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Hybrid Logical Clock Tests Charter
|
||||
|
||||
## Mission
|
||||
Validate HLC timestamping, serialization, and monotonic behavior across scenarios.
|
||||
|
||||
## Responsibilities
|
||||
- Maintain unit/integration tests for HLC behavior and state stores.
|
||||
- Keep tests deterministic and offline-friendly.
|
||||
- Track sprint tasks in `TASKS.md` and update the sprint tracker.
|
||||
|
||||
## Key Paths
|
||||
- `HlcTimestampTests.cs`
|
||||
- `HybridLogicalClockTests.cs`
|
||||
- `HybridLogicalClockIntegrationTests.cs`
|
||||
|
||||
## Required Reading
|
||||
- `docs/modules/platform/architecture-overview.md`
|
||||
- `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`
|
||||
|
||||
## Working Agreement
|
||||
- 1. Avoid hardware-dependent perf assertions in CI by gating as needed.
|
||||
- 2. Use deterministic TimeProvider in tests.
|
||||
- 3. Update `TASKS.md` and sprint statuses when work changes.
|
||||
@@ -98,9 +98,9 @@ public class HlcTimestampTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_NullString_ThrowsArgumentException()
|
||||
public void Parse_NullString_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => HlcTimestamp.Parse(null!));
|
||||
Assert.Throws<ArgumentNullException>(() => HlcTimestamp.Parse(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -457,9 +457,9 @@ public class HlcTimestampTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Now_NullNodeId_ThrowsArgumentException()
|
||||
public void Now_NullNodeId_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => HlcTimestamp.Now(null!, TimeProvider.System));
|
||||
Assert.Throws<ArgumentNullException>(() => HlcTimestamp.Now(null!, TimeProvider.System));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -431,9 +431,9 @@ public class HybridLogicalClockTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NullNodeId_ThrowsArgumentException()
|
||||
public void Constructor_NullNodeId_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new HybridLogicalClock(
|
||||
TimeProvider.System,
|
||||
null!,
|
||||
|
||||
@@ -68,11 +68,11 @@ public class InMemoryHlcStateStoreTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_NullNodeId_ThrowsArgumentException()
|
||||
public async Task LoadAsync_NullNodeId_ThrowsArgumentNullException()
|
||||
{
|
||||
var store = new InMemoryHlcStateStore();
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => store.LoadAsync(null!));
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => store.LoadAsync(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Hybrid Logical Clock Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0027-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0027-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0027-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
@@ -20,4 +20,8 @@
|
||||
<ProjectReference Include="..\..\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
|
||||
<ProjectReference Include="../../StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Migrations\\TestMigrations\\*.sql" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0360-M | DONE | Revalidated 2026-01-07; maintainability audit for Infrastructure.Postgres.Tests. |
|
||||
| AUDIT-0360-T | DONE | Revalidated 2026-01-07; test coverage audit for Infrastructure.Postgres.Tests. |
|
||||
| AUDIT-0360-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| AUDIT-0028-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0028-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0028-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
|
||||
@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0386-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Metrics.Tests. |
|
||||
| AUDIT-0386-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Metrics.Tests. |
|
||||
| AUDIT-0386-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| AUDIT-0029-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0029-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0029-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
@@ -39,6 +40,7 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
ApplicationName = "MinimalApiTestApp"
|
||||
});
|
||||
builder.WebHost.UseUrls("http://127.0.0.1:0");
|
||||
|
||||
// Register test services
|
||||
builder.Services.AddSingleton(new StellaRouterBridgeOptions
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -37,6 +38,7 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
ApplicationName = "BridgeIntegrationTestApp"
|
||||
});
|
||||
builder.WebHost.UseUrls("http://127.0.0.1:0");
|
||||
|
||||
// Add authorization services (required by DefaultAuthorizationClaimMapper)
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0389-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Microservice.AspNetCore.Tests. |
|
||||
| AUDIT-0389-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Microservice.AspNetCore.Tests. |
|
||||
| AUDIT-0389-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| AUDIT-0030-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0030-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0030-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
|
||||
@@ -5,6 +5,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0437-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Plugin.Tests. |
|
||||
| AUDIT-0437-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Plugin.Tests. |
|
||||
| AUDIT-0437-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| AUDIT-0031-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0031-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0031-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
|
||||
32
src/__Libraries/__Tests/StellaOps.Provcache.Tests/AGENTS.md
Normal file
32
src/__Libraries/__Tests/StellaOps.Provcache.Tests/AGENTS.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# StellaOps.Provcache.Tests - Local Agent Charter
|
||||
|
||||
## Roles
|
||||
- Backend developer
|
||||
- QA automation engineer
|
||||
|
||||
## Working directory
|
||||
- src/__Libraries/__Tests/StellaOps.Provcache.Tests
|
||||
|
||||
## Allowed dependencies
|
||||
- src/__Libraries/StellaOps.Provcache
|
||||
- src/__Libraries/StellaOps.Provcache.Api
|
||||
- src/__Libraries/StellaOps.TestKit
|
||||
|
||||
## Required reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/prov-cache/README.md
|
||||
- docs/modules/prov-cache/architecture.md
|
||||
- docs/modules/prov-cache/metrics-alerting.md
|
||||
|
||||
## Determinism and test rules
|
||||
- Use deterministic inputs: avoid DateTime.UtcNow, DateTimeOffset.UtcNow, Guid.NewGuid, and Random.Shared in tests.
|
||||
- Use TimeProvider and fixed seeds or fixtures for time- and randomness-dependent tests.
|
||||
- Use CultureInfo.InvariantCulture for parsing and formatting in tests.
|
||||
- Tag tests with TestCategories (Unit, Integration, Performance) and keep integration tests out of unit-only runs.
|
||||
- Keep tests offline and deterministic; prefer TestServer/TestHost with fixed inputs.
|
||||
|
||||
## Quality and safety
|
||||
- ASCII-only strings and comments unless explicitly justified.
|
||||
- Clean up temp files/directories created during tests.
|
||||
10
src/__Libraries/__Tests/StellaOps.Provcache.Tests/TASKS.md
Normal file
10
src/__Libraries/__Tests/StellaOps.Provcache.Tests/TASKS.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# StellaOps.Provcache.Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0032-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0032-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0032-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
30
src/__Libraries/__Tests/StellaOps.Provenance.Tests/AGENTS.md
Normal file
30
src/__Libraries/__Tests/StellaOps.Provenance.Tests/AGENTS.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# StellaOps.Provenance.Tests - Local Agent Charter
|
||||
|
||||
## Roles
|
||||
- Backend developer
|
||||
- QA automation engineer
|
||||
|
||||
## Working directory
|
||||
- src/__Libraries/__Tests/StellaOps.Provenance.Tests
|
||||
|
||||
## Allowed dependencies
|
||||
- src/__Libraries/StellaOps.Provenance
|
||||
- src/__Libraries/StellaOps.TestKit
|
||||
|
||||
## Required reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/provenance/README.md
|
||||
- docs/modules/provenance/architecture.md
|
||||
- docs/modules/provenance/guides/provenance-attestation.md
|
||||
|
||||
## Determinism and test rules
|
||||
- Use deterministic inputs; avoid DateTime.UtcNow, DateTimeOffset.UtcNow, Guid.NewGuid, and Random.Shared in tests.
|
||||
- Use TimeProvider and fixed seeds or fixtures for time- and randomness-dependent tests.
|
||||
- Use CultureInfo.InvariantCulture for parsing and formatting in tests.
|
||||
- Tag tests with TestCategories (Unit, Integration, Performance) and keep integration tests out of unit-only runs.
|
||||
|
||||
## Quality and safety
|
||||
- ASCII-only strings and comments unless explicitly justified.
|
||||
- Keep fixtures small and deterministic; clean up temp resources.
|
||||
@@ -58,7 +58,7 @@ public sealed class ProvenanceExtensionsTests
|
||||
|
||||
var provenanceDoc = (DocumentObject)((DocumentObject)document["provenance"])["dsse"];
|
||||
Assert.Equal("sha256:deadbeef", ((DocumentString)provenanceDoc["envelopeDigest"]).Value);
|
||||
Assert.Equal(123, ((DocumentInt64)((DocumentObject)provenanceDoc["rekor"])["logIndex"]).Value);
|
||||
Assert.Equal(123L, ((DocumentInt64)((DocumentObject)provenanceDoc["rekor"])["logIndex"]).Value);
|
||||
Assert.Equal("att:build#1", ((DocumentString)((DocumentObject)((DocumentArray)provenanceDoc["chain"])[0])["id"]).Value);
|
||||
|
||||
var trustDoc = (DocumentObject)document["trust"];
|
||||
|
||||
10
src/__Libraries/__Tests/StellaOps.Provenance.Tests/TASKS.md
Normal file
10
src/__Libraries/__Tests/StellaOps.Provenance.Tests/TASKS.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# StellaOps.Provenance.Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0033-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0033-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0033-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
29
src/__Libraries/__Tests/StellaOps.ReachGraph.Tests/AGENTS.md
Normal file
29
src/__Libraries/__Tests/StellaOps.ReachGraph.Tests/AGENTS.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# StellaOps.ReachGraph.Tests - Local Agent Charter
|
||||
|
||||
## Roles
|
||||
- Backend developer
|
||||
- QA automation engineer
|
||||
|
||||
## Working directory
|
||||
- src/__Libraries/__Tests/StellaOps.ReachGraph.Tests
|
||||
|
||||
## Allowed dependencies
|
||||
- src/__Libraries/StellaOps.ReachGraph
|
||||
- src/__Libraries/StellaOps.TestKit
|
||||
|
||||
## Required reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/reach-graph/README.md
|
||||
- docs/modules/reach-graph/architecture.md
|
||||
|
||||
## Determinism and test rules
|
||||
- Use deterministic inputs; avoid DateTime.UtcNow, DateTimeOffset.UtcNow, Guid.NewGuid, and Random.Shared in tests.
|
||||
- Use TimeProvider and fixed seeds or fixtures for time- and randomness-dependent tests.
|
||||
- Use CultureInfo.InvariantCulture for parsing and formatting in tests.
|
||||
- Tag tests with TestCategories (Unit, Integration, Performance) and keep integration tests out of unit-only runs.
|
||||
|
||||
## Quality and safety
|
||||
- ASCII-only strings and comments unless explicitly justified.
|
||||
- Keep fixtures small and deterministic; clean up temp resources.
|
||||
@@ -137,7 +137,7 @@ public class GoldenSampleTests
|
||||
foreach (var node in graph.Nodes)
|
||||
{
|
||||
Assert.NotEmpty(node.Id);
|
||||
Assert.NotEqual(ReachGraphNodeKind.Package, default); // Kind is set
|
||||
Assert.True(Enum.IsDefined(node.Kind)); // Kind is set
|
||||
Assert.NotEmpty(node.Ref);
|
||||
}
|
||||
}
|
||||
|
||||
10
src/__Libraries/__Tests/StellaOps.ReachGraph.Tests/TASKS.md
Normal file
10
src/__Libraries/__Tests/StellaOps.ReachGraph.Tests/TASKS.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# StellaOps.ReachGraph.Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0034-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0034-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0034-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
@@ -0,0 +1,30 @@
|
||||
# StellaOps.Replay.Core.Tests - Local Agent Charter
|
||||
|
||||
## Roles
|
||||
- Backend developer
|
||||
- QA automation engineer
|
||||
|
||||
## Working directory
|
||||
- src/__Libraries/__Tests/StellaOps.Replay.Core.Tests
|
||||
|
||||
## Allowed dependencies
|
||||
- src/__Libraries/StellaOps.Replay.Core
|
||||
- src/__Libraries/StellaOps.TestKit
|
||||
|
||||
## Required reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/replay/architecture.md
|
||||
- docs/modules/replay/guides/DETERMINISTIC_REPLAY.md
|
||||
- docs/modules/replay/guides/TEST_STRATEGY.md
|
||||
|
||||
## Determinism and test rules
|
||||
- Use deterministic inputs; avoid DateTime.UtcNow, DateTimeOffset.UtcNow, Guid.NewGuid, and Random.Shared in tests.
|
||||
- Use TimeProvider and fixed seeds or fixtures for time- and randomness-dependent tests.
|
||||
- Use CultureInfo.InvariantCulture for parsing and formatting in tests.
|
||||
- Tag tests with TestCategories (Unit, Integration, Performance) and keep integration tests out of unit-only runs.
|
||||
|
||||
## Quality and safety
|
||||
- ASCII-only strings and comments unless explicitly justified.
|
||||
- Keep fixtures small and deterministic; clean up temp resources.
|
||||
@@ -39,7 +39,7 @@ public class ReachabilityReplayWriterTests
|
||||
Assert.Equal("analysis-123", manifest.Reachability.AnalysisId);
|
||||
Assert.Equal("cas://reachability/graphs/a", manifest.Reachability.Graphs.First().CasUri);
|
||||
Assert.Equal("cas://runtime/trace/1", manifest.Reachability.RuntimeTraces.First().CasUri);
|
||||
Assert.All(manifest.Reachability.Graphs, g => Assert.Equal("blake3-256", g.HashAlgorithm));
|
||||
Assert.All(manifest.Reachability.Graphs, g => Assert.Equal("sha256", g.HashAlgorithm));
|
||||
Assert.All(manifest.Reachability.RuntimeTraces, t => Assert.False(string.IsNullOrWhiteSpace(t.HashAlgorithm)));
|
||||
|
||||
// canonical hash should be stable regardless of input order
|
||||
|
||||
10
src/__Libraries/__Tests/StellaOps.Replay.Core.Tests/TASKS.md
Normal file
10
src/__Libraries/__Tests/StellaOps.Replay.Core.Tests/TASKS.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# StellaOps.Replay.Core.Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0035-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0035-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0035-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
32
src/__Libraries/__Tests/StellaOps.Replay.Tests/AGENTS.md
Normal file
32
src/__Libraries/__Tests/StellaOps.Replay.Tests/AGENTS.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# StellaOps.Replay.Tests - Local Agent Charter
|
||||
|
||||
## Roles
|
||||
- Backend developer
|
||||
- QA automation engineer
|
||||
|
||||
## Working directory
|
||||
- src/__Libraries/__Tests/StellaOps.Replay.Tests
|
||||
|
||||
## Allowed dependencies
|
||||
- src/__Libraries/StellaOps.Replay
|
||||
- src/__Libraries/StellaOps.Evidence
|
||||
- src/__Libraries/StellaOps.TestKit
|
||||
- src/__Tests/__Libraries/StellaOps.Testing.Manifests
|
||||
|
||||
## Required reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/replay/architecture.md
|
||||
- docs/modules/replay/guides/DETERMINISTIC_REPLAY.md
|
||||
- docs/modules/replay/guides/TEST_STRATEGY.md
|
||||
|
||||
## Determinism and test rules
|
||||
- Use deterministic inputs; avoid DateTime.UtcNow, DateTimeOffset.UtcNow, Guid.NewGuid, and Random.Shared in tests.
|
||||
- Use TimeProvider and fixed seeds or fixtures for time- and randomness-dependent tests.
|
||||
- Use CultureInfo.InvariantCulture for parsing and formatting in tests.
|
||||
- Tag tests with TestCategories (Unit, Integration, Performance) and keep integration tests out of unit-only runs.
|
||||
|
||||
## Quality and safety
|
||||
- ASCII-only strings and comments unless explicitly justified.
|
||||
- Keep fixtures small and deterministic; clean up temp resources.
|
||||
@@ -32,7 +32,7 @@ public class ReplayEngineTests
|
||||
var manifest1 = CreateManifest();
|
||||
var manifest2 = manifest1 with
|
||||
{
|
||||
FeedSnapshot = manifest1.FeedSnapshot with { Version = "v2" }
|
||||
PolicySnapshot = manifest1.PolicySnapshot with { LatticeRulesDigest = new string('f', 64) }
|
||||
};
|
||||
|
||||
var engine = CreateEngine();
|
||||
|
||||
10
src/__Libraries/__Tests/StellaOps.Replay.Tests/TASKS.md
Normal file
10
src/__Libraries/__Tests/StellaOps.Replay.Tests/TASKS.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# StellaOps.Replay.Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0036-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0036-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0036-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
29
src/__Libraries/__Tests/StellaOps.Signals.Tests/AGENTS.md
Normal file
29
src/__Libraries/__Tests/StellaOps.Signals.Tests/AGENTS.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# StellaOps.Signals.Tests - Local Agent Charter
|
||||
|
||||
## Roles
|
||||
- Backend developer
|
||||
- QA automation engineer
|
||||
|
||||
## Working directory
|
||||
- src/__Libraries/__Tests/StellaOps.Signals.Tests
|
||||
|
||||
## Allowed dependencies
|
||||
- src/Signals/StellaOps.Signals
|
||||
- src/__Libraries/StellaOps.TestKit
|
||||
|
||||
## Required reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/signals/README.md
|
||||
- docs/modules/signals/contracts/signals-provenance-contract.md
|
||||
|
||||
## Determinism and test rules
|
||||
- Use deterministic inputs; avoid DateTime.UtcNow, DateTimeOffset.UtcNow, Guid.NewGuid, and Random.Shared in tests.
|
||||
- Use TimeProvider and fixed seeds or fixtures for time- and randomness-dependent tests.
|
||||
- Use CultureInfo.InvariantCulture for parsing and formatting in tests.
|
||||
- Tag tests with TestCategories (Unit, Integration, Performance) and keep integration tests out of unit-only runs.
|
||||
|
||||
## Quality and safety
|
||||
- ASCII-only strings and comments unless explicitly justified.
|
||||
- Keep fixtures small and deterministic; clean up temp resources.
|
||||
10
src/__Libraries/__Tests/StellaOps.Signals.Tests/TASKS.md
Normal file
10
src/__Libraries/__Tests/StellaOps.Signals.Tests/TASKS.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# StellaOps.Signals.Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0037-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0037-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0037-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Services;
|
||||
|
||||
namespace StellaOps.Signals.Tests.TestInfrastructure;
|
||||
|
||||
internal sealed class InMemoryReachabilityCache : IReachabilityCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ReachabilityFactDocument> cache = new(StringComparer.Ordinal);
|
||||
|
||||
public Task<ReachabilityFactDocument?> GetAsync(string subjectKey, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(cache.TryGetValue(subjectKey, out var document) ? document : null);
|
||||
}
|
||||
|
||||
public Task SetAsync(ReachabilityFactDocument document, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
cache[document.SubjectKey] = document;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task InvalidateAsync(string subjectKey, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
cache.TryRemove(subjectKey, out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Signals.Services;
|
||||
|
||||
namespace StellaOps.Signals.Tests.TestInfrastructure;
|
||||
|
||||
@@ -33,6 +36,12 @@ public sealed class SignalsTestFactory : WebApplicationFactory<Program>, IAsyncL
|
||||
|
||||
configuration.AddInMemoryCollection(settings);
|
||||
});
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
services.RemoveAll<IReachabilityCache>();
|
||||
services.AddSingleton<IReachabilityCache, InMemoryReachabilityCache>();
|
||||
});
|
||||
}
|
||||
|
||||
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
// <copyright file="Spdx3ParserBenchmarks.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Spdx3.JsonLd;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Performance benchmarks for SPDX 3.0.1 parser.
|
||||
/// Task: SP3-018 - Validate parsing performance for various document sizes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These tests measure parsing performance and compare against 2.x parser baseline.
|
||||
/// Target: SPDX 3.0.1 parser should be within 2x of 2.x parser performance.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To run as proper benchmarks, consider using BenchmarkDotNet in a dedicated
|
||||
/// benchmark project (StellaOps.Spdx3.Benchmarks).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Trait("Category", "Performance")]
|
||||
public sealed class Spdx3ParserBenchmarks : IDisposable
|
||||
{
|
||||
private const int WarmupIterations = 3;
|
||||
private const int BenchmarkIterations = 10;
|
||||
|
||||
private readonly Spdx3Parser _parser;
|
||||
private readonly MemoryCache _cache;
|
||||
private readonly string _tempDir;
|
||||
|
||||
public Spdx3ParserBenchmarks()
|
||||
{
|
||||
_cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 1000 });
|
||||
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||
var options = Options.Create(new Spdx3ContextResolverOptions { AllowRemoteContexts = false });
|
||||
var resolver = new Spdx3ContextResolver(
|
||||
httpClientFactory.Object,
|
||||
_cache,
|
||||
NullLogger<Spdx3ContextResolver>.Instance,
|
||||
options,
|
||||
TimeProvider.System);
|
||||
|
||||
_parser = new Spdx3Parser(resolver, NullLogger<Spdx3Parser>.Instance);
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"spdx3-bench-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Benchmark_Parse100Elements_CompletesWithinTarget()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var documentPath = GenerateSpdx3Document(100);
|
||||
|
||||
// Warmup
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
}
|
||||
|
||||
// Act
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var result = await _parser.ParseAsync(documentPath, ct);
|
||||
Assert.True(result.Success, $"Parse failed: {string.Join(", ", result.Errors.Select(e => e.Message))}");
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / BenchmarkIterations;
|
||||
var maxTargetMs = 100.0; // Target: < 100ms for 100 elements
|
||||
|
||||
Assert.True(
|
||||
avgMs < maxTargetMs,
|
||||
$"100-element parse averaged {avgMs:F2}ms, target < {maxTargetMs}ms");
|
||||
|
||||
// Log for visibility
|
||||
TestContext.Current.TestOutputHelper?.WriteLine(
|
||||
$"100-element parse: {avgMs:F2}ms average over {BenchmarkIterations} iterations");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Benchmark_Parse1000Elements_CompletesWithinTarget()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var documentPath = GenerateSpdx3Document(1000);
|
||||
|
||||
// Warmup
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
}
|
||||
|
||||
// Act
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var result = await _parser.ParseAsync(documentPath, ct);
|
||||
Assert.True(result.Success, $"Parse failed: {string.Join(", ", result.Errors.Select(e => e.Message))}");
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / BenchmarkIterations;
|
||||
var maxTargetMs = 500.0; // Target: < 500ms for 1000 elements
|
||||
|
||||
Assert.True(
|
||||
avgMs < maxTargetMs,
|
||||
$"1000-element parse averaged {avgMs:F2}ms, target < {maxTargetMs}ms");
|
||||
|
||||
TestContext.Current.TestOutputHelper?.WriteLine(
|
||||
$"1000-element parse: {avgMs:F2}ms average over {BenchmarkIterations} iterations");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Benchmark_Parse10000Elements_CompletesWithinTarget()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var documentPath = GenerateSpdx3Document(10000);
|
||||
|
||||
// Warmup
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
}
|
||||
|
||||
// Act
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var result = await _parser.ParseAsync(documentPath, ct);
|
||||
Assert.True(result.Success, $"Parse failed: {string.Join(", ", result.Errors.Select(e => e.Message))}");
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / BenchmarkIterations;
|
||||
var maxTargetMs = 5000.0; // Target: < 5000ms for 10000 elements
|
||||
|
||||
Assert.True(
|
||||
avgMs < maxTargetMs,
|
||||
$"10000-element parse averaged {avgMs:F2}ms, target < {maxTargetMs}ms");
|
||||
|
||||
TestContext.Current.TestOutputHelper?.WriteLine(
|
||||
$"10000-element parse: {avgMs:F2}ms average over {BenchmarkIterations} iterations");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Benchmark_ScalingCharacteristics_SubLinear()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var sizes = new[] { 100, 500, 1000, 2000 };
|
||||
var timings = new Dictionary<int, double>();
|
||||
|
||||
foreach (var size in sizes)
|
||||
{
|
||||
var documentPath = GenerateSpdx3Document(size);
|
||||
|
||||
// Warmup
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
|
||||
// Measure
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
timings[size] = sw.Elapsed.TotalMilliseconds / 5;
|
||||
}
|
||||
|
||||
// Assert: scaling should be roughly linear (within 2x expected)
|
||||
// If 100 elements takes T, then 1000 should take ~10T (not 100T)
|
||||
var time100 = timings[100];
|
||||
var time1000 = timings[1000];
|
||||
var scalingFactor = time1000 / time100;
|
||||
var expectedScaling = 10.0; // Linear scaling
|
||||
var maxScaling = expectedScaling * 2.5; // Allow 2.5x tolerance
|
||||
|
||||
Assert.True(
|
||||
scalingFactor < maxScaling,
|
||||
$"Scaling factor {scalingFactor:F2}x exceeds target {maxScaling:F2}x (expected ~{expectedScaling:F2}x)");
|
||||
|
||||
TestContext.Current.TestOutputHelper?.WriteLine($"Scaling results:");
|
||||
foreach (var (size, time) in timings)
|
||||
{
|
||||
TestContext.Current.TestOutputHelper?.WriteLine($" {size} elements: {time:F2}ms");
|
||||
}
|
||||
TestContext.Current.TestOutputHelper?.WriteLine($"Scaling factor (100 -> 1000): {scalingFactor:F2}x");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Benchmark_MemoryUsage_StaysWithinBounds()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var documentPath = GenerateSpdx3Document(1000);
|
||||
|
||||
// Force GC before measurement
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
|
||||
var memBefore = GC.GetTotalMemory(true);
|
||||
|
||||
// Act
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var result = await _parser.ParseAsync(documentPath, ct);
|
||||
Assert.True(result.Success);
|
||||
}
|
||||
|
||||
// Allow time for finalization
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
|
||||
var memAfter = GC.GetTotalMemory(true);
|
||||
var memDelta = memAfter - memBefore;
|
||||
|
||||
// Assert: memory growth should be bounded (not leaking)
|
||||
// Allow up to 50MB growth for 10 parses of 1000-element docs
|
||||
var maxMemGrowthBytes = 50 * 1024 * 1024L;
|
||||
|
||||
Assert.True(
|
||||
memDelta < maxMemGrowthBytes,
|
||||
$"Memory grew by {memDelta / 1024.0 / 1024.0:F2}MB, target < {maxMemGrowthBytes / 1024.0 / 1024.0:F2}MB");
|
||||
|
||||
TestContext.Current.TestOutputHelper?.WriteLine(
|
||||
$"Memory growth after 10 parses: {memDelta / 1024.0 / 1024.0:F2}MB");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an SPDX 3.0.1 JSON-LD document with the specified number of package elements.
|
||||
/// </summary>
|
||||
private string GenerateSpdx3Document(int packageCount)
|
||||
{
|
||||
var graph = new JsonArray();
|
||||
|
||||
// Add SpdxDocument root
|
||||
var document = new JsonObject
|
||||
{
|
||||
["@type"] = "SpdxDocument",
|
||||
["@id"] = "https://stellaops.org/spdx/benchmark-doc",
|
||||
["spdxId"] = "https://stellaops.org/spdx/benchmark-doc",
|
||||
["name"] = $"Benchmark Document ({packageCount} packages)",
|
||||
["specVersion"] = "3.0.1",
|
||||
["creationInfo"] = new JsonObject
|
||||
{
|
||||
["@type"] = "CreationInfo",
|
||||
["created"] = "2026-01-08T00:00:00Z",
|
||||
["createdBy"] = new JsonArray { "https://stellaops.org/spdx/tool/benchmark" },
|
||||
["specVersion"] = "3.0.1"
|
||||
},
|
||||
["rootElement"] = new JsonArray { "https://stellaops.org/spdx/benchmark-root-pkg" },
|
||||
["profileConformance"] = new JsonArray { "core", "software" }
|
||||
};
|
||||
graph.Add(document);
|
||||
|
||||
// Add root package
|
||||
var rootPackage = new JsonObject
|
||||
{
|
||||
["@type"] = "software_Package",
|
||||
["@id"] = "https://stellaops.org/spdx/benchmark-root-pkg",
|
||||
["spdxId"] = "https://stellaops.org/spdx/benchmark-root-pkg",
|
||||
["name"] = "benchmark-root",
|
||||
["packageVersion"] = "1.0.0",
|
||||
["downloadLocation"] = "https://example.com/benchmark-root-1.0.0.tar.gz"
|
||||
};
|
||||
graph.Add(rootPackage);
|
||||
|
||||
// Add package elements
|
||||
for (var i = 0; i < packageCount; i++)
|
||||
{
|
||||
var pkg = new JsonObject
|
||||
{
|
||||
["@type"] = "software_Package",
|
||||
["@id"] = $"https://stellaops.org/spdx/pkg-{i:D5}",
|
||||
["spdxId"] = $"https://stellaops.org/spdx/pkg-{i:D5}",
|
||||
["name"] = $"package-{i:D5}",
|
||||
["packageVersion"] = $"{i / 100}.{i % 100}.0",
|
||||
["downloadLocation"] = $"https://example.com/pkg-{i:D5}.tar.gz",
|
||||
["externalIdentifier"] = new JsonArray
|
||||
{
|
||||
new JsonObject
|
||||
{
|
||||
["@type"] = "ExternalIdentifier",
|
||||
["externalIdentifierType"] = "packageUrl",
|
||||
["identifier"] = $"pkg:generic/package-{i:D5}@{i / 100}.{i % 100}.0"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add some relationships to make it more realistic
|
||||
if (i > 0 && i % 10 == 0)
|
||||
{
|
||||
var relationship = new JsonObject
|
||||
{
|
||||
["@type"] = "Relationship",
|
||||
["@id"] = $"https://stellaops.org/spdx/rel-{i:D5}",
|
||||
["spdxId"] = $"https://stellaops.org/spdx/rel-{i:D5}",
|
||||
["relationshipType"] = "dependsOn",
|
||||
["from"] = $"https://stellaops.org/spdx/pkg-{i:D5}",
|
||||
["to"] = new JsonArray { $"https://stellaops.org/spdx/pkg-{i - 1:D5}" }
|
||||
};
|
||||
graph.Add(relationship);
|
||||
}
|
||||
|
||||
graph.Add(pkg);
|
||||
}
|
||||
|
||||
var root = new JsonObject
|
||||
{
|
||||
["@context"] = "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
||||
["@graph"] = graph
|
||||
};
|
||||
|
||||
var filePath = Path.Combine(_tempDir, $"spdx3-{packageCount}-elements.json");
|
||||
var json = JsonSerializer.Serialize(root, new JsonSerializerOptions { WriteIndented = false });
|
||||
File.WriteAllText(filePath, json);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
|
||||
// Clean up temp files
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_tempDir))
|
||||
{
|
||||
Directory.Delete(_tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/__Libraries/__Tests/StellaOps.Spdx3.Tests/TASKS.md
Normal file
10
src/__Libraries/__Tests/StellaOps.Spdx3.Tests/TASKS.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# StellaOps.Spdx3.Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0038-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0038-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0038-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
29
src/__Libraries/__Tests/StellaOps.TestKit.Tests/AGENTS.md
Normal file
29
src/__Libraries/__Tests/StellaOps.TestKit.Tests/AGENTS.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# StellaOps.TestKit.Tests - Local Agent Charter
|
||||
|
||||
## Roles
|
||||
- Backend developer
|
||||
- QA automation engineer
|
||||
|
||||
## Working directory
|
||||
- src/__Libraries/__Tests/StellaOps.TestKit.Tests
|
||||
|
||||
## Allowed dependencies
|
||||
- src/__Libraries/StellaOps.TestKit
|
||||
- src/__Libraries/StellaOps.Canonical.Json
|
||||
- src/__Tests/__Libraries/StellaOps.Testing.Determinism
|
||||
|
||||
## Required reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/cli/contracts/output-determinism.md
|
||||
|
||||
## Determinism and test rules
|
||||
- Use deterministic inputs; avoid DateTime.UtcNow, DateTimeOffset.UtcNow, Guid.NewGuid, and Random.Shared in tests.
|
||||
- Use TimeProvider and fixed seeds or fixtures for time- and randomness-dependent tests.
|
||||
- Use CultureInfo.InvariantCulture for parsing and formatting in tests.
|
||||
- Tag tests with TestCategories (Unit, Integration, Performance) and keep integration tests out of unit-only runs.
|
||||
|
||||
## Quality and safety
|
||||
- ASCII-only strings and comments unless explicitly justified.
|
||||
- Keep fixtures small and deterministic; clean up temp resources.
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Testing.Determinism;
|
||||
@@ -132,7 +133,7 @@ public sealed class DeterminismManifestTests
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest() with { SchemaVersion = "2.0" };
|
||||
var bytes = DeterminismManifestWriter.ToCanonicalBytes(manifest);
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(manifest);
|
||||
|
||||
// Act
|
||||
Action act = () => DeterminismManifestReader.FromBytes(bytes);
|
||||
|
||||
10
src/__Libraries/__Tests/StellaOps.TestKit.Tests/TASKS.md
Normal file
10
src/__Libraries/__Tests/StellaOps.TestKit.Tests/TASKS.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# StellaOps.TestKit.Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0041-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0041-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0041-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
@@ -0,0 +1,29 @@
|
||||
# StellaOps.Testing.Determinism.Tests - Local Agent Charter
|
||||
|
||||
## Roles
|
||||
- Backend developer
|
||||
- QA automation engineer
|
||||
|
||||
## Working directory
|
||||
- src/__Libraries/__Tests/StellaOps.Testing.Determinism.Tests
|
||||
|
||||
## Allowed dependencies
|
||||
- src/__Tests/__Libraries/StellaOps.Testing.Determinism
|
||||
- src/__Libraries/StellaOps.TestKit
|
||||
|
||||
## Required reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/cli/contracts/output-determinism.md
|
||||
- docs/modules/policy/design/policy-determinism-tests.md
|
||||
|
||||
## Determinism and test rules
|
||||
- Use deterministic inputs; avoid DateTime.UtcNow, DateTimeOffset.UtcNow, Guid.NewGuid, and Random.Shared in tests.
|
||||
- Use TimeProvider and fixed seeds or fixtures for time- and randomness-dependent tests.
|
||||
- Use CultureInfo.InvariantCulture for parsing and formatting in tests.
|
||||
- Tag tests with TestCategories (Unit, Integration, Performance) and keep integration tests out of unit-only runs.
|
||||
|
||||
## Quality and safety
|
||||
- ASCII-only strings and comments unless explicitly justified.
|
||||
- Keep fixtures small and deterministic; clean up temp resources.
|
||||
@@ -0,0 +1,10 @@
|
||||
# StellaOps.Testing.Determinism.Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0039-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0039-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0039-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
@@ -0,0 +1,29 @@
|
||||
# StellaOps.Testing.Manifests.Tests - Local Agent Charter
|
||||
|
||||
## Roles
|
||||
- Backend developer
|
||||
- QA automation engineer
|
||||
|
||||
## Working directory
|
||||
- src/__Libraries/__Tests/StellaOps.Testing.Manifests.Tests
|
||||
|
||||
## Allowed dependencies
|
||||
- src/__Tests/__Libraries/StellaOps.Testing.Manifests
|
||||
- src/__Libraries/StellaOps.TestKit
|
||||
|
||||
## Required reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/replay/architecture.md
|
||||
- docs/modules/replay/guides/replay-manifest-guide.md
|
||||
|
||||
## Determinism and test rules
|
||||
- Use deterministic inputs; avoid DateTime.UtcNow, DateTimeOffset.UtcNow, Guid.NewGuid, and Random.Shared in tests.
|
||||
- Use TimeProvider and fixed seeds or fixtures for time- and randomness-dependent tests.
|
||||
- Use CultureInfo.InvariantCulture for parsing and formatting in tests.
|
||||
- Tag tests with TestCategories (Unit, Integration, Performance) and keep integration tests out of unit-only runs.
|
||||
|
||||
## Quality and safety
|
||||
- ASCII-only strings and comments unless explicitly justified.
|
||||
- Keep fixtures small and deterministic; clean up temp resources.
|
||||
@@ -0,0 +1,10 @@
|
||||
# StellaOps.Testing.Manifests.Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0040-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0040-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0040-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
@@ -0,0 +1,27 @@
|
||||
# StellaOps.VersionComparison.Tests - Local Agent Charter
|
||||
|
||||
## Roles
|
||||
- Backend developer
|
||||
- QA automation engineer
|
||||
|
||||
## Working directory
|
||||
- src/__Libraries/__Tests/StellaOps.VersionComparison.Tests
|
||||
|
||||
## Allowed dependencies
|
||||
- src/__Libraries/StellaOps.VersionComparison
|
||||
- src/__Libraries/StellaOps.TestKit
|
||||
|
||||
## Required reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
|
||||
## Determinism and test rules
|
||||
- Use deterministic inputs; avoid DateTime.UtcNow, DateTimeOffset.UtcNow, Guid.NewGuid, and Random.Shared in tests.
|
||||
- Use TimeProvider and fixed seeds or fixtures for time- and randomness-dependent tests.
|
||||
- Use CultureInfo.InvariantCulture for parsing and formatting in tests.
|
||||
- Tag tests with TestCategories (Unit, Integration, Performance) and keep integration tests out of unit-only runs.
|
||||
|
||||
## Quality and safety
|
||||
- ASCII-only strings and comments unless explicitly justified.
|
||||
- Keep fixtures small and deterministic; clean up temp resources.
|
||||
@@ -0,0 +1,10 @@
|
||||
# StellaOps.VersionComparison.Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0042-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0042-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0042-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
Reference in New Issue
Block a user