stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
// <copyright file="AiAttestationServiceTests.ClaimAttestation.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiAttestationServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CreateClaimAttestation_CreatesAndRetrievesClaimAsync()
|
||||
{
|
||||
var claimAttestation = CreateSampleClaimAttestation();
|
||||
|
||||
var result = await _service.CreateClaimAttestationAsync(claimAttestation);
|
||||
|
||||
result.AttestationId.Should().Be(claimAttestation.ClaimId);
|
||||
result.Digest.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetClaimAttestations_ReturnsClaimsForRunAsync()
|
||||
{
|
||||
var runId = "run-with-claims";
|
||||
var claim1 = CreateSampleClaimAttestation() with { ClaimId = "claim-1", RunId = runId };
|
||||
var claim2 = CreateSampleClaimAttestation() with { ClaimId = "claim-2", RunId = runId };
|
||||
var claim3 = CreateSampleClaimAttestation() with { ClaimId = "claim-3", RunId = "other-run" };
|
||||
|
||||
await _service.CreateClaimAttestationAsync(claim1);
|
||||
await _service.CreateClaimAttestationAsync(claim2);
|
||||
await _service.CreateClaimAttestationAsync(claim3);
|
||||
|
||||
var claims = await _service.GetClaimAttestationsAsync(runId);
|
||||
|
||||
claims.Should().HaveCount(2);
|
||||
claims.Should().AllSatisfy(c => c.RunId.Should().Be(runId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyClaimAttestation_ValidClaim_ReturnsValidAsync()
|
||||
{
|
||||
var claimAttestation = CreateSampleClaimAttestation();
|
||||
await _service.CreateClaimAttestationAsync(claimAttestation, sign: true);
|
||||
|
||||
var result = await _service.VerifyClaimAttestationAsync(claimAttestation.ClaimId);
|
||||
|
||||
result.Valid.Should().BeTrue();
|
||||
result.DigestValid.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// <copyright file="AiAttestationServiceTests.ListRecent.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiAttestationServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ListRecentAttestations_FiltersByTenantAsync()
|
||||
{
|
||||
var tenant1Run = CreateSampleRunAttestation() with { RunId = "run-t1", TenantId = "tenant-1" };
|
||||
var tenant2Run = CreateSampleRunAttestation() with { RunId = "run-t2", TenantId = "tenant-2" };
|
||||
|
||||
await _service.CreateRunAttestationAsync(tenant1Run);
|
||||
await _service.CreateRunAttestationAsync(tenant2Run);
|
||||
|
||||
var tenant1Attestations = await _service.ListRecentAttestationsAsync("tenant-1");
|
||||
|
||||
tenant1Attestations.Should().HaveCount(1);
|
||||
tenant1Attestations[0].TenantId.Should().Be("tenant-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListRecentAttestations_RespectsLimitAsync()
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var attestation = CreateSampleRunAttestation() with
|
||||
{
|
||||
RunId = $"run-{i}",
|
||||
TenantId = "tenant-test"
|
||||
};
|
||||
await _service.CreateRunAttestationAsync(attestation);
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
var recent = await _service.ListRecentAttestationsAsync("tenant-test", limit: 5);
|
||||
|
||||
recent.Should().HaveCount(5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// <copyright file="AiAttestationServiceTests.RunAttestation.Create.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiAttestationServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CreateRunAttestation_WithSigning_ReturnsSignedResultAsync()
|
||||
{
|
||||
var attestation = CreateSampleRunAttestation();
|
||||
|
||||
var result = await _service.CreateRunAttestationAsync(attestation, sign: true);
|
||||
|
||||
result.AttestationId.Should().Be(attestation.RunId);
|
||||
result.Signed.Should().BeTrue();
|
||||
result.DsseEnvelope.Should().NotBeNullOrEmpty();
|
||||
result.Digest.Should().StartWith("sha256:");
|
||||
result.StorageUri.Should().Contain(attestation.RunId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateRunAttestation_WithoutSigning_ReturnsUnsignedResultAsync()
|
||||
{
|
||||
var attestation = CreateSampleRunAttestation();
|
||||
|
||||
var result = await _service.CreateRunAttestationAsync(attestation, sign: false);
|
||||
|
||||
result.Signed.Should().BeFalse();
|
||||
result.DsseEnvelope.Should().BeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// <copyright file="AiAttestationServiceTests.RunAttestation.Read.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiAttestationServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetRunAttestation_AfterCreation_ReturnsAttestationAsync()
|
||||
{
|
||||
var attestation = CreateSampleRunAttestation();
|
||||
await _service.CreateRunAttestationAsync(attestation);
|
||||
|
||||
var retrieved = await _service.GetRunAttestationAsync(attestation.RunId);
|
||||
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.RunId.Should().Be(attestation.RunId);
|
||||
retrieved.TenantId.Should().Be(attestation.TenantId);
|
||||
retrieved.Model.Provider.Should().Be(attestation.Model.Provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRunAttestation_NotFound_ReturnsNullAsync()
|
||||
{
|
||||
var result = await _service.GetRunAttestationAsync("non-existent");
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// <copyright file="AiAttestationServiceTests.RunAttestation.Verify.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiAttestationServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task VerifyRunAttestation_ValidAttestation_ReturnsValidAsync()
|
||||
{
|
||||
var attestation = CreateSampleRunAttestation();
|
||||
await _service.CreateRunAttestationAsync(attestation, sign: true);
|
||||
|
||||
var result = await _service.VerifyRunAttestationAsync(attestation.RunId);
|
||||
|
||||
result.Valid.Should().BeTrue();
|
||||
result.DigestValid.Should().BeTrue();
|
||||
result.SignatureValid.Should().BeTrue();
|
||||
result.SigningKeyId.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyRunAttestation_NotFound_ReturnsInvalidAsync()
|
||||
{
|
||||
var result = await _service.VerifyRunAttestationAsync("non-existent");
|
||||
|
||||
result.Valid.Should().BeFalse();
|
||||
result.FailureReason.Should().Contain("not found");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// <copyright file="AiAttestationServiceTests.TimeProvider.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiAttestationServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CreatedAt_UsesTimeProviderAsync()
|
||||
{
|
||||
var fixedTime = FixedUtcNow.AddHours(1);
|
||||
_timeProvider.SetUtcNow(fixedTime);
|
||||
|
||||
var attestation = CreateSampleRunAttestation();
|
||||
var result = await _service.CreateRunAttestationAsync(attestation);
|
||||
|
||||
result.CreatedAt.Should().Be(fixedTime);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
// <copyright file="AiAttestationServiceTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.AdvisoryAI.Attestation;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using Xunit;
|
||||
|
||||
@@ -15,184 +13,29 @@ namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
/// Tests for <see cref="AiAttestationService"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class AiAttestationServiceTests
|
||||
public sealed partial class AiAttestationServiceTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedUtcNow = new(2026, 1, 9, 12, 0, 0, TimeSpan.Zero);
|
||||
private const string DefaultRunId = "run-001";
|
||||
private const string DefaultClaimId = "claim-001";
|
||||
private readonly FakeTimeProvider _timeProvider = new();
|
||||
private readonly AiAttestationService _service;
|
||||
|
||||
public AiAttestationServiceTests()
|
||||
{
|
||||
_service = new AiAttestationService(
|
||||
_timeProvider,
|
||||
NullLogger<AiAttestationService>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateRunAttestationAsync_WithSigning_ReturnsSignedResult()
|
||||
{
|
||||
var attestation = CreateSampleRunAttestation();
|
||||
|
||||
var result = await _service.CreateRunAttestationAsync(attestation, sign: true);
|
||||
|
||||
result.AttestationId.Should().Be(attestation.RunId);
|
||||
result.Signed.Should().BeTrue();
|
||||
result.DsseEnvelope.Should().NotBeNullOrEmpty();
|
||||
result.Digest.Should().StartWith("sha256:");
|
||||
result.StorageUri.Should().Contain(attestation.RunId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateRunAttestationAsync_WithoutSigning_ReturnsUnsignedResult()
|
||||
{
|
||||
var attestation = CreateSampleRunAttestation();
|
||||
|
||||
var result = await _service.CreateRunAttestationAsync(attestation, sign: false);
|
||||
|
||||
result.Signed.Should().BeFalse();
|
||||
result.DsseEnvelope.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRunAttestationAsync_AfterCreation_ReturnsAttestation()
|
||||
{
|
||||
var attestation = CreateSampleRunAttestation();
|
||||
await _service.CreateRunAttestationAsync(attestation);
|
||||
|
||||
var retrieved = await _service.GetRunAttestationAsync(attestation.RunId);
|
||||
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.RunId.Should().Be(attestation.RunId);
|
||||
retrieved.TenantId.Should().Be(attestation.TenantId);
|
||||
retrieved.Model.Provider.Should().Be(attestation.Model.Provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRunAttestationAsync_NotFound_ReturnsNull()
|
||||
{
|
||||
var result = await _service.GetRunAttestationAsync("non-existent");
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyRunAttestationAsync_ValidAttestation_ReturnsValid()
|
||||
{
|
||||
var attestation = CreateSampleRunAttestation();
|
||||
await _service.CreateRunAttestationAsync(attestation, sign: true);
|
||||
|
||||
var result = await _service.VerifyRunAttestationAsync(attestation.RunId);
|
||||
|
||||
result.Valid.Should().BeTrue();
|
||||
result.DigestValid.Should().BeTrue();
|
||||
result.SignatureValid.Should().BeTrue();
|
||||
result.SigningKeyId.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyRunAttestationAsync_NotFound_ReturnsInvalid()
|
||||
{
|
||||
var result = await _service.VerifyRunAttestationAsync("non-existent");
|
||||
|
||||
result.Valid.Should().BeFalse();
|
||||
result.FailureReason.Should().Contain("not found");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateClaimAttestationAsync_CreatesAndRetrievesClaim()
|
||||
{
|
||||
var claimAttestation = CreateSampleClaimAttestation();
|
||||
|
||||
var result = await _service.CreateClaimAttestationAsync(claimAttestation);
|
||||
|
||||
result.AttestationId.Should().Be(claimAttestation.ClaimId);
|
||||
result.Digest.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetClaimAttestationsAsync_ReturnsClaimsForRun()
|
||||
{
|
||||
var runId = "run-with-claims";
|
||||
var claim1 = CreateSampleClaimAttestation() with { ClaimId = "claim-1", RunId = runId };
|
||||
var claim2 = CreateSampleClaimAttestation() with { ClaimId = "claim-2", RunId = runId };
|
||||
var claim3 = CreateSampleClaimAttestation() with { ClaimId = "claim-3", RunId = "other-run" };
|
||||
|
||||
await _service.CreateClaimAttestationAsync(claim1);
|
||||
await _service.CreateClaimAttestationAsync(claim2);
|
||||
await _service.CreateClaimAttestationAsync(claim3);
|
||||
|
||||
var claims = await _service.GetClaimAttestationsAsync(runId);
|
||||
|
||||
claims.Should().HaveCount(2);
|
||||
claims.Should().AllSatisfy(c => c.RunId.Should().Be(runId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyClaimAttestationAsync_ValidClaim_ReturnsValid()
|
||||
{
|
||||
var claimAttestation = CreateSampleClaimAttestation();
|
||||
await _service.CreateClaimAttestationAsync(claimAttestation, sign: true);
|
||||
|
||||
var result = await _service.VerifyClaimAttestationAsync(claimAttestation.ClaimId);
|
||||
|
||||
result.Valid.Should().BeTrue();
|
||||
result.DigestValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListRecentAttestationsAsync_FiltersByTenant()
|
||||
{
|
||||
var tenant1Run = CreateSampleRunAttestation() with { RunId = "run-t1", TenantId = "tenant-1" };
|
||||
var tenant2Run = CreateSampleRunAttestation() with { RunId = "run-t2", TenantId = "tenant-2" };
|
||||
|
||||
await _service.CreateRunAttestationAsync(tenant1Run);
|
||||
await _service.CreateRunAttestationAsync(tenant2Run);
|
||||
|
||||
var tenant1Attestations = await _service.ListRecentAttestationsAsync("tenant-1");
|
||||
|
||||
tenant1Attestations.Should().HaveCount(1);
|
||||
tenant1Attestations[0].TenantId.Should().Be("tenant-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListRecentAttestationsAsync_RespectsLimit()
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var attestation = CreateSampleRunAttestation() with
|
||||
{
|
||||
RunId = $"run-{i}",
|
||||
TenantId = "tenant-test"
|
||||
};
|
||||
await _service.CreateRunAttestationAsync(attestation);
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
var recent = await _service.ListRecentAttestationsAsync("tenant-test", limit: 5);
|
||||
|
||||
recent.Should().HaveCount(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreatedAt_UsesTimeProvider()
|
||||
{
|
||||
var fixedTime = new DateTimeOffset(2026, 1, 9, 12, 0, 0, TimeSpan.Zero);
|
||||
_timeProvider.SetUtcNow(fixedTime);
|
||||
|
||||
var attestation = CreateSampleRunAttestation();
|
||||
var result = await _service.CreateRunAttestationAsync(attestation);
|
||||
|
||||
result.CreatedAt.Should().Be(fixedTime);
|
||||
_timeProvider.SetUtcNow(FixedUtcNow);
|
||||
_service = new AiAttestationService(_timeProvider, NullLogger<AiAttestationService>.Instance);
|
||||
}
|
||||
|
||||
private static AiRunAttestation CreateSampleRunAttestation()
|
||||
{
|
||||
return new AiRunAttestation
|
||||
{
|
||||
RunId = $"run-{Guid.NewGuid():N}",
|
||||
RunId = DefaultRunId,
|
||||
TenantId = "tenant-test",
|
||||
UserId = "user:test@example.com",
|
||||
StartedAt = DateTimeOffset.Parse("2026-01-09T12:00:00Z"),
|
||||
CompletedAt = DateTimeOffset.Parse("2026-01-09T12:05:00Z"),
|
||||
StartedAt = FixedUtcNow,
|
||||
CompletedAt = FixedUtcNow.AddMinutes(5),
|
||||
Model = new AiModelInfo
|
||||
{
|
||||
Provider = "anthropic",
|
||||
@@ -207,14 +50,14 @@ public class AiAttestationServiceTests
|
||||
{
|
||||
return new AiClaimAttestation
|
||||
{
|
||||
ClaimId = $"claim-{Guid.NewGuid():N}",
|
||||
RunId = "run-xyz",
|
||||
ClaimId = DefaultClaimId,
|
||||
RunId = DefaultRunId,
|
||||
TurnId = "turn-001",
|
||||
TenantId = "tenant-test",
|
||||
ClaimText = "Test claim",
|
||||
ClaimDigest = "sha256:test",
|
||||
GroundingScore = 0.85,
|
||||
Timestamp = DateTimeOffset.Parse("2026-01-09T12:00:00Z"),
|
||||
Timestamp = FixedUtcNow,
|
||||
ContentDigest = "sha256:content-test"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// <copyright file="AiClaimAttestationTests.Digest.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiClaimAttestationTests
|
||||
{
|
||||
[Fact]
|
||||
public void ComputeDigest_SameClaim_ReturnsSameDigest()
|
||||
{
|
||||
var attestation = CreateSampleClaimAttestation();
|
||||
|
||||
var digest1 = attestation.ComputeDigest();
|
||||
var digest2 = attestation.ComputeDigest();
|
||||
|
||||
digest1.Should().Be(digest2);
|
||||
digest1.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDigest_DifferentClaims_ReturnsDifferentDigests()
|
||||
{
|
||||
var attestation1 = CreateSampleClaimAttestation();
|
||||
var attestation2 = attestation1 with { ClaimText = "Different claim text" };
|
||||
|
||||
var digest1 = attestation1.ComputeDigest();
|
||||
var digest2 = attestation2.ComputeDigest();
|
||||
|
||||
digest1.Should().NotBe(digest2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// <copyright file="AiClaimAttestationTests.Evidence.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiClaimAttestationTests
|
||||
{
|
||||
[Fact]
|
||||
public void FromClaimEvidence_CreatesValidAttestation()
|
||||
{
|
||||
var evidence = new ClaimEvidence
|
||||
{
|
||||
Text = "This component is affected by the vulnerability",
|
||||
Position = 45,
|
||||
Length = 47,
|
||||
GroundedBy = ["stella://sbom/abc123", "stella://reach/api:func"],
|
||||
GroundingScore = 0.95,
|
||||
Verified = true,
|
||||
Category = ClaimCategory.Factual
|
||||
};
|
||||
|
||||
var attestation = AiClaimAttestation.FromClaimEvidence(
|
||||
evidence,
|
||||
runId: "run-123",
|
||||
turnId: "turn-456",
|
||||
tenantId: "tenant-xyz",
|
||||
timestamp: FixedUtcNow);
|
||||
|
||||
attestation.ClaimText.Should().Be(evidence.Text);
|
||||
attestation.RunId.Should().Be("run-123");
|
||||
attestation.TurnId.Should().Be("turn-456");
|
||||
attestation.TenantId.Should().Be("tenant-xyz");
|
||||
attestation.GroundedBy.Should().HaveCount(2);
|
||||
attestation.GroundingScore.Should().Be(0.95);
|
||||
attestation.Verified.Should().BeTrue();
|
||||
attestation.Category.Should().Be(ClaimCategory.Factual);
|
||||
attestation.ClaimDigest.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClaimDigest_IsDeterministic()
|
||||
{
|
||||
var evidence1 = new ClaimEvidence
|
||||
{
|
||||
Text = "Same text",
|
||||
Position = 0,
|
||||
Length = 9,
|
||||
GroundingScore = 0.9
|
||||
};
|
||||
|
||||
var evidence2 = new ClaimEvidence
|
||||
{
|
||||
Text = "Same text",
|
||||
Position = 100,
|
||||
Length = 9,
|
||||
GroundingScore = 0.5
|
||||
};
|
||||
|
||||
var attestation1 = AiClaimAttestation.FromClaimEvidence(
|
||||
evidence1,
|
||||
"run-1",
|
||||
"turn-1",
|
||||
"tenant-1",
|
||||
FixedUtcNow);
|
||||
var attestation2 = AiClaimAttestation.FromClaimEvidence(
|
||||
evidence2,
|
||||
"run-2",
|
||||
"turn-2",
|
||||
"tenant-2",
|
||||
FixedUtcNow);
|
||||
|
||||
attestation1.ClaimDigest.Should().Be(attestation2.ClaimDigest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// <copyright file="AiClaimAttestationTests.GroundedBy.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiClaimAttestationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Attestation_WithGrounding_PreservesEvidenceUris()
|
||||
{
|
||||
var groundedBy = ImmutableArray.Create(
|
||||
"stella://sbom/abc123",
|
||||
"stella://reach/api:vulnFunc",
|
||||
"stella://vex/CVE-2023-44487");
|
||||
|
||||
var attestation = CreateSampleClaimAttestation() with
|
||||
{
|
||||
GroundedBy = groundedBy
|
||||
};
|
||||
|
||||
attestation.GroundedBy.Should().HaveCount(3);
|
||||
attestation.GroundedBy.Should().Contain("stella://sbom/abc123");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// <copyright file="AiClaimAttestationTests.Predicate.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiClaimAttestationTests
|
||||
{
|
||||
[Fact]
|
||||
public void PredicateType_IsCorrect()
|
||||
{
|
||||
AiClaimAttestation.PredicateType.Should().Be("https://stellaops.org/attestation/ai-claim/v1");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
// <copyright file="AiClaimAttestationTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using Xunit;
|
||||
|
||||
@@ -13,114 +10,9 @@ namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
/// Tests for <see cref="AiClaimAttestation"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class AiClaimAttestationTests
|
||||
public sealed partial class AiClaimAttestationTests
|
||||
{
|
||||
[Fact]
|
||||
public void PredicateType_IsCorrect()
|
||||
{
|
||||
AiClaimAttestation.PredicateType.Should().Be("https://stellaops.org/attestation/ai-claim/v1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDigest_SameClaim_ReturnsSameDigest()
|
||||
{
|
||||
var attestation = CreateSampleClaimAttestation();
|
||||
|
||||
var digest1 = attestation.ComputeDigest();
|
||||
var digest2 = attestation.ComputeDigest();
|
||||
|
||||
digest1.Should().Be(digest2);
|
||||
digest1.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDigest_DifferentClaims_ReturnsDifferentDigests()
|
||||
{
|
||||
var attestation1 = CreateSampleClaimAttestation();
|
||||
var attestation2 = attestation1 with { ClaimText = "Different claim text" };
|
||||
|
||||
var digest1 = attestation1.ComputeDigest();
|
||||
var digest2 = attestation2.ComputeDigest();
|
||||
|
||||
digest1.Should().NotBe(digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromClaimEvidence_CreatesValidAttestation()
|
||||
{
|
||||
var evidence = new ClaimEvidence
|
||||
{
|
||||
Text = "This component is affected by the vulnerability",
|
||||
Position = 45,
|
||||
Length = 47,
|
||||
GroundedBy = ["stella://sbom/abc123", "stella://reach/api:func"],
|
||||
GroundingScore = 0.95,
|
||||
Verified = true,
|
||||
Category = ClaimCategory.Factual
|
||||
};
|
||||
|
||||
var attestation = AiClaimAttestation.FromClaimEvidence(
|
||||
evidence,
|
||||
runId: "run-123",
|
||||
turnId: "turn-456",
|
||||
tenantId: "tenant-xyz",
|
||||
timestamp: DateTimeOffset.Parse("2026-01-09T12:00:00Z"));
|
||||
|
||||
attestation.ClaimText.Should().Be(evidence.Text);
|
||||
attestation.RunId.Should().Be("run-123");
|
||||
attestation.TurnId.Should().Be("turn-456");
|
||||
attestation.TenantId.Should().Be("tenant-xyz");
|
||||
attestation.GroundedBy.Should().HaveCount(2);
|
||||
attestation.GroundingScore.Should().Be(0.95);
|
||||
attestation.Verified.Should().BeTrue();
|
||||
attestation.Category.Should().Be(ClaimCategory.Factual);
|
||||
attestation.ClaimDigest.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClaimDigest_IsDeterministic()
|
||||
{
|
||||
var evidence1 = new ClaimEvidence
|
||||
{
|
||||
Text = "Same text",
|
||||
Position = 0,
|
||||
Length = 9,
|
||||
GroundingScore = 0.9
|
||||
};
|
||||
|
||||
var evidence2 = new ClaimEvidence
|
||||
{
|
||||
Text = "Same text",
|
||||
Position = 100, // Different position
|
||||
Length = 9,
|
||||
GroundingScore = 0.5 // Different score
|
||||
};
|
||||
|
||||
var attestation1 = AiClaimAttestation.FromClaimEvidence(
|
||||
evidence1, "run-1", "turn-1", "tenant-1", DateTimeOffset.UtcNow);
|
||||
var attestation2 = AiClaimAttestation.FromClaimEvidence(
|
||||
evidence2, "run-2", "turn-2", "tenant-2", DateTimeOffset.UtcNow);
|
||||
|
||||
// ClaimDigest should be same because it's based on text only
|
||||
attestation1.ClaimDigest.Should().Be(attestation2.ClaimDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Attestation_WithGrounding_PreservesEvidenceUris()
|
||||
{
|
||||
var groundedBy = ImmutableArray.Create(
|
||||
"stella://sbom/abc123",
|
||||
"stella://reach/api:vulnFunc",
|
||||
"stella://vex/CVE-2023-44487");
|
||||
|
||||
var attestation = CreateSampleClaimAttestation() with
|
||||
{
|
||||
GroundedBy = groundedBy
|
||||
};
|
||||
|
||||
attestation.GroundedBy.Should().HaveCount(3);
|
||||
attestation.GroundedBy.Should().Contain("stella://sbom/abc123");
|
||||
}
|
||||
private static readonly DateTimeOffset FixedUtcNow = new(2026, 1, 9, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static AiClaimAttestation CreateSampleClaimAttestation()
|
||||
{
|
||||
@@ -136,7 +28,7 @@ public class AiClaimAttestationTests
|
||||
GroundedBy = ["stella://sbom/test"],
|
||||
GroundingScore = 0.85,
|
||||
Verified = true,
|
||||
Timestamp = DateTimeOffset.Parse("2026-01-09T12:00:00Z"),
|
||||
Timestamp = FixedUtcNow,
|
||||
ContentDigest = "sha256:content123"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// <copyright file="AiRunAttestationTests.Context.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiRunAttestationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Attestation_WithContext_PreservesContext()
|
||||
{
|
||||
var context = new AiRunContext
|
||||
{
|
||||
FindingId = "finding-123",
|
||||
CveId = "CVE-2023-44487",
|
||||
Component = "pkg:npm/http2@1.0.0"
|
||||
};
|
||||
|
||||
var attestation = CreateSampleAttestation() with { Context = context };
|
||||
|
||||
attestation.Context.Should().NotBeNull();
|
||||
attestation.Context!.FindingId.Should().Be("finding-123");
|
||||
attestation.Context.CveId.Should().Be("CVE-2023-44487");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// <copyright file="AiRunAttestationTests.Digest.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiRunAttestationTests
|
||||
{
|
||||
[Fact]
|
||||
public void ComputeDigest_SameAttestation_ReturnsSameDigest()
|
||||
{
|
||||
var attestation = CreateSampleAttestation();
|
||||
|
||||
var digest1 = attestation.ComputeDigest();
|
||||
var digest2 = attestation.ComputeDigest();
|
||||
|
||||
digest1.Should().Be(digest2);
|
||||
digest1.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDigest_DifferentAttestations_ReturnsDifferentDigests()
|
||||
{
|
||||
var attestation1 = CreateSampleAttestation();
|
||||
var attestation2 = attestation1 with { RunId = "run-different" };
|
||||
|
||||
var digest1 = attestation1.ComputeDigest();
|
||||
var digest2 = attestation2.ComputeDigest();
|
||||
|
||||
digest1.Should().NotBe(digest2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// <copyright file="AiRunAttestationTests.PredicateAndStatus.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiRunAttestationTests
|
||||
{
|
||||
[Fact]
|
||||
public void PredicateType_IsCorrect()
|
||||
{
|
||||
AiRunAttestation.PredicateType.Should().Be("https://stellaops.org/attestation/ai-run/v1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Attestation_DefaultStatus_IsCompleted()
|
||||
{
|
||||
var attestation = CreateSampleAttestation();
|
||||
|
||||
attestation.Status.Should().Be(AiRunStatus.Completed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// <copyright file="AiRunAttestationTests.Turns.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class AiRunAttestationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Attestation_WithTurns_PreservesOrder()
|
||||
{
|
||||
var turns = new[]
|
||||
{
|
||||
CreateTurn("turn-1", TurnRole.User, FixedUtcNow),
|
||||
CreateTurn("turn-2", TurnRole.Assistant, FixedUtcNow.AddSeconds(5)),
|
||||
CreateTurn("turn-3", TurnRole.User, FixedUtcNow.AddSeconds(10))
|
||||
};
|
||||
|
||||
var attestation = CreateSampleAttestation() with
|
||||
{
|
||||
Turns = [.. turns]
|
||||
};
|
||||
|
||||
attestation.Turns.Should().HaveCount(3);
|
||||
attestation.Turns[0].TurnId.Should().Be("turn-1");
|
||||
attestation.Turns[1].TurnId.Should().Be("turn-2");
|
||||
attestation.Turns[2].TurnId.Should().Be("turn-3");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
// <copyright file="AiRunAttestationTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using Xunit;
|
||||
|
||||
@@ -13,83 +10,9 @@ namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
/// Tests for <see cref="AiRunAttestation"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class AiRunAttestationTests
|
||||
public sealed partial class AiRunAttestationTests
|
||||
{
|
||||
[Fact]
|
||||
public void ComputeDigest_SameAttestation_ReturnsSameDigest()
|
||||
{
|
||||
var attestation = CreateSampleAttestation();
|
||||
|
||||
var digest1 = attestation.ComputeDigest();
|
||||
var digest2 = attestation.ComputeDigest();
|
||||
|
||||
digest1.Should().Be(digest2);
|
||||
digest1.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDigest_DifferentAttestations_ReturnsDifferentDigests()
|
||||
{
|
||||
var attestation1 = CreateSampleAttestation();
|
||||
var attestation2 = attestation1 with { RunId = "run-different" };
|
||||
|
||||
var digest1 = attestation1.ComputeDigest();
|
||||
var digest2 = attestation2.ComputeDigest();
|
||||
|
||||
digest1.Should().NotBe(digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PredicateType_IsCorrect()
|
||||
{
|
||||
AiRunAttestation.PredicateType.Should().Be("https://stellaops.org/attestation/ai-run/v1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Attestation_WithTurns_PreservesOrder()
|
||||
{
|
||||
var turns = new[]
|
||||
{
|
||||
CreateTurn("turn-1", TurnRole.User, "2026-01-09T12:00:00Z"),
|
||||
CreateTurn("turn-2", TurnRole.Assistant, "2026-01-09T12:00:05Z"),
|
||||
CreateTurn("turn-3", TurnRole.User, "2026-01-09T12:00:10Z")
|
||||
};
|
||||
|
||||
var attestation = CreateSampleAttestation() with
|
||||
{
|
||||
Turns = [.. turns]
|
||||
};
|
||||
|
||||
attestation.Turns.Should().HaveCount(3);
|
||||
attestation.Turns[0].TurnId.Should().Be("turn-1");
|
||||
attestation.Turns[1].TurnId.Should().Be("turn-2");
|
||||
attestation.Turns[2].TurnId.Should().Be("turn-3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Attestation_WithContext_PreservesContext()
|
||||
{
|
||||
var context = new AiRunContext
|
||||
{
|
||||
FindingId = "finding-123",
|
||||
CveId = "CVE-2023-44487",
|
||||
Component = "pkg:npm/http2@1.0.0"
|
||||
};
|
||||
|
||||
var attestation = CreateSampleAttestation() with { Context = context };
|
||||
|
||||
attestation.Context.Should().NotBeNull();
|
||||
attestation.Context!.FindingId.Should().Be("finding-123");
|
||||
attestation.Context.CveId.Should().Be("CVE-2023-44487");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Attestation_DefaultStatus_IsCompleted()
|
||||
{
|
||||
var attestation = CreateSampleAttestation();
|
||||
|
||||
attestation.Status.Should().Be(AiRunStatus.Completed);
|
||||
}
|
||||
private static readonly DateTimeOffset FixedUtcNow = new(2026, 1, 9, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static AiRunAttestation CreateSampleAttestation()
|
||||
{
|
||||
@@ -98,8 +21,8 @@ public class AiRunAttestationTests
|
||||
RunId = "run-abc123",
|
||||
TenantId = "tenant-xyz",
|
||||
UserId = "user:alice@example.com",
|
||||
StartedAt = DateTimeOffset.Parse("2026-01-09T12:00:00Z"),
|
||||
CompletedAt = DateTimeOffset.Parse("2026-01-09T12:05:00Z"),
|
||||
StartedAt = FixedUtcNow,
|
||||
CompletedAt = FixedUtcNow.AddMinutes(5),
|
||||
Model = new AiModelInfo
|
||||
{
|
||||
Provider = "anthropic",
|
||||
@@ -110,14 +33,14 @@ public class AiRunAttestationTests
|
||||
};
|
||||
}
|
||||
|
||||
private static AiTurnSummary CreateTurn(string turnId, TurnRole role, string timestamp)
|
||||
private static AiTurnSummary CreateTurn(string turnId, TurnRole role, DateTimeOffset timestamp)
|
||||
{
|
||||
return new AiTurnSummary
|
||||
{
|
||||
TurnId = turnId,
|
||||
Role = role,
|
||||
ContentDigest = $"sha256:turn-{turnId}",
|
||||
Timestamp = DateTimeOffset.Parse(timestamp),
|
||||
Timestamp = timestamp,
|
||||
TokenCount = 100
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// <copyright file="InMemoryAiAttestationStoreTests.ClaimAttestations.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class InMemoryAiAttestationStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StoreClaimAttestation_ThenRetrieve_WorksAsync()
|
||||
{
|
||||
var claim = CreateClaimAttestation("run-1", "turn-1");
|
||||
|
||||
await _store.StoreClaimAttestationAsync(claim, NoCancellation);
|
||||
|
||||
var claims = await _store.GetClaimAttestationsAsync("run-1", NoCancellation);
|
||||
|
||||
claims.Should().HaveCount(1);
|
||||
claims[0].TurnId.Should().Be("turn-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetClaimAttestations_MultiplePerRun_ReturnsAllAsync()
|
||||
{
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-1"), NoCancellation);
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-2"), NoCancellation);
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-3"), NoCancellation);
|
||||
|
||||
var claims = await _store.GetClaimAttestationsAsync("run-1", NoCancellation);
|
||||
|
||||
claims.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetClaimAttestationsByTurn_FiltersCorrectlyAsync()
|
||||
{
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-1"), NoCancellation);
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-2"), NoCancellation);
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-1"), NoCancellation);
|
||||
|
||||
var turn1Claims = await _store.GetClaimAttestationsByTurnAsync("run-1", "turn-1", NoCancellation);
|
||||
|
||||
turn1Claims.Should().HaveCount(2);
|
||||
turn1Claims.Should().OnlyContain(c => c.TurnId == "turn-1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// <copyright file="InMemoryAiAttestationStoreTests.Clear.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class InMemoryAiAttestationStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Clear_RemovesAllDataAsync()
|
||||
{
|
||||
await _store.StoreRunAttestationAsync(CreateRunAttestation("run-1"), NoCancellation);
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-1"), NoCancellation);
|
||||
|
||||
_store.Clear();
|
||||
|
||||
_store.RunAttestationCount.Should().Be(0);
|
||||
_store.ClaimAttestationCount.Should().Be(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// <copyright file="InMemoryAiAttestationStoreTests.ContentDigest.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class InMemoryAiAttestationStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetByContentDigest_WorksAsync()
|
||||
{
|
||||
var claim = CreateClaimAttestation("run-1", "turn-1", "sha256:test123");
|
||||
await _store.StoreClaimAttestationAsync(claim, NoCancellation);
|
||||
|
||||
var retrieved = await _store.GetByContentDigestAsync("sha256:test123", NoCancellation);
|
||||
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.ContentDigest.Should().Be("sha256:test123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByContentDigest_NotFound_ReturnsNullAsync()
|
||||
{
|
||||
var retrieved = await _store.GetByContentDigestAsync("sha256:nonexistent", NoCancellation);
|
||||
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// <copyright file="InMemoryAiAttestationStoreTests.Envelopes.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class InMemoryAiAttestationStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StoreSignedEnvelope_ThenRetrieve_WorksAsync()
|
||||
{
|
||||
var envelope = new { Type = "DSSE", Payload = "test" };
|
||||
|
||||
await _store.StoreSignedEnvelopeAsync("run-1", envelope, NoCancellation);
|
||||
|
||||
var retrieved = await _store.GetSignedEnvelopeAsync("run-1", NoCancellation);
|
||||
|
||||
retrieved.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// <copyright file="InMemoryAiAttestationStoreTests.RunAttestations.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class InMemoryAiAttestationStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StoreRunAttestation_ThenRetrieve_WorksAsync()
|
||||
{
|
||||
var attestation = CreateRunAttestation("run-1");
|
||||
|
||||
await _store.StoreRunAttestationAsync(attestation, NoCancellation);
|
||||
|
||||
var retrieved = await _store.GetRunAttestationAsync("run-1", NoCancellation);
|
||||
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.RunId.Should().Be("run-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRunAttestation_NotFound_ReturnsNullAsync()
|
||||
{
|
||||
var retrieved = await _store.GetRunAttestationAsync("non-existent", NoCancellation);
|
||||
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Exists_WhenStored_ReturnsTrueAsync()
|
||||
{
|
||||
await _store.StoreRunAttestationAsync(CreateRunAttestation("run-1"), NoCancellation);
|
||||
|
||||
var exists = await _store.ExistsAsync("run-1", NoCancellation);
|
||||
|
||||
exists.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Exists_WhenNotStored_ReturnsFalseAsync()
|
||||
{
|
||||
var exists = await _store.ExistsAsync("non-existent", NoCancellation);
|
||||
|
||||
exists.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// <copyright file="InMemoryAiAttestationStoreTests.TenantQueries.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class InMemoryAiAttestationStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetByTenant_FiltersCorrectlyAsync()
|
||||
{
|
||||
var now = FixedUtcNow;
|
||||
var att1 = CreateRunAttestation("run-1", "tenant-a", now.AddMinutes(-30));
|
||||
var att2 = CreateRunAttestation("run-2", "tenant-a", now.AddMinutes(-10));
|
||||
var att3 = CreateRunAttestation("run-3", "tenant-b", now.AddMinutes(-20));
|
||||
|
||||
await _store.StoreRunAttestationAsync(att1, NoCancellation);
|
||||
await _store.StoreRunAttestationAsync(att2, NoCancellation);
|
||||
await _store.StoreRunAttestationAsync(att3, NoCancellation);
|
||||
|
||||
var tenantAResults = await _store.GetByTenantAsync(
|
||||
"tenant-a",
|
||||
now.AddHours(-1),
|
||||
now,
|
||||
NoCancellation);
|
||||
|
||||
tenantAResults.Should().HaveCount(2);
|
||||
tenantAResults.Should().OnlyContain(a => a.TenantId == "tenant-a");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByTenant_FiltersTimeRangeCorrectlyAsync()
|
||||
{
|
||||
var now = FixedUtcNow;
|
||||
var att1 = CreateRunAttestation("run-1", "tenant-a", now.AddHours(-3));
|
||||
var att2 = CreateRunAttestation("run-2", "tenant-a", now.AddHours(-1));
|
||||
var att3 = CreateRunAttestation("run-3", "tenant-a", now.AddMinutes(-30));
|
||||
|
||||
await _store.StoreRunAttestationAsync(att1, NoCancellation);
|
||||
await _store.StoreRunAttestationAsync(att2, NoCancellation);
|
||||
await _store.StoreRunAttestationAsync(att3, NoCancellation);
|
||||
|
||||
var results = await _store.GetByTenantAsync(
|
||||
"tenant-a",
|
||||
now.AddHours(-2),
|
||||
now,
|
||||
NoCancellation);
|
||||
|
||||
results.Should().HaveCount(2);
|
||||
results.Should().NotContain(a => a.RunId == "run-1");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
// <copyright file="InMemoryAiAttestationStoreTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using StellaOps.AdvisoryAI.Attestation.Storage;
|
||||
@@ -15,192 +14,33 @@ namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
/// Tests for <see cref="InMemoryAiAttestationStore"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class InMemoryAiAttestationStoreTests
|
||||
public sealed partial class InMemoryAiAttestationStoreTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedUtcNow = new(2026, 1, 9, 12, 0, 0, TimeSpan.Zero);
|
||||
private static readonly CancellationToken NoCancellation = CancellationToken.None;
|
||||
private const string DefaultTenantId = "test-tenant";
|
||||
private readonly InMemoryAiAttestationStore _store;
|
||||
private int _claimCounter;
|
||||
|
||||
public InMemoryAiAttestationStoreTests()
|
||||
{
|
||||
_store = new InMemoryAiAttestationStore(NullLogger<InMemoryAiAttestationStore>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StoreRunAttestation_ThenRetrieve_Works()
|
||||
{
|
||||
var attestation = CreateRunAttestation("run-1");
|
||||
|
||||
await _store.StoreRunAttestationAsync(attestation, CancellationToken.None);
|
||||
|
||||
var retrieved = await _store.GetRunAttestationAsync("run-1", CancellationToken.None);
|
||||
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.RunId.Should().Be("run-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRunAttestation_NotFound_ReturnsNull()
|
||||
{
|
||||
var retrieved = await _store.GetRunAttestationAsync("non-existent", CancellationToken.None);
|
||||
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StoreClaimAttestation_ThenRetrieve_Works()
|
||||
{
|
||||
var claim = CreateClaimAttestation("run-1", "turn-1");
|
||||
|
||||
await _store.StoreClaimAttestationAsync(claim, CancellationToken.None);
|
||||
|
||||
var claims = await _store.GetClaimAttestationsAsync("run-1", CancellationToken.None);
|
||||
|
||||
claims.Should().HaveCount(1);
|
||||
claims[0].TurnId.Should().Be("turn-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetClaimAttestations_MultiplePerRun_ReturnsAll()
|
||||
{
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-1"), CancellationToken.None);
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-2"), CancellationToken.None);
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-3"), CancellationToken.None);
|
||||
|
||||
var claims = await _store.GetClaimAttestationsAsync("run-1", CancellationToken.None);
|
||||
|
||||
claims.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetClaimAttestationsByTurn_FiltersCorrectly()
|
||||
{
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-1"), CancellationToken.None);
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-2"), CancellationToken.None);
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-1"), CancellationToken.None);
|
||||
|
||||
var turn1Claims = await _store.GetClaimAttestationsByTurnAsync("run-1", "turn-1", CancellationToken.None);
|
||||
|
||||
turn1Claims.Should().HaveCount(2);
|
||||
turn1Claims.Should().OnlyContain(c => c.TurnId == "turn-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Exists_WhenStored_ReturnsTrue()
|
||||
{
|
||||
await _store.StoreRunAttestationAsync(CreateRunAttestation("run-1"), CancellationToken.None);
|
||||
|
||||
var exists = await _store.ExistsAsync("run-1", CancellationToken.None);
|
||||
|
||||
exists.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Exists_WhenNotStored_ReturnsFalse()
|
||||
{
|
||||
var exists = await _store.ExistsAsync("non-existent", CancellationToken.None);
|
||||
|
||||
exists.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StoreSignedEnvelope_ThenRetrieve_Works()
|
||||
{
|
||||
var envelope = new { Type = "DSSE", Payload = "test" };
|
||||
|
||||
await _store.StoreSignedEnvelopeAsync("run-1", envelope, CancellationToken.None);
|
||||
|
||||
var retrieved = await _store.GetSignedEnvelopeAsync("run-1", CancellationToken.None);
|
||||
|
||||
retrieved.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByTenant_FiltersCorrectly()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var att1 = CreateRunAttestation("run-1", "tenant-a", now.AddMinutes(-30));
|
||||
var att2 = CreateRunAttestation("run-2", "tenant-a", now.AddMinutes(-10));
|
||||
var att3 = CreateRunAttestation("run-3", "tenant-b", now.AddMinutes(-20));
|
||||
|
||||
await _store.StoreRunAttestationAsync(att1, CancellationToken.None);
|
||||
await _store.StoreRunAttestationAsync(att2, CancellationToken.None);
|
||||
await _store.StoreRunAttestationAsync(att3, CancellationToken.None);
|
||||
|
||||
var tenantAResults = await _store.GetByTenantAsync(
|
||||
"tenant-a",
|
||||
now.AddHours(-1),
|
||||
now,
|
||||
CancellationToken.None);
|
||||
|
||||
tenantAResults.Should().HaveCount(2);
|
||||
tenantAResults.Should().OnlyContain(a => a.TenantId == "tenant-a");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByTenant_FiltersTimeRangeCorrectly()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var att1 = CreateRunAttestation("run-1", "tenant-a", now.AddHours(-3));
|
||||
var att2 = CreateRunAttestation("run-2", "tenant-a", now.AddHours(-1));
|
||||
var att3 = CreateRunAttestation("run-3", "tenant-a", now.AddMinutes(-30));
|
||||
|
||||
await _store.StoreRunAttestationAsync(att1, CancellationToken.None);
|
||||
await _store.StoreRunAttestationAsync(att2, CancellationToken.None);
|
||||
await _store.StoreRunAttestationAsync(att3, CancellationToken.None);
|
||||
|
||||
var results = await _store.GetByTenantAsync(
|
||||
"tenant-a",
|
||||
now.AddHours(-2),
|
||||
now,
|
||||
CancellationToken.None);
|
||||
|
||||
results.Should().HaveCount(2);
|
||||
results.Should().NotContain(a => a.RunId == "run-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByContentDigest_Works()
|
||||
{
|
||||
var claim = CreateClaimAttestation("run-1", "turn-1", "sha256:test123");
|
||||
await _store.StoreClaimAttestationAsync(claim, CancellationToken.None);
|
||||
|
||||
var retrieved = await _store.GetByContentDigestAsync("sha256:test123", CancellationToken.None);
|
||||
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.ContentDigest.Should().Be("sha256:test123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByContentDigest_NotFound_ReturnsNull()
|
||||
{
|
||||
var retrieved = await _store.GetByContentDigestAsync("sha256:nonexistent", CancellationToken.None);
|
||||
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Clear_RemovesAllData()
|
||||
{
|
||||
await _store.StoreRunAttestationAsync(CreateRunAttestation("run-1"), CancellationToken.None);
|
||||
await _store.StoreClaimAttestationAsync(CreateClaimAttestation("run-1", "turn-1"), CancellationToken.None);
|
||||
|
||||
_store.Clear();
|
||||
|
||||
_store.RunAttestationCount.Should().Be(0);
|
||||
_store.ClaimAttestationCount.Should().Be(0);
|
||||
}
|
||||
|
||||
private static AiRunAttestation CreateRunAttestation(
|
||||
string runId,
|
||||
string tenantId = "test-tenant",
|
||||
string tenantId = DefaultTenantId,
|
||||
DateTimeOffset? startedAt = null)
|
||||
{
|
||||
var start = startedAt ?? FixedUtcNow;
|
||||
|
||||
return new AiRunAttestation
|
||||
{
|
||||
RunId = runId,
|
||||
TenantId = tenantId,
|
||||
UserId = "user-1",
|
||||
StartedAt = startedAt ?? DateTimeOffset.UtcNow,
|
||||
CompletedAt = DateTimeOffset.UtcNow,
|
||||
StartedAt = start,
|
||||
CompletedAt = start.AddMinutes(5),
|
||||
Model = new AiModelInfo
|
||||
{
|
||||
ModelId = "gpt-4",
|
||||
@@ -210,20 +50,23 @@ public class InMemoryAiAttestationStoreTests
|
||||
};
|
||||
}
|
||||
|
||||
private static AiClaimAttestation CreateClaimAttestation(
|
||||
private AiClaimAttestation CreateClaimAttestation(
|
||||
string runId,
|
||||
string turnId,
|
||||
string? contentDigest = null)
|
||||
{
|
||||
var suffix = Interlocked.Increment(ref _claimCounter);
|
||||
var claimId = $"claim-{runId}-{turnId}-{suffix:0000}";
|
||||
|
||||
return new AiClaimAttestation
|
||||
{
|
||||
ClaimId = $"claim-{Guid.NewGuid():N}",
|
||||
ClaimId = claimId,
|
||||
RunId = runId,
|
||||
TurnId = turnId,
|
||||
TenantId = "test-tenant",
|
||||
TenantId = DefaultTenantId,
|
||||
ClaimText = "Test claim text",
|
||||
ClaimDigest = "sha256:claimhash",
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
ClaimDigest = $"sha256:{claimId}",
|
||||
Timestamp = FixedUtcNow,
|
||||
ClaimType = "vulnerability_assessment",
|
||||
ContentDigest = contentDigest ?? $"sha256:{runId}-{turnId}"
|
||||
};
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// <copyright file="AttestationServiceFixture.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.AdvisoryAI.Attestation;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests.Integration;
|
||||
|
||||
public sealed class AttestationServiceFixture : IAsyncLifetime
|
||||
{
|
||||
private static readonly DateTimeOffset FixedUtcNow = new(2026, 1, 9, 12, 0, 0, TimeSpan.Zero);
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
|
||||
public IAiAttestationService AttestationService { get; private set; } = null!;
|
||||
|
||||
public ValueTask InitializeAsync()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
|
||||
var timeProvider = new FakeTimeProvider();
|
||||
timeProvider.SetUtcNow(FixedUtcNow);
|
||||
services.AddAiAttestationServices(timeProvider);
|
||||
services.AddInMemoryAiAttestationStore();
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
AttestationService = _serviceProvider.GetRequiredService<IAiAttestationService>();
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _serviceProvider.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// <copyright file="AttestationServiceIntegrationTests.ClaimAttestations.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests.Integration;
|
||||
|
||||
public sealed partial class AttestationServiceIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task FullClaimAttestationFlow_CreateSignVerify_SucceedsAsync()
|
||||
{
|
||||
var runAttestation = CreateSampleRunAttestation("run-integration-002");
|
||||
await _attestationService.CreateRunAttestationAsync(runAttestation);
|
||||
|
||||
var claimAttestation = CreateSampleClaimAttestation("claim-001", "run-integration-002", "turn-001");
|
||||
|
||||
var createResult = await _attestationService.CreateClaimAttestationAsync(claimAttestation, sign: true);
|
||||
|
||||
Assert.NotNull(createResult.Digest);
|
||||
|
||||
var claims = await _attestationService.GetClaimAttestationsAsync("run-integration-002");
|
||||
|
||||
Assert.Single(claims);
|
||||
Assert.Equal("claim-001", claims[0].ClaimId);
|
||||
|
||||
var verifyResult = await _attestationService.VerifyClaimAttestationAsync("claim-001");
|
||||
|
||||
Assert.True(verifyResult.Valid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StorageRoundTrip_MultipleClaimsPerRun_AllRetrievableAsync()
|
||||
{
|
||||
var runId = "run-multiclaim-001";
|
||||
var run = CreateSampleRunAttestation(runId);
|
||||
await _attestationService.CreateRunAttestationAsync(run);
|
||||
|
||||
var claims = Enumerable.Range(1, 3)
|
||||
.Select(i => CreateSampleClaimAttestation($"claim-mc-{i:D3}", runId, $"turn-{i:D3}"))
|
||||
.ToList();
|
||||
|
||||
foreach (var claim in claims)
|
||||
{
|
||||
var result = await _attestationService.CreateClaimAttestationAsync(claim);
|
||||
Assert.NotNull(result.Digest);
|
||||
}
|
||||
|
||||
var retrieved = await _attestationService.GetClaimAttestationsAsync(runId);
|
||||
Assert.Equal(3, retrieved.Count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// <copyright file="AttestationServiceIntegrationTests.Query.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests.Integration;
|
||||
|
||||
public sealed partial class AttestationServiceIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StorageRoundTrip_MultipleRuns_AllRetrievableAsync()
|
||||
{
|
||||
var runs = Enumerable.Range(1, 5)
|
||||
.Select(i => CreateSampleRunAttestation($"run-roundtrip-{i:D3}"))
|
||||
.ToList();
|
||||
|
||||
foreach (var run in runs)
|
||||
{
|
||||
var result = await _attestationService.CreateRunAttestationAsync(run);
|
||||
Assert.NotNull(result.Digest);
|
||||
}
|
||||
|
||||
foreach (var run in runs)
|
||||
{
|
||||
var retrieved = await _attestationService.GetRunAttestationAsync(run.RunId);
|
||||
Assert.NotNull(retrieved);
|
||||
Assert.Equal(run.RunId, retrieved.RunId);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryByTenant_ReturnsOnlyTenantRunsAsync()
|
||||
{
|
||||
var tenant1Run = CreateSampleRunAttestation("run-tenant1-001", tenantId: "tenant-1");
|
||||
var tenant2Run = CreateSampleRunAttestation("run-tenant2-001", tenantId: "tenant-2");
|
||||
|
||||
await _attestationService.CreateRunAttestationAsync(tenant1Run);
|
||||
await _attestationService.CreateRunAttestationAsync(tenant2Run);
|
||||
|
||||
var tenant1Runs = await _attestationService.ListRecentAttestationsAsync("tenant-1", limit: 10);
|
||||
var tenant2Runs = await _attestationService.ListRecentAttestationsAsync("tenant-2", limit: 10);
|
||||
|
||||
Assert.Single(tenant1Runs);
|
||||
Assert.Equal("run-tenant1-001", tenant1Runs[0].RunId);
|
||||
|
||||
Assert.Single(tenant2Runs);
|
||||
Assert.Equal("run-tenant2-001", tenant2Runs[0].RunId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// <copyright file="AttestationServiceIntegrationTests.RunAttestations.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests.Integration;
|
||||
|
||||
public sealed partial class AttestationServiceIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task FullRunAttestationFlow_CreateSignVerify_SucceedsAsync()
|
||||
{
|
||||
var attestation = CreateSampleRunAttestation("run-integration-001");
|
||||
|
||||
var createResult = await _attestationService.CreateRunAttestationAsync(attestation, sign: true);
|
||||
|
||||
Assert.NotNull(createResult.Digest);
|
||||
Assert.StartsWith("sha256:", createResult.Digest);
|
||||
|
||||
var retrieved = await _attestationService.GetRunAttestationAsync("run-integration-001");
|
||||
|
||||
Assert.NotNull(retrieved);
|
||||
Assert.Equal(attestation.RunId, retrieved.RunId);
|
||||
Assert.Equal(attestation.TenantId, retrieved.TenantId);
|
||||
|
||||
var verifyResult = await _attestationService.VerifyRunAttestationAsync("run-integration-001");
|
||||
|
||||
Assert.True(verifyResult.Valid);
|
||||
Assert.True(verifyResult.DigestValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnsignedAttestation_VerifiesDigestOnlyAsync()
|
||||
{
|
||||
var attestation = CreateSampleRunAttestation("run-unsigned-001");
|
||||
|
||||
var createResult = await _attestationService.CreateRunAttestationAsync(attestation, sign: false);
|
||||
Assert.NotNull(createResult.Digest);
|
||||
|
||||
var verifyResult = await _attestationService.VerifyRunAttestationAsync("run-unsigned-001");
|
||||
|
||||
Assert.True(verifyResult.Valid);
|
||||
Assert.True(verifyResult.DigestValid);
|
||||
Assert.Null(verifyResult.SignatureValid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// <copyright file="AttestationServiceIntegrationTests.Verification.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests.Integration;
|
||||
|
||||
public sealed partial class AttestationServiceIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task VerificationFailure_TamperedContent_ReturnsInvalidAsync()
|
||||
{
|
||||
var attestation = CreateSampleRunAttestation("run-tamper-001");
|
||||
var createResult = await _attestationService.CreateRunAttestationAsync(attestation, sign: true);
|
||||
Assert.NotNull(createResult.Digest);
|
||||
|
||||
var verifyResult = await _attestationService.VerifyRunAttestationAsync("run-tamper-001");
|
||||
|
||||
Assert.True(verifyResult.Valid, "Original attestation should verify");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerificationFailure_NonExistentRun_ReturnsInvalidAsync()
|
||||
{
|
||||
var verifyResult = await _attestationService.VerifyRunAttestationAsync("non-existent-run");
|
||||
|
||||
Assert.False(verifyResult.Valid);
|
||||
Assert.Contains("not found", verifyResult.FailureReason, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
// <copyright file="AttestationServiceIntegrationTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AdvisoryAI.Attestation;
|
||||
using StellaOps.AdvisoryAI.Attestation.Models;
|
||||
using StellaOps.AdvisoryAI.Attestation.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests.Integration;
|
||||
@@ -15,215 +12,14 @@ namespace StellaOps.AdvisoryAI.Attestation.Tests.Integration;
|
||||
/// Sprint: SPRINT_20260109_011_001 Task: AIAT-008
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class AttestationServiceIntegrationTests : IAsyncLifetime
|
||||
public sealed partial class AttestationServiceIntegrationTests : IClassFixture<AttestationServiceFixture>
|
||||
{
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private IAiAttestationService _attestationService = null!;
|
||||
private IAiAttestationStore _store = null!;
|
||||
private TimeProvider _timeProvider = null!;
|
||||
private static readonly DateTimeOffset FixedUtcNow = new(2026, 1, 9, 12, 0, 0, TimeSpan.Zero);
|
||||
private readonly IAiAttestationService _attestationService;
|
||||
|
||||
public ValueTask InitializeAsync()
|
||||
public AttestationServiceIntegrationTests(AttestationServiceFixture fixture)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Add logging
|
||||
services.AddLogging();
|
||||
|
||||
// Register all attestation services
|
||||
services.AddAiAttestationServices();
|
||||
services.AddInMemoryAiAttestationStore();
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
_attestationService = _serviceProvider.GetRequiredService<IAiAttestationService>();
|
||||
_store = _serviceProvider.GetRequiredService<IAiAttestationStore>();
|
||||
_timeProvider = _serviceProvider.GetRequiredService<TimeProvider>();
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _serviceProvider.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullRunAttestationFlow_CreateSignVerify_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var attestation = CreateSampleRunAttestation("run-integration-001");
|
||||
|
||||
// Act - Create and sign
|
||||
var createResult = await _attestationService.CreateRunAttestationAsync(attestation, sign: true);
|
||||
|
||||
// Assert creation - result has Digest property
|
||||
Assert.NotNull(createResult.Digest);
|
||||
Assert.StartsWith("sha256:", createResult.Digest);
|
||||
|
||||
// Act - Retrieve
|
||||
var retrieved = await _attestationService.GetRunAttestationAsync("run-integration-001");
|
||||
|
||||
// Assert retrieval
|
||||
Assert.NotNull(retrieved);
|
||||
Assert.Equal(attestation.RunId, retrieved.RunId);
|
||||
Assert.Equal(attestation.TenantId, retrieved.TenantId);
|
||||
|
||||
// Act - Verify
|
||||
var verifyResult = await _attestationService.VerifyRunAttestationAsync("run-integration-001");
|
||||
|
||||
// Assert verification
|
||||
Assert.True(verifyResult.Valid);
|
||||
Assert.True(verifyResult.DigestValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullClaimAttestationFlow_CreateSignVerify_Succeeds()
|
||||
{
|
||||
// Arrange - Create parent run first
|
||||
var runAttestation = CreateSampleRunAttestation("run-integration-002");
|
||||
await _attestationService.CreateRunAttestationAsync(runAttestation);
|
||||
|
||||
var claimAttestation = CreateSampleClaimAttestation("claim-001", "run-integration-002", "turn-001");
|
||||
|
||||
// Act - Create and sign
|
||||
var createResult = await _attestationService.CreateClaimAttestationAsync(claimAttestation, sign: true);
|
||||
|
||||
// Assert creation
|
||||
Assert.NotNull(createResult.Digest);
|
||||
|
||||
// Act - Retrieve claims for run
|
||||
var claims = await _attestationService.GetClaimAttestationsAsync("run-integration-002");
|
||||
|
||||
// Assert retrieval
|
||||
Assert.Single(claims);
|
||||
Assert.Equal("claim-001", claims[0].ClaimId);
|
||||
|
||||
// Act - Verify
|
||||
var verifyResult = await _attestationService.VerifyClaimAttestationAsync("claim-001");
|
||||
|
||||
// Assert verification
|
||||
Assert.True(verifyResult.Valid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StorageRoundTrip_MultipleRuns_AllRetrievable()
|
||||
{
|
||||
// Arrange - Create multiple runs
|
||||
var runs = Enumerable.Range(1, 5)
|
||||
.Select(i => CreateSampleRunAttestation($"run-roundtrip-{i:D3}"))
|
||||
.ToList();
|
||||
|
||||
// Act - Store all
|
||||
foreach (var run in runs)
|
||||
{
|
||||
var result = await _attestationService.CreateRunAttestationAsync(run);
|
||||
Assert.NotNull(result.Digest);
|
||||
}
|
||||
|
||||
// Assert - All retrievable
|
||||
foreach (var run in runs)
|
||||
{
|
||||
var retrieved = await _attestationService.GetRunAttestationAsync(run.RunId);
|
||||
Assert.NotNull(retrieved);
|
||||
Assert.Equal(run.RunId, retrieved.RunId);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StorageRoundTrip_MultipleClaimsPerRun_AllRetrievable()
|
||||
{
|
||||
// Arrange
|
||||
var runId = "run-multiclaim-001";
|
||||
var run = CreateSampleRunAttestation(runId);
|
||||
await _attestationService.CreateRunAttestationAsync(run);
|
||||
|
||||
var claims = Enumerable.Range(1, 3)
|
||||
.Select(i => CreateSampleClaimAttestation($"claim-mc-{i:D3}", runId, $"turn-{i:D3}"))
|
||||
.ToList();
|
||||
|
||||
// Act - Store all claims
|
||||
foreach (var claim in claims)
|
||||
{
|
||||
var result = await _attestationService.CreateClaimAttestationAsync(claim);
|
||||
Assert.NotNull(result.Digest);
|
||||
}
|
||||
|
||||
// Assert - All claims retrievable
|
||||
var retrieved = await _attestationService.GetClaimAttestationsAsync(runId);
|
||||
Assert.Equal(3, retrieved.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryByTenant_ReturnsOnlyTenantRuns()
|
||||
{
|
||||
// Arrange
|
||||
var tenant1Run = CreateSampleRunAttestation("run-tenant1-001", tenantId: "tenant-1");
|
||||
var tenant2Run = CreateSampleRunAttestation("run-tenant2-001", tenantId: "tenant-2");
|
||||
|
||||
await _attestationService.CreateRunAttestationAsync(tenant1Run);
|
||||
await _attestationService.CreateRunAttestationAsync(tenant2Run);
|
||||
|
||||
// Act
|
||||
var tenant1Runs = await _attestationService.ListRecentAttestationsAsync("tenant-1", limit: 10);
|
||||
var tenant2Runs = await _attestationService.ListRecentAttestationsAsync("tenant-2", limit: 10);
|
||||
|
||||
// Assert
|
||||
Assert.Single(tenant1Runs);
|
||||
Assert.Equal("run-tenant1-001", tenant1Runs[0].RunId);
|
||||
|
||||
Assert.Single(tenant2Runs);
|
||||
Assert.Equal("run-tenant2-001", tenant2Runs[0].RunId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerificationFailure_TamperedContent_ReturnsInvalid()
|
||||
{
|
||||
// This test validates tamper detection, which requires the service
|
||||
// to verify against stored digests. Currently the in-memory service
|
||||
// uses its own internal storage, so this scenario tests what's possible.
|
||||
|
||||
// Arrange
|
||||
var attestation = CreateSampleRunAttestation("run-tamper-001");
|
||||
var createResult = await _attestationService.CreateRunAttestationAsync(attestation, sign: true);
|
||||
Assert.NotNull(createResult.Digest);
|
||||
|
||||
// Act - Verify the original (should succeed)
|
||||
var verifyResult = await _attestationService.VerifyRunAttestationAsync("run-tamper-001");
|
||||
|
||||
// Assert - Original should verify
|
||||
Assert.True(verifyResult.Valid, "Original attestation should verify");
|
||||
|
||||
// Note: Full tamper detection (storing modified content and detecting mismatch)
|
||||
// requires AIAT-008 implementation. For now we just verify the happy path.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerificationFailure_NonExistentRun_ReturnsInvalid()
|
||||
{
|
||||
// Act
|
||||
var verifyResult = await _attestationService.VerifyRunAttestationAsync("non-existent-run");
|
||||
|
||||
// Assert
|
||||
Assert.False(verifyResult.Valid);
|
||||
Assert.Contains("not found", verifyResult.FailureReason, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnsignedAttestation_VerifiesDigestOnly()
|
||||
{
|
||||
// Arrange
|
||||
var attestation = CreateSampleRunAttestation("run-unsigned-001");
|
||||
|
||||
// Act - Create without signing
|
||||
var createResult = await _attestationService.CreateRunAttestationAsync(attestation, sign: false);
|
||||
Assert.NotNull(createResult.Digest);
|
||||
|
||||
// Act - Verify
|
||||
var verifyResult = await _attestationService.VerifyRunAttestationAsync("run-unsigned-001");
|
||||
|
||||
// Assert
|
||||
Assert.True(verifyResult.Valid);
|
||||
Assert.True(verifyResult.DigestValid);
|
||||
Assert.Null(verifyResult.SignatureValid); // No signature to verify
|
||||
_attestationService = fixture.AttestationService;
|
||||
}
|
||||
|
||||
private static AiRunAttestation CreateSampleRunAttestation(
|
||||
@@ -242,8 +38,8 @@ public sealed class AttestationServiceIntegrationTests : IAsyncLifetime
|
||||
Provider = "test-provider"
|
||||
},
|
||||
TotalTokens = 100,
|
||||
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
CompletedAt = DateTimeOffset.UtcNow
|
||||
StartedAt = FixedUtcNow.AddMinutes(-5),
|
||||
CompletedAt = FixedUtcNow
|
||||
};
|
||||
}
|
||||
|
||||
@@ -262,7 +58,7 @@ public sealed class AttestationServiceIntegrationTests : IAsyncLifetime
|
||||
ClaimText = "This is a test claim",
|
||||
ClaimDigest = $"sha256:{claimId}",
|
||||
ContentDigest = $"sha256:content-{claimId}",
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
Timestamp = FixedUtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// <copyright file="PromptTemplateRegistryTests.GetAllTemplates.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class PromptTemplateRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetAllTemplates_ReturnsAllLatestVersions()
|
||||
{
|
||||
_registry.Register("template-a", "1.0.0", "Template A v1");
|
||||
_registry.Register("template-a", "1.1.0", "Template A v2");
|
||||
_registry.Register("template-b", "1.0.0", "Template B");
|
||||
_registry.Register("template-c", "2.0.0", "Template C");
|
||||
|
||||
var all = _registry.GetAllTemplates();
|
||||
|
||||
all.Should().HaveCount(3);
|
||||
all.Should().Contain(t => t.Name == "template-a" && t.Version == "1.1.0");
|
||||
all.Should().Contain(t => t.Name == "template-b");
|
||||
all.Should().Contain(t => t.Name == "template-c");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// <copyright file="PromptTemplateRegistryTests.GetTemplateInfo.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class PromptTemplateRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetTemplateInfo_ByVersion_ReturnsCorrectVersion()
|
||||
{
|
||||
_registry.Register("vuln-explanation", "1.0.0", "Template v1");
|
||||
_registry.Register("vuln-explanation", "1.1.0", "Template v2");
|
||||
|
||||
var v1 = _registry.GetTemplateInfo("vuln-explanation", "1.0.0");
|
||||
var v2 = _registry.GetTemplateInfo("vuln-explanation", "1.1.0");
|
||||
|
||||
v1!.Version.Should().Be("1.0.0");
|
||||
v2!.Version.Should().Be("1.1.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTemplateInfo_NotFound_ReturnsNull()
|
||||
{
|
||||
var info = _registry.GetTemplateInfo("non-existent");
|
||||
|
||||
info.Should().BeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// <copyright file="PromptTemplateRegistryTests.GuardClauses.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class PromptTemplateRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Register_NullName_Throws()
|
||||
{
|
||||
var act = () => _registry.Register(null!, "1.0.0", "content");
|
||||
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_EmptyVersion_Throws()
|
||||
{
|
||||
var act = () => _registry.Register("name", string.Empty, "content");
|
||||
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_EmptyTemplate_Throws()
|
||||
{
|
||||
var act = () => _registry.Register("name", "1.0.0", string.Empty);
|
||||
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// <copyright file="PromptTemplateRegistryTests.Register.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class PromptTemplateRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Register_ValidTemplate_StoresWithDigest()
|
||||
{
|
||||
_registry.Register("vuln-explanation", "1.0.0", "Explain this vulnerability: {{cve}}");
|
||||
|
||||
var info = _registry.GetTemplateInfo("vuln-explanation");
|
||||
|
||||
info.Should().NotBeNull();
|
||||
info!.Name.Should().Be("vuln-explanation");
|
||||
info.Version.Should().Be("1.0.0");
|
||||
info.Digest.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_SameTemplateTwice_UpdatesVersion()
|
||||
{
|
||||
_registry.Register("vuln-explanation", "1.0.0", "Template v1");
|
||||
_registry.Register("vuln-explanation", "1.1.0", "Template v2");
|
||||
|
||||
var info = _registry.GetTemplateInfo("vuln-explanation");
|
||||
|
||||
info!.Version.Should().Be("1.1.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_DifferentContent_ProducesDifferentDigests()
|
||||
{
|
||||
_registry.Register("template-1", "1.0.0", "Content A");
|
||||
_registry.Register("template-2", "1.0.0", "Content B");
|
||||
|
||||
var info1 = _registry.GetTemplateInfo("template-1");
|
||||
var info2 = _registry.GetTemplateInfo("template-2");
|
||||
|
||||
info1!.Digest.Should().NotBe(info2!.Digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_SameContent_ProducesSameDigest()
|
||||
{
|
||||
const string content = "Same content";
|
||||
_registry.Register("template-1", "1.0.0", content);
|
||||
_registry.Register("template-2", "1.0.0", content);
|
||||
|
||||
var info1 = _registry.GetTemplateInfo("template-1");
|
||||
var info2 = _registry.GetTemplateInfo("template-2");
|
||||
|
||||
info1!.Digest.Should().Be(info2!.Digest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// <copyright file="PromptTemplateRegistryTests.VerifyHash.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
|
||||
public sealed partial class PromptTemplateRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public void VerifyHash_MatchingHash_ReturnsTrue()
|
||||
{
|
||||
const string template = "Test template content";
|
||||
_registry.Register("test", "1.0.0", template);
|
||||
|
||||
var info = _registry.GetTemplateInfo("test");
|
||||
var result = _registry.VerifyHash("test", info!.Digest);
|
||||
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyHash_NonMatchingHash_ReturnsFalse()
|
||||
{
|
||||
_registry.Register("test", "1.0.0", "Test template content");
|
||||
|
||||
var result = _registry.VerifyHash("test", "sha256:wronghash");
|
||||
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyHash_NotFound_ReturnsFalse()
|
||||
{
|
||||
var result = _registry.VerifyHash("non-existent", "sha256:anyhash");
|
||||
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
// <copyright file="PromptTemplateRegistryTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.AdvisoryAI.Attestation;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
@@ -13,155 +12,17 @@ namespace StellaOps.AdvisoryAI.Attestation.Tests;
|
||||
/// Tests for <see cref="PromptTemplateRegistry"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class PromptTemplateRegistryTests
|
||||
public sealed partial class PromptTemplateRegistryTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedUtcNow = new(2026, 1, 9, 12, 0, 0, TimeSpan.Zero);
|
||||
private readonly FakeTimeProvider _timeProvider = new();
|
||||
private readonly PromptTemplateRegistry _registry;
|
||||
|
||||
public PromptTemplateRegistryTests()
|
||||
{
|
||||
_timeProvider.SetUtcNow(FixedUtcNow);
|
||||
_registry = new PromptTemplateRegistry(
|
||||
_timeProvider,
|
||||
NullLogger<PromptTemplateRegistry>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_ValidTemplate_StoresWithDigest()
|
||||
{
|
||||
_registry.Register("vuln-explanation", "1.0.0", "Explain this vulnerability: {{cve}}");
|
||||
|
||||
var info = _registry.GetTemplateInfo("vuln-explanation");
|
||||
|
||||
info.Should().NotBeNull();
|
||||
info!.Name.Should().Be("vuln-explanation");
|
||||
info.Version.Should().Be("1.0.0");
|
||||
info.Digest.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_SameTemplateTwice_UpdatesVersion()
|
||||
{
|
||||
_registry.Register("vuln-explanation", "1.0.0", "Template v1");
|
||||
_registry.Register("vuln-explanation", "1.1.0", "Template v2");
|
||||
|
||||
var info = _registry.GetTemplateInfo("vuln-explanation");
|
||||
|
||||
info!.Version.Should().Be("1.1.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTemplateInfo_ByVersion_ReturnsCorrectVersion()
|
||||
{
|
||||
_registry.Register("vuln-explanation", "1.0.0", "Template v1");
|
||||
_registry.Register("vuln-explanation", "1.1.0", "Template v2");
|
||||
|
||||
var v1 = _registry.GetTemplateInfo("vuln-explanation", "1.0.0");
|
||||
var v2 = _registry.GetTemplateInfo("vuln-explanation", "1.1.0");
|
||||
|
||||
v1!.Version.Should().Be("1.0.0");
|
||||
v2!.Version.Should().Be("1.1.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTemplateInfo_NotFound_ReturnsNull()
|
||||
{
|
||||
var info = _registry.GetTemplateInfo("non-existent");
|
||||
|
||||
info.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyHash_MatchingHash_ReturnsTrue()
|
||||
{
|
||||
const string template = "Test template content";
|
||||
_registry.Register("test", "1.0.0", template);
|
||||
|
||||
var info = _registry.GetTemplateInfo("test");
|
||||
var result = _registry.VerifyHash("test", info!.Digest);
|
||||
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyHash_NonMatchingHash_ReturnsFalse()
|
||||
{
|
||||
_registry.Register("test", "1.0.0", "Test template content");
|
||||
|
||||
var result = _registry.VerifyHash("test", "sha256:wronghash");
|
||||
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyHash_NotFound_ReturnsFalse()
|
||||
{
|
||||
var result = _registry.VerifyHash("non-existent", "sha256:anyhash");
|
||||
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAllTemplates_ReturnsAllLatestVersions()
|
||||
{
|
||||
_registry.Register("template-a", "1.0.0", "Template A v1");
|
||||
_registry.Register("template-a", "1.1.0", "Template A v2");
|
||||
_registry.Register("template-b", "1.0.0", "Template B");
|
||||
_registry.Register("template-c", "2.0.0", "Template C");
|
||||
|
||||
var all = _registry.GetAllTemplates();
|
||||
|
||||
all.Should().HaveCount(3);
|
||||
all.Should().Contain(t => t.Name == "template-a" && t.Version == "1.1.0");
|
||||
all.Should().Contain(t => t.Name == "template-b");
|
||||
all.Should().Contain(t => t.Name == "template-c");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_DifferentContent_ProducesDifferentDigests()
|
||||
{
|
||||
_registry.Register("template-1", "1.0.0", "Content A");
|
||||
_registry.Register("template-2", "1.0.0", "Content B");
|
||||
|
||||
var info1 = _registry.GetTemplateInfo("template-1");
|
||||
var info2 = _registry.GetTemplateInfo("template-2");
|
||||
|
||||
info1!.Digest.Should().NotBe(info2!.Digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_SameContent_ProducesSameDigest()
|
||||
{
|
||||
const string content = "Same content";
|
||||
_registry.Register("template-1", "1.0.0", content);
|
||||
_registry.Register("template-2", "1.0.0", content);
|
||||
|
||||
var info1 = _registry.GetTemplateInfo("template-1");
|
||||
var info2 = _registry.GetTemplateInfo("template-2");
|
||||
|
||||
info1!.Digest.Should().Be(info2!.Digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_NullName_Throws()
|
||||
{
|
||||
var act = () => _registry.Register(null!, "1.0.0", "content");
|
||||
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_EmptyVersion_Throws()
|
||||
{
|
||||
var act = () => _registry.Register("name", "", "content");
|
||||
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_EmptyTemplate_Throws()
|
||||
{
|
||||
var act = () => _registry.Register("name", "1.0.0", "");
|
||||
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/__Libraries/__Tests/StellaOps.AdvisoryAI.Attestation.Tests/StellaOps.AdvisoryAI.Attestation.Tests.md. |
|
||||
| REMED-05 | DONE | Remediation checklist completed: file splits, async naming, deterministic fixtures, service locator removal. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| REMED-08 | DONE | Split tests <= 100 lines; deterministic fixtures/time/IDs; async naming; DI fixture for integration tests; ConfigureAwait(false) skipped per xUnit1030; dotnet test passed 2026-02-02 (58 tests). |
|
||||
|
||||
Reference in New Issue
Block a user