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

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

View File

@@ -78,7 +78,6 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
public async Task Database_CanCreateAndVerifySchema()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
using var connection = new Npgsql.NpgsqlConnection(_connectionString);
await connection.OpenAsync(TestContext.Current.CancellationToken);
@@ -101,7 +100,6 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
public async Task Database_CanPerformCrudOperations()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
using var connection = new Npgsql.NpgsqlConnection(_connectionString);
await connection.OpenAsync(TestContext.Current.CancellationToken);
@@ -151,7 +149,6 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
public async Task Database_CanRunDdlMigrations()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
using var connection = new Npgsql.NpgsqlConnection(_connectionString);
await connection.OpenAsync(TestContext.Current.CancellationToken);
@@ -193,7 +190,6 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
public async Task Database_CanCreateExtensions()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
using var connection = new Npgsql.NpgsqlConnection(_connectionString);
await connection.OpenAsync(TestContext.Current.CancellationToken);

View File

@@ -10,14 +10,6 @@
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="FixtureHarvester.csproj" />

View File

@@ -0,0 +1,24 @@
# AGENTS - Testing.Chaos Tests
## Roles
- QA / test engineer: deterministic unit tests for chaos testing helpers.
- Backend engineer: maintain test utilities and fixtures.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- src/__Libraries/AGENTS.md
- Current sprint file under docs/implplan/SPRINT_*.md
## Working Directory & Boundaries
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Chaos.Tests
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Chaos
- Avoid cross-module edits unless explicitly noted in the sprint file.
## Determinism and Safety
- Avoid DateTimeOffset.UtcNow and real delays in tests.
- Prefer simulated or fake time providers for time-based behavior.
## Testing
- Cover failure injection, choreography steps, and convergence tracking with edge cases.

View File

@@ -14,10 +14,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,24 @@
# AGENTS - Testing.Chaos Library
## Roles
- Backend engineer: maintain chaos testing helpers and failure injectors.
- QA / test engineer: ensure deterministic, repeatable chaos simulations.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- src/__Libraries/AGENTS.md
- Current sprint file under docs/implplan/SPRINT_*.md
## Working Directory & Boundaries
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Chaos
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Chaos.Tests
- Avoid cross-module edits unless explicitly noted in the sprint file.
## Determinism and Safety
- Avoid DateTimeOffset.UtcNow and Random.Shared; use TimeProvider and deterministic RNG.
- Avoid real Task.Delay in test utilities; prefer simulated time.
## Testing
- Cover failure types, registry parsing, and deterministic ordering behavior.

View File

@@ -0,0 +1,24 @@
# AGENTS - Testing.ConfigDiff Library
## Roles
- Backend engineer: maintain config-diff test helpers.
- QA / test engineer: verify deterministic behavior snapshots and delta checks.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- src/__Libraries/AGENTS.md
- Current sprint file under docs/implplan/SPRINT_*.md
## Working Directory & Boundaries
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.ConfigDiff
- Used by config-diff test suites in modules.
- Avoid cross-module edits unless explicitly noted in the sprint file.
## Determinism and Safety
- Avoid DateTimeOffset.UtcNow in snapshots; pass explicit timestamps or TimeProvider.
- Use InvariantCulture for value formatting when capturing behavior snapshots.
## Testing
- Cover delta computation, ignored behaviors, and numeric tolerance behavior.

View File

@@ -0,0 +1,24 @@
# AGENTS - Testing.Coverage Library
## Roles
- Backend engineer: maintain coverage parsing and enforcement helpers.
- QA / test engineer: verify deterministic coverage calculations and reporting.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- src/__Libraries/AGENTS.md
- Current sprint file under docs/implplan/SPRINT_*.md
## Working Directory & Boundaries
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Coverage
- Used by test suites that enforce coverage thresholds.
- Avoid cross-module edits unless explicitly noted in the sprint file.
## Determinism and Safety
- Avoid DateTimeOffset.UtcNow for report timestamps; use explicit values or TimeProvider.
- Use invariant formatting for coverage percentages and numeric outputs.
## Testing
- Cover Cobertura parsing, dead-path detection, and exemption matching.

View File

@@ -0,0 +1,24 @@
# AGENTS - Testing.Evidence Tests
## Roles
- QA / test engineer: deterministic unit tests for test evidence services.
- Backend engineer: maintain test fixtures and validation coverage.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- src/__Libraries/AGENTS.md
- Current sprint file under docs/implplan/SPRINT_*.md
## Working Directory & Boundaries
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Evidence.Tests
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Evidence
- Avoid cross-module edits unless explicitly noted in the sprint file.
## Determinism and Safety
- Avoid DateTimeOffset.UtcNow in fixtures; use FakeTimeProvider or fixed timestamps.
- Keep deterministic ordering when asserting hashes or summaries.
## Testing
- Cover finalization, bundle retrieval, and deterministic hashing paths.

