Files
dotnet__skills/eng/skill-validator/tests/Evaluate/RejudgeCommandTests.cs
YuliiaKovalova 76a5ec8cb3 Decouple agent runs from judging in skill-validator (--no-judge + cross-dir rejudge) (#769)
* Decouple agent runs from judging in skill-validator

Allow baseline and treatment agent runs to execute in one parallel pool, with
judging deferred to a final step, removing the serial precompute-baseline ->
evaluate barrier.

evaluate --no-judge: runs the requested agent arms and persists sessions.db +
metrics, but skips all judging and requires no baseline file (exits 0 on
successful runs). Forces session persistence; mutually exclusive with
--baseline-out/--baseline-from; requires --results-dir. Only the agent model is
validated (judge model recorded as metadata); warns when zero runs persist.

rejudge --baseline-dir: cross-directory judging that pairs each treatment run
with its baseline run by the BaselineStore key (prompt SHA + target SHA,
persisted in a new sessions.db baseline_key column), runs the same
pairwise/independent judges and pass/fail gates evaluate uses, and writes
verdicts/reports. Baseline and treatment must share --model (rejected if either
side mixes models); effective judge model resolves to --judge-model, else the
treatment DB's, else the baseline DB's, rejecting a persisted mismatch. The
inline per-run judging is extracted into a shared JudgeRunGroup helper reused by
both the same-dir and cross-dir paths (no duplicated judging logic).

Adds tests for baseline_key round-trip + legacy migration, ComputeScenarioKey,
cross-dir pairing, and model/judge-model compatibility. Updates README and
InvestigatingResults docs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Hoist ComputeScenarioKey to scenario level to avoid per-run fixture hashing

Compute the baseline key once per scenario instead of once per run, so
multi-run evaluations (--runs N) don't re-hash fixture files N times.
The key is threaded into ExecuteRun/ExecuteAgentRun as a parameter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Memoize scenario-key fixture hashing for the duration of an evaluation

Route the persisted baseline_key through an evaluation-scoped BaselineStore
cache (ComputeScenarioKeyCached) instead of the static ComputeScenarioKey, so
a scenario's setup fixtures are hashed at most once across every run, arm, and
scenario of an evaluation. When a baseline store already exists (--baseline-from
/--baseline-out) its warm input-hash cache is reused; otherwise a dedicated,
side-effect-free cache (BaselineStore.ForKeyCache) is threaded through.

Addresses the PR review comment about ComputeScenarioKey re-hashing large
fixture directories. Adds a test asserting the cached variant equals the
canonical static computation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bump session schema version to 3 for the baseline_key column

The baseline_key column was added without bumping schema_info.version, so
external tools keying off the version couldn't detect the new column. Introduce
a single SchemaVersion constant (= 3), stamp it after migrations run, and drop
the drift-prone version literal from the raw CREATE SQL so SetSchemaInfo is the
sole source of truth. Update the schema-info and legacy-upgrade tests to expect
version 3.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fail fast on judging-dependent options under --no-judge; fix README prefix

Under --no-judge the noise test and overfitting fix cannot run (they require
judging), so reject --noise-skills-dir and --overfitting-fix up front instead
of silently ignoring them, mirroring the existing --baseline-out/--baseline-from
guard. Add EvaluateCommandTests covering these early rejections (they short-
circuit before any model/network call).

Also prefix the decoupled-runs README example with 'skill-validator' for
consistency with the other CLI invocations in the doc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-16 13:35:33 +02:00

193 lines
6.1 KiB
C#

using SkillValidator.Evaluate;
namespace SkillValidator.Tests;
public class RejudgeCommandTests
{
private static SessionRecord Rec(
string id,
string role,
int runIndex,
string? baselineKey,
string skill = "skill",
string scenario = "scn",
string model = "model-x",
string? metrics = "{}") =>
new(
Id: id,
SkillName: skill,
SkillPath: "/path/" + skill,
ScenarioName: scenario,
RunIndex: runIndex,
Role: role,
Model: model,
ConfigDir: "cfg",
WorkDir: "/work",
Prompt: "prompt",
SkillSha: "sha",
RubricJson: null,
Status: "completed",
MetricsJson: metrics,
JudgeJson: null,
PairwiseJson: null,
BaselineKey: baselineKey);
[Fact]
public void PairCrossDir_MatchesByBaselineKeyAndRunIndex()
{
var baseline = new[]
{
Rec("b0", "baseline", 0, "K1"),
Rec("b1", "baseline", 1, "K1"),
};
var treatment = new[]
{
Rec("t0", "with-skill-isolated", 0, "K1"),
Rec("t1", "with-skill-isolated", 1, "K1"),
};
var pairing = RejudgeCommand.PairCrossDir(baseline, treatment);
Assert.Empty(pairing.Unmatched);
Assert.Equal(2, pairing.Pairs.Count);
Assert.Equal("b0", pairing.Pairs.Single(p => p.RunIndex == 0).Baseline.Id);
Assert.Equal("b1", pairing.Pairs.Single(p => p.RunIndex == 1).Baseline.Id);
Assert.Equal("t0", pairing.Pairs.Single(p => p.RunIndex == 0).Isolated.Id);
}
[Fact]
public void PairCrossDir_FallsBackToFirstBaseline_WhenRunIndexMissing()
{
var baseline = new[] { Rec("b0", "baseline", 0, "K1") };
var treatment = new[] { Rec("t2", "with-skill-isolated", 2, "K1") };
var pairing = RejudgeCommand.PairCrossDir(baseline, treatment);
var pair = Assert.Single(pairing.Pairs);
Assert.Equal("b0", pair.Baseline.Id);
Assert.Equal(2, pair.RunIndex);
}
[Fact]
public void PairCrossDir_ReportsUnmatched_WhenNoBaselineKeyMatches()
{
var baseline = new[] { Rec("b0", "baseline", 0, "K1") };
var treatment = new[] { Rec("t0", "with-skill-isolated", 0, "K2") };
var pairing = RejudgeCommand.PairCrossDir(baseline, treatment);
Assert.Empty(pairing.Pairs);
var unmatched = Assert.Single(pairing.Unmatched);
Assert.Contains("scn", unmatched);
}
[Fact]
public void PairCrossDir_IncludesPluginRole()
{
var baseline = new[] { Rec("b0", "baseline", 0, "K1") };
var treatment = new[]
{
Rec("iso", "with-skill-isolated", 0, "K1"),
Rec("plug", "with-skill-plugin", 0, "K1"),
};
var pairing = RejudgeCommand.PairCrossDir(baseline, treatment);
var pair = Assert.Single(pairing.Pairs);
Assert.Equal("iso", pair.Isolated.Id);
Assert.NotNull(pair.Plugin);
Assert.Equal("plug", pair.Plugin!.Id);
}
[Fact]
public void PairCrossDir_SupportsAgentRolesAndReusedBaseline()
{
var baseline = new[] { Rec("b0", "baseline-reused", 0, "K1") };
var treatment = new[] { Rec("a0", "with-agent-isolated", 0, "K1") };
var pairing = RejudgeCommand.PairCrossDir(baseline, treatment);
var pair = Assert.Single(pairing.Pairs);
Assert.Equal("b0", pair.Baseline.Id);
Assert.Equal("a0", pair.Isolated.Id);
}
[Fact]
public void ValidateCrossDirCompat_RejectsModelMismatch()
{
var (ok, effective, error) = RejudgeCommand.ValidateCrossDirCompat(
baselineModel: "model-a", treatmentModel: "model-b",
baselineJudgeModel: "judge", treatmentJudgeModel: "judge", explicitJudgeModel: null);
Assert.False(ok);
Assert.Null(effective);
Assert.Contains("model-a", error);
Assert.Contains("model-b", error);
}
[Fact]
public void ValidateCrossDirCompat_RejectsJudgeModelMismatch()
{
var (ok, effective, error) = RejudgeCommand.ValidateCrossDirCompat(
"model-x", "model-x", "judge-a", "judge-b", explicitJudgeModel: null);
Assert.False(ok);
Assert.Null(effective);
Assert.Contains("judge-a", error);
Assert.Contains("judge-b", error);
}
[Fact]
public void ValidateCrossDirCompat_ExplicitJudgeOverridesMismatch()
{
var (ok, effective, error) = RejudgeCommand.ValidateCrossDirCompat(
"model-x", "model-x", "judge-a", "judge-b", explicitJudgeModel: "judge-c");
Assert.True(ok);
Assert.Equal("judge-c", effective);
Assert.Null(error);
}
[Fact]
public void ValidateCrossDirCompat_PrefersTreatmentJudgeModel()
{
var (ok, effective, _) = RejudgeCommand.ValidateCrossDirCompat(
"model-x", "model-x", baselineJudgeModel: null, treatmentJudgeModel: "judge-t", explicitJudgeModel: null);
Assert.True(ok);
Assert.Equal("judge-t", effective);
}
[Fact]
public void ValidateCrossDirCompat_FallsBackToBaselineJudgeModel()
{
var (ok, effective, _) = RejudgeCommand.ValidateCrossDirCompat(
"model-x", "model-x", baselineJudgeModel: "judge-b", treatmentJudgeModel: null, explicitJudgeModel: null);
Assert.True(ok);
Assert.Equal("judge-b", effective);
}
[Fact]
public void ValidateCrossDirCompat_FailsWhenNoJudgeModelAvailable()
{
var (ok, effective, error) = RejudgeCommand.ValidateCrossDirCompat(
"model-x", "model-x", baselineJudgeModel: null, treatmentJudgeModel: null, explicitJudgeModel: null);
Assert.False(ok);
Assert.Null(effective);
Assert.NotNull(error);
}
[Fact]
public void ValidateCrossDirCompat_AcceptsMatchingJudgeModels()
{
var (ok, effective, error) = RejudgeCommand.ValidateCrossDirCompat(
"model-x", "model-x", "judge", "judge", explicitJudgeModel: null);
Assert.True(ok);
Assert.Equal("judge", effective);
Assert.Null(error);
}
}