mirror of
https://github.com/dotnet/skills.git
synced 2026-09-20 09:49:54 +08:00
b92dde1854
Reorganize the skill-validator project from layer-based grouping (Commands/, Services/, Models/, Utilities/) to feature-based slices (Check/, Evaluate/, Consolidate/, Shared/). Key changes: - Check/: CheckCommand, SkillProfiler, AgentProfiler, PluginValidator, ExternalDependencyChecker, ReferenceScanner - Evaluate/: EvaluateCommand, RejudgeCommand (now a subcommand of evaluate), AgentRunner, Judge, PairwiseJudge, and other eval services - Consolidate/: ConsolidateCommand - Shared/: Models, SkillDiscovery, PluginParser (extracted from PluginValidator), Reporter, and utilities Split PluginValidator: parsing helpers (ParsePluginJson, TryGetSafeSubdirectory) moved to Shared/PluginParser.cs; validation logic stays in Check/PluginValidator.cs. Move RawFrontmatter and RawAgentFrontmatter types from EvalSchema into SkillDiscovery since they are only consumed by discovery. Wire RejudgeCommand as subcommand: skill-validator evaluate rejudge. Co-authored-by: Viktor Hofer <vihofer@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
270 lines
7.9 KiB
C#
270 lines
7.9 KiB
C#
using SkillValidator.Evaluate;
|
|
using SkillValidator.Shared;
|
|
|
|
namespace SkillValidator.Tests;
|
|
|
|
public class ParseEvalConfigTests
|
|
{
|
|
[Fact]
|
|
public void ParsesValidEvalConfig()
|
|
{
|
|
var yaml = """
|
|
scenarios:
|
|
- name: "Test scenario"
|
|
prompt: "Do something"
|
|
assertions:
|
|
- type: "output_contains"
|
|
value: "hello"
|
|
rubric:
|
|
- "Output is correct"
|
|
timeout: 60
|
|
""";
|
|
var config = EvalSchema.ParseEvalConfig(yaml);
|
|
|
|
Assert.Single(config.Scenarios);
|
|
Assert.Equal("Test scenario", config.Scenarios[0].Name);
|
|
Assert.Equal(60, config.Scenarios[0].Timeout);
|
|
}
|
|
|
|
[Fact]
|
|
public void AppliesDefaultTimeout()
|
|
{
|
|
var yaml = """
|
|
scenarios:
|
|
- name: "Test"
|
|
prompt: "Do it"
|
|
""";
|
|
var config = EvalSchema.ParseEvalConfig(yaml);
|
|
|
|
Assert.Equal(120, config.Scenarios[0].Timeout);
|
|
}
|
|
|
|
[Fact]
|
|
public void RejectsEmptyScenarios()
|
|
{
|
|
var yaml = "scenarios: []";
|
|
var ex = Assert.Throws<InvalidOperationException>(() => EvalSchema.ParseEvalConfig(yaml));
|
|
Assert.Contains("at least one scenario", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void RejectsMissingPrompt()
|
|
{
|
|
var yaml = """
|
|
scenarios:
|
|
- name: "Test"
|
|
""";
|
|
Assert.Throws<InvalidOperationException>(() => EvalSchema.ParseEvalConfig(yaml));
|
|
}
|
|
|
|
[Fact]
|
|
public void RejectsInvalidAssertionType()
|
|
{
|
|
var yaml = """
|
|
scenarios:
|
|
- name: "Test"
|
|
prompt: "Do it"
|
|
assertions:
|
|
- type: "invalid_type"
|
|
""";
|
|
Assert.Throws<InvalidOperationException>(() => EvalSchema.ParseEvalConfig(yaml));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParsesFileContainsAssertion()
|
|
{
|
|
var yaml = """
|
|
scenarios:
|
|
- name: "Test"
|
|
prompt: "Do it"
|
|
assertions:
|
|
- type: "file_contains"
|
|
path: "*.cs"
|
|
value: "stackalloc"
|
|
""";
|
|
var config = EvalSchema.ParseEvalConfig(yaml);
|
|
Assert.Equal(AssertionType.FileContains, config.Scenarios[0].Assertions![0].Type);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParsesScenarioLevelConstraints()
|
|
{
|
|
var yaml = """
|
|
scenarios:
|
|
- name: "Test"
|
|
prompt: "Do it"
|
|
expect_tools:
|
|
- "bash"
|
|
reject_tools:
|
|
- "create_file"
|
|
max_turns: 10
|
|
max_tokens: 5000
|
|
""";
|
|
var config = EvalSchema.ParseEvalConfig(yaml);
|
|
var s = config.Scenarios[0];
|
|
Assert.Equal(["bash"], s.ExpectTools);
|
|
Assert.Equal(["create_file"], s.RejectTools);
|
|
Assert.Equal(10, s.MaxTurns);
|
|
Assert.Equal(5000, s.MaxTokens);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParsesSetupCommands()
|
|
{
|
|
var yaml = """
|
|
scenarios:
|
|
- name: "Build first"
|
|
prompt: "Fix the build"
|
|
setup:
|
|
copy_test_files: true
|
|
commands:
|
|
- "dotnet build /bl:build.binlog"
|
|
- "rm -rf src/"
|
|
""";
|
|
var config = EvalSchema.ParseEvalConfig(yaml);
|
|
var setup = config.Scenarios[0].Setup;
|
|
Assert.NotNull(setup);
|
|
Assert.True(setup!.CopyTestFiles);
|
|
Assert.NotNull(setup.Commands);
|
|
Assert.Equal(2, setup.Commands!.Count);
|
|
Assert.Equal("dotnet build /bl:build.binlog", setup.Commands[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParsesConfigSection()
|
|
{
|
|
var yaml = """
|
|
config:
|
|
max_parallel_scenarios: 1
|
|
max_parallel_runs: 2
|
|
scenarios:
|
|
- name: "Test"
|
|
prompt: "Do it"
|
|
""";
|
|
var config = EvalSchema.ParseEvalConfig(yaml);
|
|
|
|
Assert.Equal(1, config.MaxParallelScenarios);
|
|
Assert.Equal(2, config.MaxParallelRuns);
|
|
Assert.Single(config.Scenarios);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConfigSectionIsOptional()
|
|
{
|
|
var yaml = """
|
|
scenarios:
|
|
- name: "Test"
|
|
prompt: "Do it"
|
|
""";
|
|
var config = EvalSchema.ParseEvalConfig(yaml);
|
|
|
|
Assert.Null(config.MaxParallelScenarios);
|
|
Assert.Null(config.MaxParallelRuns);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParsesPartialConfigSection()
|
|
{
|
|
var yaml = """
|
|
config:
|
|
max_parallel_scenarios: 2
|
|
scenarios:
|
|
- name: "Test"
|
|
prompt: "Do it"
|
|
""";
|
|
var config = EvalSchema.ParseEvalConfig(yaml);
|
|
|
|
Assert.Equal(2, config.MaxParallelScenarios);
|
|
Assert.Null(config.MaxParallelRuns);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParsesRunCommandAndAssertAssertion()
|
|
{
|
|
var yaml = """
|
|
scenarios:
|
|
- name: "Build check"
|
|
prompt: "Build the project"
|
|
assertions:
|
|
- type: "run_command_and_assert"
|
|
command_to_run: "dotnet"
|
|
command_arguments: "build"
|
|
expected_exit_code: 0
|
|
expected_std_output_contains: "Build succeeded"
|
|
expected_std_error_contains: "warning"
|
|
expected_std_output_matches: "Build \\w+"
|
|
expected_std_error_matches: "warn.*"
|
|
command_timeout: 60
|
|
""";
|
|
var config = EvalSchema.ParseEvalConfig(yaml);
|
|
var assertion = config.Scenarios[0].Assertions![0];
|
|
Assert.Equal(AssertionType.RunCommandAndAssert, assertion.Type);
|
|
Assert.NotNull(assertion.CommandArgs);
|
|
var cmd = assertion.CommandArgs!;
|
|
Assert.Equal("dotnet", cmd.CommandToRun);
|
|
Assert.Equal("build", cmd.CommandArguments);
|
|
Assert.Equal(0, cmd.ExpectedExitCode);
|
|
Assert.Equal("Build succeeded", cmd.ExpectedStdOutContains);
|
|
Assert.Equal("warning", cmd.ExpectedStdErrorContains);
|
|
Assert.Equal("Build \\w+", cmd.ExpectedStdOutMatches);
|
|
Assert.Equal("warn.*", cmd.ExpectedStdErrorMatches);
|
|
Assert.Equal(60, cmd.Timeout);
|
|
}
|
|
|
|
[Fact]
|
|
public void RejectsRunCommandAndAssertWithoutCommandToRun()
|
|
{
|
|
var yaml = """
|
|
scenarios:
|
|
- name: "Test"
|
|
prompt: "Do it"
|
|
assertions:
|
|
- type: "run_command_and_assert"
|
|
expected_exit_code: 0
|
|
""";
|
|
var ex = Assert.Throws<InvalidOperationException>(() => EvalSchema.ParseEvalConfig(yaml));
|
|
Assert.Contains("command_to_run", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void RejectsRunCommandAndAssertWithoutAnyExpectedChecks()
|
|
{
|
|
var yaml = """
|
|
scenarios:
|
|
- name: "Test"
|
|
prompt: "Do it"
|
|
assertions:
|
|
- type: "run_command_and_assert"
|
|
command_to_run: "dotnet"
|
|
""";
|
|
var ex = Assert.Throws<InvalidOperationException>(() => EvalSchema.ParseEvalConfig(yaml));
|
|
Assert.Contains("expected_exit_code", ex.Message);
|
|
Assert.Contains("expected_std_output_contains", ex.Message);
|
|
}
|
|
}
|
|
|
|
public class ValidateEvalConfigTests
|
|
{
|
|
[Fact]
|
|
public void ReturnsSuccessForValidConfig()
|
|
{
|
|
var yaml = """
|
|
scenarios:
|
|
- name: "Test"
|
|
prompt: "Do it"
|
|
""";
|
|
var result = EvalSchema.ValidateEvalConfig(yaml);
|
|
Assert.True(result.Success);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsErrorsForInvalidConfig()
|
|
{
|
|
var yaml = "scenarios: \"not-an-array\"";
|
|
var result = EvalSchema.ValidateEvalConfig(yaml);
|
|
Assert.False(result.Success);
|
|
Assert.NotNull(result.Errors);
|
|
Assert.True(result.Errors!.Count > 0);
|
|
}
|
|
}
|