mirror of
https://github.com/dotnet/skills.git
synced 2026-09-20 09:49:54 +08:00
3fff1f2573
* Skill-validator: perf, cancellation, and reliability improvements Performance: - Cache skill-validator-dist.tar.gz in CI (evaluation.yml and skill-check.yml) to skip .NET SDK setup, build, and tar on cache hit. Evaluate and comment-on-pr jobs restore from cache with artifact download fallback. - Replace SemaphoreSlim with Lock in SessionDatabase for lighter-weight synchronous locking. Cancellation: - Thread CancellationToken from System.CommandLine entry point through the entire evaluate call chain: Run -> EvaluateTarget -> EvaluateSkill/EvaluateAgent -> ExecuteScenario/ExecuteAgentScenario -> ExecuteRun/ExecuteAgentRun, plus EvaluateSkillNoise -> ExecuteNoiseTest. - Add CancellationToken parameter to AgentRunner.RunAgent, Judge.JudgeRun, PairwiseJudge.Judge, and OverfittingJudge.Analyze, forwarding to RetryHelper and LlmSession.SendAsync respectively. - Pass CancellationToken to all ConcurrencyLimiter.RunAsync call sites so semaphore waits are cancellable. - Re-throw OperationCanceledException at the top-level skill evaluation loop so Ctrl+C / CI cancellation propagates instead of being swallowed as a skill failure. Reliability: - Smart timeline truncation in Judge and PairwiseJudge: error events (session.error, runner.error) are always preserved during truncation regardless of position, with a summary marker showing event type breakdown. - Add jitter (±25%) to RetryHelper exponential backoff to prevent thundering herd on rate-limit recovery. - Add CancellationToken parameter to ConcurrencyLimiter.RunAsync<T>. - Add ConcurrencyLimiter tests (6 tests) and RetryHelper jitter tests (2 tests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback - RetryHelper: re-clamp delay after jitter so it never exceeds the remaining budget or MaxSingleDelayMs. - Judge/PairwiseJudge: handle edge case where error count alone exceeds MaxTimelineEvents by trimming errors with a summary instead of unconditionally appending all of them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add high-variance detection and refactor FailureKind to enum - Add Statistics.CoefficientOfVariation() for measuring score consistency - Flag scenarios with CV > 0.5 as high-variance in ScenarioComparison - Show console warning and markdown footnote for high-variance scenarios - Include high-variance scenario names in Comparator verdict reason - Refactor FailureKind from magic strings to strongly-typed enum with JsonStringEnumMemberName attributes for backwards-compatible JSON output - Add 6 new CV unit tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Viktor Hofer <vihofer@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
183 lines
5.0 KiB
C#
183 lines
5.0 KiB
C#
using SkillValidator.Evaluate;
|
|
|
|
namespace SkillValidator.Tests;
|
|
|
|
public class BootstrapConfidenceIntervalTests
|
|
{
|
|
[Fact]
|
|
public void ReturnsZeroIntervalForEmptyData()
|
|
{
|
|
var ci = Statistics.BootstrapConfidenceInterval([], 0.95);
|
|
Assert.Equal(0, ci.Low);
|
|
Assert.Equal(0, ci.High);
|
|
Assert.Equal(0.95, ci.Level);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsPointIntervalForSingleDataPoint()
|
|
{
|
|
var ci = Statistics.BootstrapConfidenceInterval([0.5], 0.95);
|
|
Assert.Equal(0.5, ci.Low);
|
|
Assert.Equal(0.5, ci.High);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProducesReasonableIntervalForPositiveData()
|
|
{
|
|
double[] data = [0.1, 0.15, 0.2, 0.12, 0.18];
|
|
var ci = Statistics.BootstrapConfidenceInterval(data, 0.95);
|
|
Assert.True(ci.Low > 0);
|
|
Assert.True(ci.High > ci.Low);
|
|
Assert.True(ci.High <= 0.2);
|
|
Assert.Equal(0.95, ci.Level);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProducesIntervalForMixedData()
|
|
{
|
|
double[] data = [-0.1, 0.2, -0.05, 0.15, -0.08, 0.1];
|
|
var ci = Statistics.BootstrapConfidenceInterval(data, 0.95);
|
|
Assert.True(ci.Low < ci.High);
|
|
}
|
|
|
|
[Fact]
|
|
public void NarrowerCiWithMoreDataPoints()
|
|
{
|
|
double[] small = [0.1, 0.2, 0.15];
|
|
double[] large = [0.1, 0.2, 0.15, 0.12, 0.18, 0.14, 0.16, 0.13, 0.17, 0.11];
|
|
var ciSmall = Statistics.BootstrapConfidenceInterval(small, 0.95);
|
|
var ciLarge = Statistics.BootstrapConfidenceInterval(large, 0.95);
|
|
var widthSmall = ciSmall.High - ciSmall.Low;
|
|
var widthLarge = ciLarge.High - ciLarge.Low;
|
|
Assert.True(widthLarge < widthSmall);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsDeterministic()
|
|
{
|
|
double[] data = [0.1, 0.2, 0.3, 0.15, 0.25];
|
|
var ci1 = Statistics.BootstrapConfidenceInterval(data, 0.95);
|
|
var ci2 = Statistics.BootstrapConfidenceInterval(data, 0.95);
|
|
Assert.Equal(ci1.Low, ci2.Low);
|
|
Assert.Equal(ci1.High, ci2.High);
|
|
}
|
|
}
|
|
|
|
public class IsStatisticallySignificantTests
|
|
{
|
|
[Fact]
|
|
public void ReturnsTrueWhenCiIsEntirelyPositive()
|
|
{
|
|
Assert.True(Statistics.IsStatisticallySignificant(new ConfidenceInterval(0.05, 0.3, 0.95)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsTrueWhenCiIsEntirelyNegative()
|
|
{
|
|
Assert.True(Statistics.IsStatisticallySignificant(new ConfidenceInterval(-0.4, -0.1, 0.95)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsFalseWhenCiSpansZero()
|
|
{
|
|
Assert.False(Statistics.IsStatisticallySignificant(new ConfidenceInterval(-0.1, 0.2, 0.95)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsFalseWhenCiIsExactlyAtZero()
|
|
{
|
|
Assert.False(Statistics.IsStatisticallySignificant(new ConfidenceInterval(0, 0.1, 0.95)));
|
|
}
|
|
}
|
|
|
|
public class WilsonScoreIntervalTests
|
|
{
|
|
[Fact]
|
|
public void ReturnsZeroIntervalForZeroTotal()
|
|
{
|
|
var ci = Statistics.WilsonScoreInterval(0, 0);
|
|
Assert.Equal(0, ci.Low);
|
|
Assert.Equal(0, ci.High);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProducesReasonableIntervalForPerfectSuccess()
|
|
{
|
|
var ci = Statistics.WilsonScoreInterval(10, 10);
|
|
Assert.True(ci.Low > 0.5);
|
|
Assert.Equal(1, ci.High);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProducesReasonableIntervalForNoSuccesses()
|
|
{
|
|
var ci = Statistics.WilsonScoreInterval(0, 10);
|
|
Assert.Equal(0, ci.Low);
|
|
Assert.True(ci.High < 0.5);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProducesIntervalCenteredAroundProportion()
|
|
{
|
|
var ci = Statistics.WilsonScoreInterval(5, 10);
|
|
Assert.True(ci.Low < 0.5);
|
|
Assert.True(ci.High > 0.5);
|
|
}
|
|
|
|
[Fact]
|
|
public void NarrowsWithMoreSamples()
|
|
{
|
|
var ci10 = Statistics.WilsonScoreInterval(5, 10);
|
|
var ci100 = Statistics.WilsonScoreInterval(50, 100);
|
|
var width10 = ci10.High - ci10.Low;
|
|
var width100 = ci100.High - ci100.Low;
|
|
Assert.True(width100 < width10);
|
|
}
|
|
}
|
|
|
|
public class CoefficientOfVariationTests
|
|
{
|
|
[Fact]
|
|
public void ReturnsNullForEmpty()
|
|
{
|
|
Assert.Null(Statistics.CoefficientOfVariation([]));
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsNullForSingleElement()
|
|
{
|
|
Assert.Null(Statistics.CoefficientOfVariation([0.5]));
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsNullWhenMeanIsZero()
|
|
{
|
|
Assert.Null(Statistics.CoefficientOfVariation([-0.1, 0.1]));
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsZeroForIdenticalValues()
|
|
{
|
|
var cv = Statistics.CoefficientOfVariation([0.3, 0.3, 0.3]);
|
|
Assert.NotNull(cv);
|
|
Assert.Equal(0.0, cv.Value, 0.001);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsHighValueForHighVariance()
|
|
{
|
|
// Scores: -0.5, 0.5, -0.3, 0.4 → high spread relative to mean
|
|
var cv = Statistics.CoefficientOfVariation([-0.5, 0.5, -0.3, 0.4]);
|
|
Assert.NotNull(cv);
|
|
Assert.True(cv > 1.0, $"Expected high CV, got {cv}");
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsLowValueForConsistentScores()
|
|
{
|
|
var cv = Statistics.CoefficientOfVariation([0.10, 0.11, 0.09, 0.10, 0.12]);
|
|
Assert.NotNull(cv);
|
|
Assert.True(cv < 0.15, $"Expected low CV, got {cv}");
|
|
}
|
|
}
|