save development progress

This commit is contained in:
StellaOps Bot
2025-12-25 23:09:58 +02:00
parent d71853ad7e
commit aa70af062e
351 changed files with 37683 additions and 150156 deletions

View File

@@ -0,0 +1,445 @@
// -----------------------------------------------------------------------------
// InterestScoreCalculatorTests.cs
// Sprint: SPRINT_8200_0013_0002_CONCEL_interest_scoring
// Task: ISCORE-8200-013
// Description: Unit tests for InterestScoreCalculator
// -----------------------------------------------------------------------------
using FluentAssertions;
using StellaOps.Concelier.Interest.Models;
using Xunit;
namespace StellaOps.Concelier.Interest.Tests;
public class InterestScoreCalculatorTests
{
private readonly InterestScoreCalculator _calculator;
private readonly InterestScoreWeights _defaultWeights = new();
public InterestScoreCalculatorTests()
{
_calculator = new InterestScoreCalculator(_defaultWeights);
}
[Fact]
public void Calculate_WithNoSignals_ReturnsBaseScore()
{
// Arrange
var input = new InterestScoreInput
{
CanonicalId = Guid.NewGuid(),
SbomMatches = [],
VexStatements = [],
RuntimeSignals = []
};
// Act
var result = _calculator.Calculate(input);
// Assert
// Only no_vex_na applies (0.15) when no signals
result.Score.Should().Be(0.15);
result.Reasons.Should().Contain("no_vex_na");
result.Reasons.Should().HaveCount(1);
}
[Fact]
public void Calculate_WithSbomMatch_AddsInSbomFactor()
{
// Arrange
var input = new InterestScoreInput
{
CanonicalId = Guid.NewGuid(),
SbomMatches =
[
new SbomMatch
{
SbomDigest = "sha256:abc123",
Purl = "pkg:npm/lodash@4.17.21",
IsReachable = false,
IsDeployed = false,
ScannedAt = DateTimeOffset.UtcNow
}
]
};
// Act
var result = _calculator.Calculate(input);
// Assert
result.Score.Should().Be(0.45); // in_sbom (0.30) + no_vex_na (0.15)
result.Reasons.Should().Contain("in_sbom");
result.Reasons.Should().Contain("no_vex_na");
}
[Fact]
public void Calculate_WithReachableSbomMatch_AddsReachableFactor()
{
// Arrange
var input = new InterestScoreInput
{
CanonicalId = Guid.NewGuid(),
SbomMatches =
[
new SbomMatch
{
SbomDigest = "sha256:abc123",
Purl = "pkg:npm/lodash@4.17.21",
IsReachable = true,
IsDeployed = false,
ScannedAt = DateTimeOffset.UtcNow
}
]
};
// Act
var result = _calculator.Calculate(input);
// Assert
result.Score.Should().Be(0.70); // in_sbom (0.30) + reachable (0.25) + no_vex_na (0.15)
result.Reasons.Should().Contain("in_sbom");
result.Reasons.Should().Contain("reachable");
result.Reasons.Should().Contain("no_vex_na");
}
[Fact]
public void Calculate_WithDeployedSbomMatch_AddsDeployedFactor()
{
// Arrange
var input = new InterestScoreInput
{
CanonicalId = Guid.NewGuid(),
SbomMatches =
[
new SbomMatch
{
SbomDigest = "sha256:abc123",
Purl = "pkg:npm/lodash@4.17.21",
IsReachable = false,
IsDeployed = true,
ScannedAt = DateTimeOffset.UtcNow
}
]
};
// Act
var result = _calculator.Calculate(input);
// Assert
result.Score.Should().Be(0.65); // in_sbom (0.30) + deployed (0.20) + no_vex_na (0.15)
result.Reasons.Should().Contain("in_sbom");
result.Reasons.Should().Contain("deployed");
result.Reasons.Should().Contain("no_vex_na");
}
[Fact]
public void Calculate_WithFullSbomMatch_AddsAllSbomFactors()
{
// Arrange
var input = new InterestScoreInput
{
CanonicalId = Guid.NewGuid(),
SbomMatches =
[
new SbomMatch
{
SbomDigest = "sha256:abc123",
Purl = "pkg:npm/lodash@4.17.21",
IsReachable = true,
IsDeployed = true,
ScannedAt = DateTimeOffset.UtcNow
}
]
};
// Act
var result = _calculator.Calculate(input);
// Assert
result.Score.Should().Be(0.90); // in_sbom (0.30) + reachable (0.25) + deployed (0.20) + no_vex_na (0.15)
result.Reasons.Should().Contain("in_sbom");
result.Reasons.Should().Contain("reachable");
result.Reasons.Should().Contain("deployed");
result.Reasons.Should().Contain("no_vex_na");
}
[Fact]
public void Calculate_WithVexNotAffected_ExcludesVexFactor()
{
// Arrange
var input = new InterestScoreInput
{
CanonicalId = Guid.NewGuid(),
SbomMatches =
[
new SbomMatch
{
SbomDigest = "sha256:abc123",
Purl = "pkg:npm/lodash@4.17.21",
IsReachable = true,
IsDeployed = true,
ScannedAt = DateTimeOffset.UtcNow
}
],
VexStatements =
[
new VexStatement
{
StatementId = "VEX-001",
Status = VexStatus.NotAffected,
Justification = "Component not used in affected context"
}
]
};
// Act
var result = _calculator.Calculate(input);
// Assert
result.Score.Should().Be(0.75); // in_sbom (0.30) + reachable (0.25) + deployed (0.20) - NO no_vex_na
result.Reasons.Should().Contain("in_sbom");
result.Reasons.Should().Contain("reachable");
result.Reasons.Should().Contain("deployed");
result.Reasons.Should().NotContain("no_vex_na");
}
[Fact]
public void Calculate_WithRecentLastSeen_AddsRecentFactor()
{
// Arrange
var input = new InterestScoreInput
{
CanonicalId = Guid.NewGuid(),
SbomMatches =
[
new SbomMatch
{
SbomDigest = "sha256:abc123",
Purl = "pkg:npm/lodash@4.17.21",
ScannedAt = DateTimeOffset.UtcNow
}
],
LastSeenInBuild = DateTimeOffset.UtcNow.AddDays(-7) // 7 days ago
};
// Act
var result = _calculator.Calculate(input);
// Assert
// in_sbom (0.30) + no_vex_na (0.15) + recent (~0.098 for 7 days)
result.Score.Should().BeApproximately(0.55, 0.02);
result.Reasons.Should().Contain("in_sbom");
result.Reasons.Should().Contain("no_vex_na");
result.Reasons.Should().Contain("recent");
}
[Fact]
public void Calculate_WithOldLastSeen_DecaysRecentFactor()
{
// Arrange
var input = new InterestScoreInput
{
CanonicalId = Guid.NewGuid(),
SbomMatches =
[
new SbomMatch
{
SbomDigest = "sha256:abc123",
Purl = "pkg:npm/lodash@4.17.21",
ScannedAt = DateTimeOffset.UtcNow.AddDays(-300)
}
],
LastSeenInBuild = DateTimeOffset.UtcNow.AddDays(-300) // 300 days ago
};
// Act
var result = _calculator.Calculate(input);
// Assert
// in_sbom (0.30) + no_vex_na (0.15) + recent (~0.018 for 300 days, no "recent" reason)
result.Score.Should().BeApproximately(0.47, 0.02);
result.Reasons.Should().Contain("in_sbom");
result.Reasons.Should().Contain("no_vex_na");
result.Reasons.Should().NotContain("recent"); // decayFactor < 0.5
}
[Fact]
public void Calculate_WithVeryOldLastSeen_NoRecentFactor()
{
// Arrange
var input = new InterestScoreInput
{
CanonicalId = Guid.NewGuid(),
SbomMatches = [],
LastSeenInBuild = DateTimeOffset.UtcNow.AddDays(-400) // > 1 year
};
// Act
var result = _calculator.Calculate(input);
// Assert
// Only no_vex_na (0.15), no recent factor (decayed to 0)
result.Score.Should().Be(0.15);
result.Reasons.Should().Contain("no_vex_na");
result.Reasons.Should().NotContain("recent");
}
[Fact]
public void Calculate_MaxScore_IsCappedAt1()
{
// Arrange - use custom weights that exceed 1.0
var heavyWeights = new InterestScoreWeights
{
InSbom = 0.50,
Reachable = 0.40,
Deployed = 0.30,
NoVexNotAffected = 0.20,
Recent = 0.10
};
var calculator = new InterestScoreCalculator(heavyWeights);
var input = new InterestScoreInput
{
CanonicalId = Guid.NewGuid(),
SbomMatches =
[
new SbomMatch
{
SbomDigest = "sha256:abc123",
Purl = "pkg:npm/lodash@4.17.21",
IsReachable = true,
IsDeployed = true,
ScannedAt = DateTimeOffset.UtcNow
}
],
LastSeenInBuild = DateTimeOffset.UtcNow
};
// Act
var result = calculator.Calculate(input);
// Assert
result.Score.Should().Be(1.0);
}
[Fact]
public void Calculate_SetsComputedAtToNow()
{
// Arrange
var input = new InterestScoreInput { CanonicalId = Guid.NewGuid() };
var before = DateTimeOffset.UtcNow;
// Act
var result = _calculator.Calculate(input);
var after = DateTimeOffset.UtcNow;
// Assert
result.ComputedAt.Should().BeOnOrAfter(before);
result.ComputedAt.Should().BeOnOrBefore(after);
}
[Fact]
public void Calculate_PreservesCanonicalId()
{
// Arrange
var canonicalId = Guid.NewGuid();
var input = new InterestScoreInput { CanonicalId = canonicalId };
// Act
var result = _calculator.Calculate(input);
// Assert
result.CanonicalId.Should().Be(canonicalId);
}
[Theory]
[InlineData(VexStatus.Affected)]
[InlineData(VexStatus.Fixed)]
[InlineData(VexStatus.UnderInvestigation)]
public void Calculate_WithNonExcludingVexStatus_IncludesNoVexNaFactor(VexStatus status)
{
// Arrange
var input = new InterestScoreInput
{
CanonicalId = Guid.NewGuid(),
VexStatements =
[
new VexStatement
{
StatementId = "VEX-001",
Status = status
}
]
};
// Act
var result = _calculator.Calculate(input);
// Assert
result.Reasons.Should().Contain("no_vex_na");
}
[Fact]
public void InterestTier_HighScore_ReturnsHigh()
{
// Arrange
var score = new InterestScore
{
CanonicalId = Guid.NewGuid(),
Score = 0.75,
Reasons = [],
ComputedAt = DateTimeOffset.UtcNow
};
// Assert
score.Tier.Should().Be(InterestTier.High);
}
[Fact]
public void InterestTier_MediumScore_ReturnsMedium()
{
// Arrange
var score = new InterestScore
{
CanonicalId = Guid.NewGuid(),
Score = 0.50,
Reasons = [],
ComputedAt = DateTimeOffset.UtcNow
};
// Assert
score.Tier.Should().Be(InterestTier.Medium);
}
[Fact]
public void InterestTier_LowScore_ReturnsLow()
{
// Arrange
var score = new InterestScore
{
CanonicalId = Guid.NewGuid(),
Score = 0.30,
Reasons = [],
ComputedAt = DateTimeOffset.UtcNow
};
// Assert
score.Tier.Should().Be(InterestTier.Low);
}
[Fact]
public void InterestTier_NoneScore_ReturnsNone()
{
// Arrange
var score = new InterestScore
{
CanonicalId = Guid.NewGuid(),
Score = 0.10,
Reasons = [],
ComputedAt = DateTimeOffset.UtcNow
};
// Assert
score.Tier.Should().Be(InterestTier.None);
}
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Concelier.Interest.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.0.0" />
<PackageReference Include="Moq" Version="4.20.72" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Concelier.Interest\StellaOps.Concelier.Interest.csproj" />
</ItemGroup>
</Project>