// // Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later. // // Sprint: SPRINT_20260105_002_005_TEST_cross_cutting // Task: CCUT-020 using System.Collections.Immutable; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.TestKit; using StellaOps.Testing.ConfigDiff; using Xunit; namespace StellaOps.Concelier.ConfigDiff.Tests; /// /// Config-diff tests for the Concelier module. /// Verifies that configuration changes produce only expected behavioral deltas. /// [Trait("Category", TestCategories.ConfigDiff)] [Trait("Category", TestCategories.Integration)] [Trait("BlastRadius", TestCategories.BlastRadius.Advisories)] public class ConcelierConfigDiffTests : ConfigDiffTestBase { /// /// Initializes a new instance of the class. /// public ConcelierConfigDiffTests() : base( new ConfigDiffTestConfig(StrictMode: true), NullLogger.Instance) { } /// /// Verifies that changing cache timeout only affects cache behavior. /// [Fact] public async Task ChangingCacheTimeout_OnlyAffectsCacheBehavior() { // Arrange var baselineConfig = new ConcelierTestConfig { CacheTimeoutMinutes = 30, MaxConcurrentDownloads = 10, RetryCount = 3 }; var changedConfig = baselineConfig with { CacheTimeoutMinutes = 60 }; // Act var result = await TestConfigIsolationAsync( baselineConfig, changedConfig, changedSetting: "CacheTimeoutMinutes", unrelatedBehaviors: [ async config => await GetDownloadBehaviorAsync(config), async config => await GetRetryBehaviorAsync(config), async config => await GetParseBehaviorAsync(config) ]); // Assert result.IsSuccess.Should().BeTrue( because: "changing cache timeout should not affect other behaviors"); } /// /// Verifies that changing retry count produces expected behavioral delta. /// [Fact] public async Task ChangingRetryCount_ProducesExpectedDelta() { // Arrange var baselineConfig = new ConcelierTestConfig { RetryCount = 3 }; var changedConfig = new ConcelierTestConfig { RetryCount = 5 }; var expectedDelta = new ConfigDelta( ChangedBehaviors: ["MaxRetryAttempts", "FailureRecoveryWindow"], BehaviorDeltas: [ new BehaviorDelta("MaxRetryAttempts", "3", "5", null), new BehaviorDelta("FailureRecoveryWindow", "increase", null, "More retries extend recovery window") ]); // Act var result = await TestConfigBehavioralDeltaAsync( baselineConfig, changedConfig, getBehavior: async config => await CaptureRetryBehaviorAsync(config), computeDelta: ComputeBehaviorSnapshotDelta, expectedDelta: expectedDelta); // Assert result.IsSuccess.Should().BeTrue( because: "retry count change should produce expected behavioral delta"); } /// /// Verifies that changing max concurrent downloads only affects concurrency. /// [Fact] public async Task ChangingMaxConcurrentDownloads_OnlyAffectsConcurrency() { // Arrange var baselineConfig = new ConcelierTestConfig { MaxConcurrentDownloads = 5 }; var changedConfig = new ConcelierTestConfig { MaxConcurrentDownloads = 20 }; // Act var result = await TestConfigIsolationAsync( baselineConfig, changedConfig, changedSetting: "MaxConcurrentDownloads", unrelatedBehaviors: [ async config => await GetCacheBehaviorAsync(config), async config => await GetRetryBehaviorAsync(config), async config => await GetParseBehaviorAsync(config) ]); // Assert result.IsSuccess.Should().BeTrue( because: "changing concurrency should not affect cache, retry, or parsing"); } /// /// Verifies that enabling strict validation produces expected changes. /// [Fact] public async Task EnablingStrictValidation_ProducesExpectedDelta() { // Arrange var baselineConfig = new ConcelierTestConfig { StrictValidation = false }; var changedConfig = new ConcelierTestConfig { StrictValidation = true }; var expectedDelta = new ConfigDelta( ChangedBehaviors: ["ValidationStrictness", "RejectionRate"], BehaviorDeltas: [ new BehaviorDelta("ValidationStrictness", "relaxed", "strict", null), new BehaviorDelta("RejectionRate", "increase", null, "Strict validation rejects more malformed advisories") ]); // Act var result = await TestConfigBehavioralDeltaAsync( baselineConfig, changedConfig, getBehavior: async config => await CaptureValidationBehaviorAsync(config), computeDelta: ComputeBehaviorSnapshotDelta, expectedDelta: expectedDelta); // Assert result.IsSuccess.Should().BeTrue(); } // Helper methods to capture behaviors private static Task GetDownloadBehaviorAsync(ConcelierTestConfig config) { return Task.FromResult(new { MaxConcurrent = config.MaxConcurrentDownloads }); } private static Task GetRetryBehaviorAsync(ConcelierTestConfig config) { return Task.FromResult(new { RetryCount = config.RetryCount }); } private static Task GetCacheBehaviorAsync(ConcelierTestConfig config) { return Task.FromResult(new { CacheTimeout = config.CacheTimeoutMinutes }); } private static Task GetParseBehaviorAsync(ConcelierTestConfig config) { return Task.FromResult(new { ParseMode = "standard" }); } private static Task CaptureRetryBehaviorAsync(ConcelierTestConfig config) { var snapshot = new BehaviorSnapshot( ConfigurationId: $"retry-{config.RetryCount}", Behaviors: [ new CapturedBehavior("MaxRetryAttempts", config.RetryCount.ToString(), DateTimeOffset.UtcNow), new CapturedBehavior("FailureRecoveryWindow", config.RetryCount > 3 ? "increase" : "standard", DateTimeOffset.UtcNow) ], CapturedAt: DateTimeOffset.UtcNow); return Task.FromResult(snapshot); } private static Task CaptureValidationBehaviorAsync(ConcelierTestConfig config) { var snapshot = new BehaviorSnapshot( ConfigurationId: $"validation-{config.StrictValidation}", Behaviors: [ new CapturedBehavior("ValidationStrictness", config.StrictValidation ? "strict" : "relaxed", DateTimeOffset.UtcNow), new CapturedBehavior("RejectionRate", config.StrictValidation ? "increase" : "standard", DateTimeOffset.UtcNow) ], CapturedAt: DateTimeOffset.UtcNow); return Task.FromResult(snapshot); } } /// /// Test configuration for Concelier module. /// public sealed record ConcelierTestConfig { public int CacheTimeoutMinutes { get; init; } = 30; public int MaxConcurrentDownloads { get; init; } = 10; public int RetryCount { get; init; } = 3; public bool StrictValidation { get; init; } = false; public TimeSpan RequestTimeout { get; init; } = TimeSpan.FromSeconds(30); }