View File

@@ -10,12 +10,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Testing.Evidence\StellaOps.Testing.Evidence.csproj" />

View File

@@ -0,0 +1,24 @@
# AGENTS - Testing.Evidence Library
## Roles
- Backend engineer: maintain test evidence storage and hashing.
- QA / test engineer: validate determinism and summary calculations.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- src/__Libraries/AGENTS.md
- Current sprint file under docs/implplan/SPRINT_*.md
## Working Directory & Boundaries
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Evidence
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Evidence.Tests
- Avoid cross-module edits unless explicitly noted in the sprint file.
## Determinism and Safety
- Use canonical JSON or stable ordering for any hash inputs.
- Avoid nondeterministic timestamps; prefer TimeProvider.
## Testing
- Cover bundle ID/merkle root determinism and summary grouping behavior.

View File

@@ -0,0 +1,24 @@
# AGENTS - Testing.Explainability Library
## Roles
- Backend engineer: maintain explainability assertion helpers.
- QA / test engineer: ensure deterministic, reproducible decision checks.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- src/__Libraries/AGENTS.md
- Current sprint file under docs/implplan/SPRINT_*.md
## Working Directory & Boundaries
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Explainability
- Used by explainability test suites across modules.
- Avoid cross-module edits unless explicitly noted in the sprint file.
## Determinism and Safety
- Use ordinal ordering when comparing factor IDs or rule names.
- Avoid culture-sensitive formatting in assertion messages.
## Testing
- Cover reproducibility checks, factor weight validation, and rule-trigger assertions.

View File

@@ -0,0 +1,24 @@
# AGENTS - Testing.Policy Library
## Roles
- Backend engineer: maintain policy diff and regression helpers.
- QA / test engineer: validate deterministic regression checks.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- src/__Libraries/AGENTS.md
- Current sprint file under docs/implplan/SPRINT_*.md
## Working Directory & Boundaries
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Policy
- Used by policy regression test suites across modules.
- Avoid cross-module edits unless explicitly noted in the sprint file.
## Determinism and Safety
- Avoid DateTimeOffset.UtcNow in default results; use TimeProvider.
- Use ordinal ordering for factor comparisons and diff outputs.
## Testing
- Cover factor delta detection, allowed-change filtering, and diff ordering.

View File

@@ -0,0 +1,24 @@
# AGENTS - Testing.Replay Tests
## Roles
- QA / test engineer: deterministic unit tests for replay helpers.
- Backend engineer: maintain replay test fixtures and trace builders.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- src/__Libraries/AGENTS.md
- Current sprint file under docs/implplan/SPRINT_*.md
## Working Directory & Boundaries
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Replay.Tests
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Replay
- Avoid cross-module edits unless explicitly noted in the sprint file.
## Determinism and Safety
- Avoid DateTimeOffset.UtcNow in trace fixtures; use fixed timestamps.
- Ensure trace ordering and output hashes are deterministic.
## Testing
- Cover query filters, batch replay expectations, and cancellation paths.

View File

@@ -10,12 +10,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Testing.Replay\StellaOps.Testing.Replay.csproj" />

View File

@@ -0,0 +1,24 @@
# AGENTS - Testing.Replay Library
## Roles
- Backend engineer: maintain replay orchestration helpers.
- QA / test engineer: validate deterministic replay outputs.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- src/__Libraries/AGENTS.md
- Current sprint file under docs/implplan/SPRINT_*.md
## Working Directory & Boundaries
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Replay
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Replay.Tests
- Avoid cross-module edits unless explicitly noted in the sprint file.
## Determinism and Safety
- Use deterministic time providers and stable ordering for trace queries.
- Avoid nondeterministic hashes by sorting inputs before hashing.
## Testing
- Cover corpus import/query, output hash determinism, and replay batching.

View File

@@ -0,0 +1,24 @@
# AGENTS - Testing.SchemaEvolution Library
## Roles
- Backend engineer: maintain schema evolution test harnesses.
- QA / test engineer: validate backward/forward compatibility checks.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- src/__Libraries/AGENTS.md
- Current sprint file under docs/implplan/SPRINT_*.md
## Working Directory & Boundaries
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.SchemaEvolution
- Used by module-specific schema evolution test suites.
- Avoid cross-module edits unless explicitly noted in the sprint file.
## Determinism and Safety
- Avoid DateTimeOffset.UtcNow defaults for schema timestamps.
- Pin or configure container images for deterministic test runs.
## Testing
- Cover migration rollback paths, version selection, and container lifecycle.

View File

