mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
6658c2bdd5
## What Adds `evalsubstrate.Rubric.FreshAgainst(currentJudgeHash)` — the **drift-parity guard** for ag-hdqu0 (SCHEMA gate-#2 parity). An Outcomes rubric is a derived artifact content-addressed off the judge; `FreshAgainst` refuses any rubric whose carried `judge_content_hash` ≠ the current judge, so a divergent/stale cloud rubric **self-invalidates** rather than grading against an outdated bar. Empty hash on either side is unverifiable → rejected. Pure-Go internal guard — **no CLI surface**, so no canary/allowlist involvement. ## Tests (TDD) - `TestRubric_FreshAgainst` — match→fresh · mismatch→error naming both hashes · empty-rubric-hash→reject · empty-current→reject. - 72 pass in evalsubstrate, vet/build clean. Closes-scenario: ag-hdqu0.4#stale-reject Bounded-context: BC1-Corpus Evidence: cli/internal/evalsubstrate/rubric_freshness_test.go
40 lines
1.5 KiB
Go
40 lines
1.5 KiB
Go
package evalsubstrate
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestRubric_FreshAgainst is the stale-rubric rejection guard (ag-hdqu0.4 / SCHEMA
|
|
// gate-#2 drift parity): an Outcomes rubric must be refused when its carried
|
|
// judge_content_hash no longer matches the current judge, exactly like a stale
|
|
// local judge — so a divergent cloud rubric self-invalidates instead of grading
|
|
// against an outdated bar.
|
|
func TestRubric_FreshAgainst(t *testing.T) {
|
|
base := ProjectRubric(Task{ID: "t1"}, []Criterion{{ID: "c1", Weight: 1}}, "sha256:current")
|
|
|
|
if err := base.FreshAgainst("sha256:current"); err != nil {
|
|
t.Errorf("matching hash should be fresh, got error: %v", err)
|
|
}
|
|
|
|
err := base.FreshAgainst("sha256:changed")
|
|
if err == nil {
|
|
t.Fatal("mismatched judge hash must be rejected, got nil")
|
|
}
|
|
// The error must name both hashes so the drift is diagnosable.
|
|
if !strings.Contains(err.Error(), "sha256:current") || !strings.Contains(err.Error(), "sha256:changed") {
|
|
t.Errorf("error should name both rubric and current hash, got: %v", err)
|
|
}
|
|
|
|
// A rubric with no carried hash cannot be proven fresh → reject.
|
|
noHash := ProjectRubric(Task{ID: "t2"}, nil, "")
|
|
if err := noHash.FreshAgainst("sha256:current"); err == nil {
|
|
t.Error("rubric with empty judge_content_hash must be rejected")
|
|
}
|
|
|
|
// An empty current hash means we cannot verify → reject.
|
|
if err := base.FreshAgainst(""); err == nil {
|
|
t.Error("empty current judge hash must be rejected (cannot verify freshness)")
|
|
}
|
|
}
|