sprints completion. new product advisories prepared
This commit is contained in:
@@ -0,0 +1,493 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// AiCodeGuardSignalContextExtensionsTests.cs
|
||||
// Sprint: SPRINT_20260112_010_POLICY_ai_code_guard_policy
|
||||
// Task: POLICY-AIGUARD-004 - Deterministic tests for AI Code Guard signal evaluation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Policy.AiCodeGuard;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.PolicyDsl.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for AI Code Guard signal context extensions.
|
||||
/// </summary>
|
||||
public sealed class AiCodeGuardSignalContextExtensionsTests
|
||||
{
|
||||
#region Test Fixtures
|
||||
|
||||
private static IAiCodeGuardEvidenceProvider CreateEmptyProvider()
|
||||
{
|
||||
return new TestAiCodeGuardEvidenceProvider
|
||||
{
|
||||
Findings = ImmutableList<AiCodeGuardFinding>.Empty,
|
||||
Overrides = ImmutableList<AiCodeGuardOverrideRecord>.Empty,
|
||||
VerdictStatus = AiCodeGuardVerdictStatus.Pass,
|
||||
AiGeneratedPercentage = null,
|
||||
ScannerInfo = null
|
||||
};
|
||||
}
|
||||
|
||||
private static IAiCodeGuardEvidenceProvider CreateProviderWithFindings()
|
||||
{
|
||||
return new TestAiCodeGuardEvidenceProvider
|
||||
{
|
||||
Findings = ImmutableList.Create(
|
||||
new AiCodeGuardFinding
|
||||
{
|
||||
Id = "finding-1",
|
||||
Category = "InsecurePattern",
|
||||
Severity = "high",
|
||||
Confidence = 0.85,
|
||||
RuleId = "guard/sql-injection",
|
||||
FilePath = "src/database.cs",
|
||||
StartLine = 42,
|
||||
EndLine = 48,
|
||||
Description = "Potential SQL injection in AI-generated code"
|
||||
},
|
||||
new AiCodeGuardFinding
|
||||
{
|
||||
Id = "finding-2",
|
||||
Category = "AiGenerated",
|
||||
Severity = "medium",
|
||||
Confidence = 0.92,
|
||||
RuleId = "guard/ai-detected",
|
||||
FilePath = "src/utils.cs",
|
||||
StartLine = 100,
|
||||
EndLine = 120,
|
||||
Description = "AI-generated code detected"
|
||||
},
|
||||
new AiCodeGuardFinding
|
||||
{
|
||||
Id = "finding-3",
|
||||
Category = "Hallucination",
|
||||
Severity = "critical",
|
||||
Confidence = 0.78,
|
||||
RuleId = "guard/api-hallucination",
|
||||
FilePath = "src/api.cs",
|
||||
StartLine = 200,
|
||||
EndLine = 210,
|
||||
Description = "Reference to non-existent API method"
|
||||
}
|
||||
),
|
||||
Overrides = ImmutableList<AiCodeGuardOverrideRecord>.Empty,
|
||||
VerdictStatus = AiCodeGuardVerdictStatus.Fail,
|
||||
AiGeneratedPercentage = 42.5,
|
||||
ScannerInfo = new AiCodeGuardScannerInfo
|
||||
{
|
||||
ScannerVersion = "1.0.0",
|
||||
ModelVersion = "2024.1",
|
||||
ConfidenceThreshold = 0.7,
|
||||
EnabledCategories = ImmutableList.Create("AiGenerated", "InsecurePattern", "Hallucination")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static IAiCodeGuardEvidenceProvider CreateProviderWithOverrides()
|
||||
{
|
||||
return new TestAiCodeGuardEvidenceProvider
|
||||
{
|
||||
Findings = ImmutableList.Create(
|
||||
new AiCodeGuardFinding
|
||||
{
|
||||
Id = "finding-1",
|
||||
Category = "InsecurePattern",
|
||||
Severity = "high",
|
||||
Confidence = 0.85,
|
||||
RuleId = "guard/sql-injection",
|
||||
FilePath = "src/database.cs",
|
||||
StartLine = 42,
|
||||
EndLine = 48,
|
||||
Description = "Potential SQL injection in AI-generated code"
|
||||
},
|
||||
new AiCodeGuardFinding
|
||||
{
|
||||
Id = "finding-2",
|
||||
Category = "AiGenerated",
|
||||
Severity = "low",
|
||||
Confidence = 0.92,
|
||||
RuleId = "guard/ai-detected",
|
||||
FilePath = "src/utils.cs",
|
||||
StartLine = 100,
|
||||
EndLine = 120,
|
||||
Description = "AI-generated code detected"
|
||||
}
|
||||
),
|
||||
Overrides = ImmutableList.Create(
|
||||
new AiCodeGuardOverrideRecord
|
||||
{
|
||||
FindingId = "finding-1",
|
||||
Action = "suppress",
|
||||
Justification = "False positive - parameterized query is safe",
|
||||
ApprovedBy = "security-team@example.com",
|
||||
ApprovedAt = DateTimeOffset.UtcNow.AddDays(-7),
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(23)
|
||||
}
|
||||
),
|
||||
VerdictStatus = AiCodeGuardVerdictStatus.PassWithWarnings,
|
||||
AiGeneratedPercentage = 15.0,
|
||||
ScannerInfo = null
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Signal Tests
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_EmptyProvider_SetsCorrectSignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateEmptyProvider();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.False(context.GetSignal<bool>("guard.has_finding"));
|
||||
Assert.Equal(0, context.GetSignal<int>("guard.count"));
|
||||
Assert.Equal("pass", context.GetSignal<string>("guard.verdict"));
|
||||
Assert.Equal("allow", context.GetSignal<string>("guard.recommendation"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithFindings_SetsSeveritySignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.GetSignal<bool>("guard.has_finding"));
|
||||
Assert.Equal(3, context.GetSignal<int>("guard.count"));
|
||||
Assert.True(context.GetSignal<bool>("guard.severity.critical"));
|
||||
Assert.True(context.GetSignal<bool>("guard.severity.high"));
|
||||
Assert.True(context.GetSignal<bool>("guard.severity.medium"));
|
||||
Assert.False(context.GetSignal<bool>("guard.severity.low"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithFindings_SetsCategorySignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.GetSignal<bool>("guard.category.insecure_pattern"));
|
||||
Assert.True(context.GetSignal<bool>("guard.category.ai_generated"));
|
||||
Assert.True(context.GetSignal<bool>("guard.category.hallucination"));
|
||||
Assert.False(context.GetSignal<bool>("guard.category.license_risk"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithFindings_SetsVerdictSignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("fail", context.GetSignal<string>("guard.verdict"));
|
||||
Assert.True(context.GetSignal<bool>("guard.verdict.fail"));
|
||||
Assert.False(context.GetSignal<bool>("guard.verdict.pass"));
|
||||
Assert.Equal("block", context.GetSignal<string>("guard.recommendation"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithFindings_SetsAiPercentage()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(42.5, context.GetSignal<double?>("guard.ai_percentage"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Override Tests
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithOverrides_FiltersActiveFindingsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithOverrides();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, context.GetSignal<int>("guard.count")); // Total findings
|
||||
Assert.Equal(1, context.GetSignal<int>("guard.active_count")); // After suppression
|
||||
Assert.True(context.GetSignal<bool>("guard.has_active_finding"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithOverrides_SetsOverrideSignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithOverrides();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, context.GetSignal<int>("guard.override.count"));
|
||||
Assert.Equal(1, context.GetSignal<int>("guard.override.active_count"));
|
||||
Assert.Equal(0, context.GetSignal<int>("guard.override.expired_count"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithOverrides_SetsCorrectVerdict()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithOverrides();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("passwithwarnings", context.GetSignal<string>("guard.verdict"));
|
||||
Assert.True(context.GetSignal<bool>("guard.verdict.pass_with_warnings"));
|
||||
Assert.Equal("review", context.GetSignal<string>("guard.recommendation"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scanner Info Tests
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithScannerInfo_SetsScannerSignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("1.0.0", context.GetSignal<string>("guard.scanner.version"));
|
||||
Assert.Equal("2024.1", context.GetSignal<string>("guard.scanner.model_version"));
|
||||
Assert.Equal(0.7, context.GetSignal<double?>("guard.scanner.confidence_threshold"));
|
||||
Assert.Equal(3, context.GetSignal<int>("guard.scanner.category_count"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_NullScannerInfo_SetsNullScannerSignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateEmptyProvider();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Null(context.GetSignal<string>("guard.scanner.version"));
|
||||
Assert.Null(context.GetSignal<string>("guard.scanner.model_version"));
|
||||
Assert.Equal(0, context.GetSignal<int>("guard.scanner.category_count"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Nested Object Tests
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_SetsNestedGuardObject()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
var guard = context.GetSignal<IReadOnlyDictionary<string, object?>>("guard");
|
||||
Assert.NotNull(guard);
|
||||
Assert.True((bool)guard["has_finding"]!);
|
||||
Assert.Equal(3, guard["count"]);
|
||||
|
||||
var severity = guard["severity"] as IReadOnlyDictionary<string, object?>;
|
||||
Assert.NotNull(severity);
|
||||
Assert.True((bool)severity["critical"]!);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateExplainTrace_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act - create trace multiple times
|
||||
var trace1 = AiCodeGuardSignalBinder.CreateExplainTrace(evidenceContext);
|
||||
var trace2 = AiCodeGuardSignalBinder.CreateExplainTrace(evidenceContext);
|
||||
var trace3 = AiCodeGuardSignalBinder.CreateExplainTrace(evidenceContext);
|
||||
|
||||
// Assert - all traces should be identical
|
||||
Assert.Equal(trace1, trace2);
|
||||
Assert.Equal(trace2, trace3);
|
||||
|
||||
// Verify trace contains expected content
|
||||
Assert.Contains("guard.verdict=Fail", trace1);
|
||||
Assert.Contains("guard.total_findings=3", trace1);
|
||||
Assert.Contains("guard.ai_percentage=42.5", trace1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFindingSummary_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var summary1 = AiCodeGuardSignalBinder.CreateFindingSummary(evidenceContext);
|
||||
var summary2 = AiCodeGuardSignalBinder.CreateFindingSummary(evidenceContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(summary1, summary2);
|
||||
Assert.Contains("3 AI code guard finding(s)", summary1);
|
||||
Assert.Contains("1 critical", summary1);
|
||||
Assert.Contains("1 high", summary1);
|
||||
Assert.Contains("1 medium", summary1);
|
||||
Assert.Contains("AI-generated: 42.5%", summary1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFindingSummary_EmptyFindings_ReturnsNoFindings()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateEmptyProvider();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var summary = AiCodeGuardSignalBinder.CreateFindingSummary(evidenceContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("No AI code guard findings detected.", summary);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Simplified Result Tests
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardResult_SetsBasicSignals()
|
||||
{
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardResult(
|
||||
status: "fail",
|
||||
totalFindings: 5,
|
||||
criticalCount: 1,
|
||||
highCount: 2,
|
||||
mediumCount: 2,
|
||||
aiPercentage: 25.0)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("fail", context.GetSignal<string>("guard.verdict"));
|
||||
Assert.Equal(5, context.GetSignal<int>("guard.count"));
|
||||
Assert.True(context.GetSignal<bool>("guard.has_finding"));
|
||||
Assert.True(context.GetSignal<bool>("guard.severity.critical"));
|
||||
Assert.Equal(1, context.GetSignal<int>("guard.severity.critical_count"));
|
||||
Assert.True(context.GetSignal<bool>("guard.severity.high"));
|
||||
Assert.Equal(2, context.GetSignal<int>("guard.severity.high_count"));
|
||||
Assert.Equal(25.0, context.GetSignal<double?>("guard.ai_percentage"));
|
||||
Assert.Equal("block", context.GetSignal<string>("guard.recommendation"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardResult_PassStatus_SetsAllowRecommendation()
|
||||
{
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardResult(
|
||||
status: "pass",
|
||||
totalFindings: 0)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("pass", context.GetSignal<string>("guard.verdict"));
|
||||
Assert.Equal("allow", context.GetSignal<string>("guard.recommendation"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardResult_WarningStatus_SetsReviewRecommendation()
|
||||
{
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardResult(
|
||||
status: "pass_with_warnings",
|
||||
totalFindings: 2,
|
||||
mediumCount: 2)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("pass_with_warnings", context.GetSignal<string>("guard.verdict"));
|
||||
Assert.Equal("review", context.GetSignal<string>("guard.recommendation"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Provider Implementation
|
||||
|
||||
private sealed class TestAiCodeGuardEvidenceProvider : IAiCodeGuardEvidenceProvider
|
||||
{
|
||||
public ImmutableList<AiCodeGuardFinding> Findings { get; init; } = ImmutableList<AiCodeGuardFinding>.Empty;
|
||||
public ImmutableList<AiCodeGuardOverrideRecord> Overrides { get; init; } = ImmutableList<AiCodeGuardOverrideRecord>.Empty;
|
||||
public AiCodeGuardVerdictStatus VerdictStatus { get; init; }
|
||||
public double? AiGeneratedPercentage { get; init; }
|
||||
public AiCodeGuardScannerInfo? ScannerInfo { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user