@@ -0,0 +1,24 @@
# AGENTS - Testing.Temporal Tests
## Roles
- QA / test engineer: deterministic unit tests for temporal helpers.
- Backend engineer: maintain time simulation fixtures.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- src/__Libraries/AGENTS.md
- Current sprint file under docs/implplan/SPRINT_*.md
## Working Directory & Boundaries
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Temporal.Tests
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Temporal
- Avoid cross-module edits unless explicitly noted in the sprint file.
## Determinism and Safety
- Use fixed timestamps in fixtures; avoid DateTimeOffset.UtcNow.
- Avoid real delays in multi-threaded tests.
## Testing
- Cover drift, leap seconds, TTL boundaries, and idempotency scenarios.

View File

@@ -12,12 +12,6 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,24 @@
# AGENTS - Testing.Temporal Library
## Roles
- Backend engineer: maintain temporal test utilities.
- QA / test engineer: validate deterministic time simulation.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- src/__Libraries/AGENTS.md
- Current sprint file under docs/implplan/SPRINT_*.md
## Working Directory & Boundaries
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Temporal
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Temporal.Tests
- Avoid cross-module edits unless explicitly noted in the sprint file.
## Determinism and Safety
- Avoid DateTimeOffset.UtcNow defaults; require explicit start times.
- Use stable hashing in comparers to avoid randomized GetHashCode behavior.
## Testing
- Cover leap-second smearing, idempotency comparisons, and time drift math.

View File

@@ -0,0 +1,812 @@
// =============================================================================
// FacetSealAdmissionE2ETests.cs
// Sprint: SPRINT_20260105_002_004_CLI
// Task: CLI-018 - E2E test: seal -> deploy -> drift check
// Description: End-to-end tests for facet seal admission workflow
// =============================================================================
using System.Collections.Immutable;
using FluentAssertions;
using Moq;
using StellaOps.Facet;
using StellaOps.Integration.E2E.Integrations.Fixtures;
using Xunit;
namespace StellaOps.Integration.E2E.Integrations;
/// <summary>
/// E2E tests for the facet seal admission workflow.
/// </summary>
/// <remarks>
/// Tests cover the complete workflow:
/// 1. Creating a facet seal for an image
/// 2. Simulating Kubernetes admission with facet validation
/// 3. Detecting drift between baseline and current state
/// 4. Verifying quota-based admission decisions
/// </remarks>
[Trait("Category", "E2E")]
[Trait("Category", "Integrations")]
[Trait("Category", "Facet")]
public class FacetSealAdmissionE2ETests : IClassFixture<IntegrationTestFixture>
{
private readonly IntegrationTestFixture _fixture;
public FacetSealAdmissionE2ETests(IntegrationTestFixture fixture)
{
_fixture = fixture;
}
#region Seal Creation Tests
[Fact(DisplayName = "Facet seal is created with correct structure")]
public void FacetSeal_CreatedWithCorrectStructure()
{
// Arrange
var imageDigest = "sha256:abc123def456789012345678901234567890123456789012345678901234";
var createdAt = DateTimeOffset.UtcNow;
// Act
var seal = CreateTestSeal(imageDigest, createdAt);
// Assert
seal.ImageDigest.Should().Be(imageDigest);
seal.CreatedAt.Should().Be(createdAt);
seal.CombinedMerkleRoot.Should().NotBeNullOrEmpty();
seal.Facets.Should().NotBeEmpty();
}
[Fact(DisplayName = "Facet seal contains all built-in facets")]
public void FacetSeal_ContainsAllBuiltInFacets()
{
// Arrange
var imageDigest = "sha256:test123";
var expectedFacets = new[] { "runtime", "config", "static" };
// Act
var seal = CreateTestSealWithFacets(imageDigest, expectedFacets);
// Assert
seal.Facets.Select(f => f.FacetId).Should().BeEquivalentTo(expectedFacets);
}
[Fact(DisplayName = "Facet seal Merkle root is deterministic")]
public void FacetSeal_MerkleRootIsDeterministic()
{
// Arrange
var imageDigest = "sha256:determinism-test";
var fixedTime = new DateTimeOffset(2026, 1, 5, 12, 0, 0, TimeSpan.Zero);
var fixedMerkleRoot = "sha256:deterministic-root-hash-for-testing";
// Act - Create seals with same fixed root
var seal1 = CreateTestSealDeterministic(imageDigest, fixedTime, fixedMerkleRoot);
var seal2 = CreateTestSealDeterministic(imageDigest, fixedTime, fixedMerkleRoot);
// Assert
seal1.CombinedMerkleRoot.Should().Be(seal2.CombinedMerkleRoot);
seal1.ImageDigest.Should().Be(seal2.ImageDigest);
seal1.CreatedAt.Should().Be(seal2.CreatedAt);
}
#endregion
#region Admission Validation Tests
[Fact(DisplayName = "Admission allows when facet validation not required")]
public async Task Admission_AllowsWhenValidationNotRequired()
{
// Arrange
var validator = CreateMockValidator();
var request = CreateAdmissionRequest(
imageDigest: "sha256:any-image",
namespaceAnnotations: new Dictionary<string, string>()); // No annotation
// Act
var result = await validator.ValidateAsync(request);
// Assert
result.Allowed.Should().BeTrue();
result.Message.Should().Contain("not required");
}
[Fact(DisplayName = "Admission allows when annotation is false")]
public async Task Admission_AllowsWhenAnnotationFalse()
{
// Arrange
var validator = CreateMockValidator();
var request = CreateAdmissionRequest(
imageDigest: "sha256:any-image",
namespaceAnnotations: new Dictionary<string, string>
{
["stellaops.io/facet-seal-required"] = "false"
});
// Act
var result = await validator.ValidateAsync(request);
// Assert
result.Allowed.Should().BeTrue();
}
[Fact(DisplayName = "Admission denies when seal missing and required")]
public async Task Admission_DeniesWhenSealMissingAndRequired()
{
// Arrange
var mockSealStore = new Mock<IFacetSealStore>();
mockSealStore
.Setup(x => x.GetLatestSealAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((FacetSeal?)null);
var validator = CreateMockValidator(mockSealStore.Object);
var request = CreateAdmissionRequest(
imageDigest: "sha256:missing-seal",
namespaceAnnotations: new Dictionary<string, string>
{
["stellaops.io/facet-seal-required"] = "true"
},
requireSeal: true);
// Act
var result = await validator.ValidateAsync(request);
// Assert
result.Allowed.Should().BeFalse();
result.Code.Should().Be("facet.seal.missing");
}
[Fact(DisplayName = "Admission allows when seal exists and no drift")]
public async Task Admission_AllowsWhenSealExistsNoDrift()
{
// Arrange
var imageDigest = "sha256:sealed-image";
var seal = CreateTestSeal(imageDigest);
var mockSealStore = new Mock<IFacetSealStore>();
mockSealStore
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
.ReturnsAsync(seal);
var validator = CreateMockValidator(mockSealStore.Object);
var request = CreateAdmissionRequest(
imageDigest: imageDigest,
namespaceAnnotations: new Dictionary<string, string>
{
["stellaops.io/facet-seal-required"] = "true"
});
// Act
var result = await validator.ValidateAsync(request);
// Assert
result.Allowed.Should().BeTrue();
result.Message.Should().Contain("verified");
}
#endregion
#region Drift Detection Tests
[Fact(DisplayName = "Drift detection identifies added files")]
public async Task DriftDetection_IdentifiesAddedFiles()
{
// Arrange
var imageDigest = "sha256:drift-test";
var baselineSeal = CreateTestSeal(imageDigest);
var currentSeal = CreateTestSealWithAdditionalFiles(imageDigest, 5);
var mockDriftDetector = new Mock<IFacetDriftDetector>();
mockDriftDetector
.Setup(x => x.DetectDriftAsync(baselineSeal, currentSeal, It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateDriftReport(QuotaVerdict.Warning, 5));
// Act
var report = await mockDriftDetector.Object.DetectDriftAsync(baselineSeal, currentSeal);
// Assert
report.TotalChangedFiles.Should().Be(5);
report.OverallVerdict.Should().Be(QuotaVerdict.Warning);
}
[Fact(DisplayName = "Drift quota Ok allows admission")]
public async Task DriftQuotaOk_AllowsAdmission()
{
// Arrange
var imageDigest = "sha256:ok-drift";
var seal = CreateTestSeal(imageDigest, merkleRoot: "baseline-root");
var currentSeal = CreateTestSeal(imageDigest, merkleRoot: "current-root");
var driftReport = CreateDriftReport(QuotaVerdict.Ok, 2);
var mockSealStore = new Mock<IFacetSealStore>();
mockSealStore
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
.ReturnsAsync(seal);
mockSealStore
.Setup(x => x.GetByCombinedRootAsync("current-root", It.IsAny<CancellationToken>()))
.ReturnsAsync(currentSeal);
var mockDriftDetector = new Mock<IFacetDriftDetector>();
mockDriftDetector
.Setup(x => x.DetectDriftAsync(seal, currentSeal, It.IsAny<CancellationToken>()))
.ReturnsAsync(driftReport);
var validator = CreateMockValidator(mockSealStore.Object, mockDriftDetector.Object);
var request = CreateAdmissionRequest(
imageDigest: imageDigest,
namespaceAnnotations: new Dictionary<string, string>
{
["stellaops.io/facet-seal-required"] = "true"
},
currentSealRoot: "current-root");
// Act
var result = await validator.ValidateAsync(request);
// Assert
result.Allowed.Should().BeTrue();
result.Message.Should().Contain("OK");
}
[Fact(DisplayName = "Drift quota Warning allows with warning")]
public async Task DriftQuotaWarning_AllowsWithWarning()
{
// Arrange
var imageDigest = "sha256:warning-drift";
var seal = CreateTestSeal(imageDigest, merkleRoot: "baseline-root");
var currentSeal = CreateTestSeal(imageDigest, merkleRoot: "current-root");
var driftReport = CreateDriftReport(QuotaVerdict.Warning, 10);
var mockSealStore = new Mock<IFacetSealStore>();
mockSealStore
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
.ReturnsAsync(seal);
mockSealStore
.Setup(x => x.GetByCombinedRootAsync("current-root", It.IsAny<CancellationToken>()))
.ReturnsAsync(currentSeal);
var mockDriftDetector = new Mock<IFacetDriftDetector>();
mockDriftDetector
.Setup(x => x.DetectDriftAsync(seal, currentSeal, It.IsAny<CancellationToken>()))
.ReturnsAsync(driftReport);
var validator = CreateMockValidator(mockSealStore.Object, mockDriftDetector.Object);
var request = CreateAdmissionRequest(
imageDigest: imageDigest,
namespaceAnnotations: new Dictionary<string, string>
{
["stellaops.io/facet-seal-required"] = "true"
},
currentSealRoot: "current-root");
// Act
var result = await validator.ValidateAsync(request);
// Assert
result.Allowed.Should().BeTrue();
result.IsWarning.Should().BeTrue();
result.Code.Should().Be("facet.quota.warning");
}
[Fact(DisplayName = "Drift quota Blocked denies admission")]
public async Task DriftQuotaBlocked_DeniesAdmission()
{
// Arrange
var imageDigest = "sha256:blocked-drift";
var seal = CreateTestSeal(imageDigest, merkleRoot: "baseline-root");
var currentSeal = CreateTestSeal(imageDigest, merkleRoot: "current-root");
var driftReport = CreateDriftReport(QuotaVerdict.Blocked, 50);
var mockSealStore = new Mock<IFacetSealStore>();
mockSealStore
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
.ReturnsAsync(seal);
mockSealStore
.Setup(x => x.GetByCombinedRootAsync("current-root", It.IsAny<CancellationToken>()))
.ReturnsAsync(currentSeal);
var mockDriftDetector = new Mock<IFacetDriftDetector>();
mockDriftDetector
.Setup(x => x.DetectDriftAsync(seal, currentSeal, It.IsAny<CancellationToken>()))
.ReturnsAsync(driftReport);
var validator = CreateMockValidator(mockSealStore.Object, mockDriftDetector.Object);
var request = CreateAdmissionRequest(
imageDigest: imageDigest,
namespaceAnnotations: new Dictionary<string, string>
{
["stellaops.io/facet-seal-required"] = "true"
},
currentSealRoot: "current-root");
// Act
var result = await validator.ValidateAsync(request);
// Assert
result.Allowed.Should().BeFalse();
result.Code.Should().Be("facet.quota.exceeded");
}
[Fact(DisplayName = "Drift RequiresVex denies admission")]
public async Task DriftRequiresVex_DeniesAdmission()
{
// Arrange
var imageDigest = "sha256:vex-required";
var seal = CreateTestSeal(imageDigest, merkleRoot: "baseline-root");
var currentSeal = CreateTestSeal(imageDigest, merkleRoot: "current-root");
var driftReport = CreateDriftReport(QuotaVerdict.RequiresVex, 30);
var mockSealStore = new Mock<IFacetSealStore>();
mockSealStore
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
.ReturnsAsync(seal);
mockSealStore
.Setup(x => x.GetByCombinedRootAsync("current-root", It.IsAny<CancellationToken>()))
.ReturnsAsync(currentSeal);
var mockDriftDetector = new Mock<IFacetDriftDetector>();
mockDriftDetector
.Setup(x => x.DetectDriftAsync(seal, currentSeal, It.IsAny<CancellationToken>()))
.ReturnsAsync(driftReport);
var validator = CreateMockValidator(mockSealStore.Object, mockDriftDetector.Object);
var request = CreateAdmissionRequest(
imageDigest: imageDigest,
namespaceAnnotations: new Dictionary<string, string>
{
["stellaops.io/facet-seal-required"] = "true"
},
currentSealRoot: "current-root");
// Act
var result = await validator.ValidateAsync(request);
// Assert
result.Allowed.Should().BeFalse();
result.Code.Should().Be("facet.vex.required");
}
#endregion
#region Full Workflow Tests
[Fact(DisplayName = "Full workflow: seal -> deploy (no drift) -> allow")]
public async Task FullWorkflow_SealDeployNoDrift_Allow()
{
// Arrange - Create seal
var imageDigest = "sha256:workflow-test-1";
var seal = CreateTestSeal(imageDigest);
// Arrange - Mock stores
var mockSealStore = new Mock<IFacetSealStore>();
mockSealStore
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
.ReturnsAsync(seal);
var validator = CreateMockValidator(mockSealStore.Object);
// Act - Simulate deploy
var request = CreateAdmissionRequest(
imageDigest: imageDigest,
namespaceAnnotations: new Dictionary<string, string>
{
["stellaops.io/facet-seal-required"] = "true"
});
var result = await validator.ValidateAsync(request);
// Assert
result.Allowed.Should().BeTrue();
}
[Fact(DisplayName = "Full workflow: seal -> deploy (with drift) -> quota check")]
public async Task FullWorkflow_SealDeployWithDrift_QuotaCheck()
{
// Arrange - Create seals (baseline and current)
var imageDigest = "sha256:workflow-test-2";
var baselineSeal = CreateTestSeal(imageDigest, merkleRoot: "baseline-root");
var currentSeal = CreateTestSealWithAdditionalFiles(imageDigest, 15, "current-root");
// Arrange - Mock stores
var mockSealStore = new Mock<IFacetSealStore>();
mockSealStore
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
.ReturnsAsync(baselineSeal);
mockSealStore
.Setup(x => x.GetByCombinedRootAsync("current-root", It.IsAny<CancellationToken>()))
.ReturnsAsync(currentSeal);
var mockDriftDetector = new Mock<IFacetDriftDetector>();
mockDriftDetector
.Setup(x => x.DetectDriftAsync(baselineSeal, currentSeal, It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateDriftReport(QuotaVerdict.Warning, 15));
var validator = CreateMockValidator(mockSealStore.Object, mockDriftDetector.Object);
// Act - Simulate deploy
var request = CreateAdmissionRequest(
imageDigest: imageDigest,
namespaceAnnotations: new Dictionary<string, string>
{
["stellaops.io/facet-seal-required"] = "true"
},
currentSealRoot: "current-root");
var result = await validator.ValidateAsync(request);
// Assert - Should allow with warning
result.Allowed.Should().BeTrue();
result.IsWarning.Should().BeTrue();
}
[Fact(DisplayName = "Full workflow: seal -> deploy (excessive drift) -> deny")]
public async Task FullWorkflow_SealDeployExcessiveDrift_Deny()
{
// Arrange - Create seals with significant drift
var imageDigest = "sha256:workflow-test-3";
var baselineSeal = CreateTestSeal(imageDigest, merkleRoot: "baseline-root");
var currentSeal = CreateTestSealWithAdditionalFiles(imageDigest, 100, "current-root");
// Arrange - Mock stores
var mockSealStore = new Mock<IFacetSealStore>();
mockSealStore
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
.ReturnsAsync(baselineSeal);
mockSealStore
.Setup(x => x.GetByCombinedRootAsync("current-root", It.IsAny<CancellationToken>()))
.ReturnsAsync(currentSeal);
var mockDriftDetector = new Mock<IFacetDriftDetector>();
mockDriftDetector
.Setup(x => x.DetectDriftAsync(baselineSeal, currentSeal, It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateDriftReport(QuotaVerdict.Blocked, 100));
var validator = CreateMockValidator(mockSealStore.Object, mockDriftDetector.Object);
// Act - Simulate deploy
var request = CreateAdmissionRequest(
imageDigest: imageDigest,
namespaceAnnotations: new Dictionary<string, string>
{
["stellaops.io/facet-seal-required"] = "true"
},
currentSealRoot: "current-root");
var result = await validator.ValidateAsync(request);
// Assert - Should deny
result.Allowed.Should().BeFalse();
result.Code.Should().Be("facet.quota.exceeded");
}
#endregion
#region Determinism Tests
[Fact(DisplayName = "Admission decisions are deterministic")]
public async Task AdmissionDecisions_AreDeterministic()
{
// Arrange
var imageDigest = "sha256:determinism-admission";
var seal = CreateTestSeal(imageDigest);
var mockSealStore = new Mock<IFacetSealStore>();
mockSealStore
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
.ReturnsAsync(seal);
var validator = CreateMockValidator(mockSealStore.Object);
var request = CreateAdmissionRequest(
imageDigest: imageDigest,
namespaceAnnotations: new Dictionary<string, string>
{
["stellaops.io/facet-seal-required"] = "true"
});
// Act
var results = await Task.WhenAll(
Enumerable.Range(0, 10).Select(_ => validator.ValidateAsync(request)));
// Assert
results.Select(r => r.Allowed).Distinct().Should().HaveCount(1);
results.Select(r => r.Code).Distinct().Should().HaveCount(1);
}
#endregion
#region Helper Methods
private static FacetSeal CreateTestSeal(
string imageDigest,
DateTimeOffset? createdAt = null,
string? merkleRoot = null)
{
return new FacetSeal
{
ImageDigest = imageDigest,
CreatedAt = createdAt ?? DateTimeOffset.UtcNow,
CombinedMerkleRoot = merkleRoot ?? $"sha256:{Guid.NewGuid():N}",
Facets = ImmutableArray.Create(
new FacetEntry
{
FacetId = "runtime",
Name = "Runtime Binaries",
Category = FacetCategory.Binaries,
Selectors = ImmutableArray<string>.Empty,
MerkleRoot = $"sha256:{Guid.NewGuid():N}",
FileCount = 10,
TotalBytes = 1024000,
Files = ImmutableArray<FacetFileEntry>.Empty
})
};
}
private static FacetSeal CreateTestSealDeterministic(
string imageDigest,
DateTimeOffset createdAt,
string merkleRoot)
{
return new FacetSeal
{
ImageDigest = imageDigest,
CreatedAt = createdAt,
CombinedMerkleRoot = merkleRoot,
Facets = ImmutableArray.Create(
new FacetEntry
{
FacetId = "runtime",
Name = "Runtime Binaries",
Category = FacetCategory.Binaries,
Selectors = ImmutableArray<string>.Empty,
MerkleRoot = "sha256:deterministic-facet-root",
FileCount = 10,
TotalBytes = 1024000,
Files = ImmutableArray<FacetFileEntry>.Empty
})
};
}
private static FacetSeal CreateTestSealWithFacets(string imageDigest, string[] facetIds)
{
var facets = facetIds.Select(id => new FacetEntry
{
FacetId = id,
Name = $"{id} facet",
Category = FacetCategory.Custom,
Selectors = ImmutableArray<string>.Empty,
MerkleRoot = $"sha256:{Guid.NewGuid():N}",
FileCount = 5,
TotalBytes = 51200,
Files = ImmutableArray<FacetFileEntry>.Empty
}).ToImmutableArray();
return new FacetSeal
{
ImageDigest = imageDigest,
CreatedAt = DateTimeOffset.UtcNow,
CombinedMerkleRoot = $"sha256:{Guid.NewGuid():N}",
Facets = facets
};
}
private static FacetSeal CreateTestSealWithAdditionalFiles(
string imageDigest,
int additionalFileCount,
string? merkleRoot = null)
{
var files = Enumerable.Range(0, additionalFileCount)
.Select(i => new FacetFileEntry($"/added/{i}.bin", $"sha256:file{i}", 1024, null))
.ToImmutableArray();
return new FacetSeal
{
ImageDigest = imageDigest,
CreatedAt = DateTimeOffset.UtcNow,
CombinedMerkleRoot = merkleRoot ?? $"sha256:{Guid.NewGuid():N}",
Facets = ImmutableArray.Create(
new FacetEntry
{
FacetId = "runtime",
Name = "Runtime Binaries",
Category = FacetCategory.Binaries,
Selectors = ImmutableArray<string>.Empty,
MerkleRoot = $"sha256:{Guid.NewGuid():N}",
FileCount = 10 + additionalFileCount,
TotalBytes = 1024000 + (additionalFileCount * 1024),
Files = files
})
};
}
private static FacetDriftReport CreateDriftReport(QuotaVerdict verdict, int changedFiles)
{
var addedFiles = changedFiles > 0
? Enumerable.Range(0, changedFiles)
.Select(i => new FacetFileEntry($"/added/{i}", $"sha256:hash{i}", 1024, null))
.ToImmutableArray()
: ImmutableArray<FacetFileEntry>.Empty;
var facetDrifts = changedFiles > 0
? ImmutableArray.Create(new FacetDrift
{
FacetId = "runtime",
BaselineFileCount = 10,
Added = addedFiles,
Removed = ImmutableArray<FacetFileEntry>.Empty,
Modified = ImmutableArray<FacetFileModification>.Empty,
DriftScore = changedFiles * 10m,
QuotaVerdict = verdict
})
: ImmutableArray<FacetDrift>.Empty;
return new FacetDriftReport
{
ImageDigest = "sha256:test",
BaselineSealId = "baseline",
AnalyzedAt = DateTimeOffset.UtcNow,
OverallVerdict = verdict,
FacetDrifts = facetDrifts
};
}
private static FacetAdmissionRequest CreateAdmissionRequest(
string imageDigest,
Dictionary<string, string> namespaceAnnotations,
bool requireSeal = false,
string? currentSealRoot = null)
{
return new FacetAdmissionRequest
{
ImageDigest = imageDigest,
NamespaceAnnotations = namespaceAnnotations,
RequireSeal = requireSeal,
CurrentSealRoot = currentSealRoot
};
}
private static IFacetAdmissionValidator CreateMockValidator(
IFacetSealStore? sealStore = null,
IFacetDriftDetector? driftDetector = null)
{
sealStore ??= new Mock<IFacetSealStore>().Object;
driftDetector ??= new Mock<IFacetDriftDetector>().Object;
return new FacetAdmissionValidator(sealStore, driftDetector);
}
#endregion
}
// Request model for E2E tests (mirrors production model)
internal sealed record FacetAdmissionRequest
{
public required string ImageDigest { get; init; }
public IReadOnlyDictionary<string, string>? NamespaceAnnotations { get; init; }
public bool RequireSeal { get; init; }
public string? CurrentSealRoot { get; init; }
}
// Result model for E2E tests (mirrors production model)
internal sealed record FacetAdmissionResult
{
public required bool Allowed { get; init; }
public bool IsWarning { get; init; }
public string? Code { get; init; }
public required string Message { get; init; }
public static FacetAdmissionResult Allow(string message)
=> new() { Allowed = true, Message = message };
public static FacetAdmissionResult AllowWithWarning(string code, string message)
=> new() { Allowed = true, IsWarning = true, Code = code, Message = message };
public static FacetAdmissionResult Deny(string code, string message)
=> new() { Allowed = false, Code = code, Message = message };
}
// Validator interface for E2E tests
internal interface IFacetAdmissionValidator
{
Task<FacetAdmissionResult> ValidateAsync(
FacetAdmissionRequest request,
CancellationToken ct = default);
}
// Validator implementation for E2E tests
internal sealed class FacetAdmissionValidator : IFacetAdmissionValidator
{
private readonly IFacetSealStore _sealStore;
private readonly IFacetDriftDetector _driftDetector;
public const string FacetSealRequiredAnnotation = "stellaops.io/facet-seal-required";
public FacetAdmissionValidator(
IFacetSealStore sealStore,
IFacetDriftDetector driftDetector)
{
_sealStore = sealStore;
_driftDetector = driftDetector;
}
public async Task<FacetAdmissionResult> ValidateAsync(
FacetAdmissionRequest request,
CancellationToken ct = default)
{
if (!IsFacetValidationRequired(request.NamespaceAnnotations))
{
return FacetAdmissionResult.Allow("Facet validation not required for namespace");
}
var seal = await _sealStore.GetLatestSealAsync(request.ImageDigest, ct)
.ConfigureAwait(false);
if (seal is null)
{
if (request.RequireSeal)
{
return FacetAdmissionResult.Deny(
"facet.seal.missing",
$"No facet seal found for image {request.ImageDigest}");
}
return FacetAdmissionResult.Allow("No seal found, seal not required");
}
if (request.CurrentSealRoot is null)
{
return FacetAdmissionResult.Allow(
$"Facet seal verified: {TruncateHash(seal.CombinedMerkleRoot)}");
}
var currentSeal = await _sealStore.GetByCombinedRootAsync(request.CurrentSealRoot, ct)
.ConfigureAwait(false);
if (currentSeal is null)
{
return FacetAdmissionResult.AllowWithWarning(
"facet.seal.current_not_found",
$"Current seal not found, using baseline: {TruncateHash(seal.CombinedMerkleRoot)}");
}
var driftReport = await _driftDetector.DetectDriftAsync(seal, currentSeal, ct)
.ConfigureAwait(false);
return driftReport.OverallVerdict switch
{
QuotaVerdict.Ok => FacetAdmissionResult.Allow(
$"Facet quotas OK: {driftReport.TotalChangedFiles} changes within limits"),
QuotaVerdict.Warning => FacetAdmissionResult.AllowWithWarning(
"facet.quota.warning",
$"Facet drift warning: {FormatBreaches(driftReport)}"),
QuotaVerdict.Blocked => FacetAdmissionResult.Deny(
"facet.quota.exceeded",
$"Facet quota exceeded: {FormatBreaches(driftReport)}"),
QuotaVerdict.RequiresVex => FacetAdmissionResult.Deny(
"facet.vex.required",
$"Facet drift requires VEX authorization: {FormatBreaches(driftReport)}"),
_ => FacetAdmissionResult.Allow("Unknown verdict - allowing")
};
}
private static bool IsFacetValidationRequired(IReadOnlyDictionary<string, string>? annotations)
{
if (annotations is null) return false;
return annotations.TryGetValue(FacetSealRequiredAnnotation, out var value) &&
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
}
private static string FormatBreaches(FacetDriftReport report)
{
var breaches = report.FacetDrifts
.Where(d => d.QuotaVerdict != QuotaVerdict.Ok)
.Select(d => $"{d.FacetId}({d.ChurnPercent:F1}%)")
.ToArray();
return string.Join(", ", breaches);
}
private static string TruncateHash(string? hash)
{
if (string.IsNullOrEmpty(hash)) return "(none)";
return hash.Length > 16 ? $"{hash[..8]}...{hash[^8..]}" : hash;
}
}

View File

@@ -45,6 +45,9 @@
<ProjectReference Include="..\..\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Testing.Determinism\StellaOps.Testing.Determinism.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Testing.AirGap\StellaOps.Testing.AirGap.csproj" />
<!-- Facet seal admission E2E tests (SPRINT_20260105_002_004_CLI) -->
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Facet\StellaOps.Facet.csproj" />
</ItemGroup>
<ItemGroup>