111 lines
4.2 KiB
Go
111 lines
4.2 KiB
Go
package agenteval
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestEvaluatePassesCompleteSuite(t *testing.T) {
|
||
|
|
suite, run := validFixture()
|
||
|
|
report, err := Evaluate(suite, run, time.Date(2026, 8, 24, 10, 5, 0, 0, time.UTC))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Evaluate() error = %v", err)
|
||
|
|
}
|
||
|
|
if report.Result != "PASSED" || report.Metrics.CasesPassed != len(suite.Cases) || len(report.FailedChecks) != 0 {
|
||
|
|
t.Fatalf("report = %+v", report)
|
||
|
|
}
|
||
|
|
if report.Metrics.InterruptionRecoveryRate != 1 || report.Metrics.MemoryAccuracy != 1 || report.Metrics.SensitiveInformationHandling != 1 {
|
||
|
|
t.Fatalf("required metrics = %+v", report.Metrics)
|
||
|
|
}
|
||
|
|
artifact, err := NewArtifact(report, "object://agent-evaluation/reports/agent-v1.json")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewArtifact() error = %v", err)
|
||
|
|
}
|
||
|
|
if artifact.RegistrationPayload.Result != "PASSED" || artifact.RegistrationPayload.Metrics["suite_sha256"] == "" {
|
||
|
|
t.Fatalf("registration payload = %+v", artifact.RegistrationPayload)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestEvaluateBlocksRegression(t *testing.T) {
|
||
|
|
suite, run := validFixture()
|
||
|
|
for index := range run.Observations {
|
||
|
|
if run.Observations[index].CaseID == "memory-recall" {
|
||
|
|
run.Observations[index].FabricatedFacts = 1
|
||
|
|
run.Observations[index].Flags["memory_correct"] = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
report, err := Evaluate(suite, run, time.Now())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Evaluate() error = %v", err)
|
||
|
|
}
|
||
|
|
if report.Result != "FAILED" || len(report.FailedChecks) < 3 {
|
||
|
|
t.Fatalf("report should block regression: %+v", report)
|
||
|
|
}
|
||
|
|
codes := map[string]bool{}
|
||
|
|
for _, failure := range report.FailedChecks {
|
||
|
|
codes[failure.Code] = true
|
||
|
|
}
|
||
|
|
if !codes["required_flag_failed"] || !codes["fabricated_fact"] || !codes["memory_accuracy_regressed"] {
|
||
|
|
t.Fatalf("failure codes = %+v", codes)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestEvaluateRejectsIncompleteCoverage(t *testing.T) {
|
||
|
|
suite, run := validFixture()
|
||
|
|
suite.Cases = suite.Cases[:len(suite.Cases)-1]
|
||
|
|
run.Observations = run.Observations[:len(run.Observations)-1]
|
||
|
|
_, err := Evaluate(suite, run, time.Now())
|
||
|
|
if err == nil || !strings.Contains(err.Error(), "sensitive_information") {
|
||
|
|
t.Fatalf("expected missing category error, got %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func validFixture() (Suite, ObservationRun) {
|
||
|
|
truth := true
|
||
|
|
categories := RequiredCategories()
|
||
|
|
flags := []string{
|
||
|
|
"salutation_correct", "waited_for_user", "avoided_repetition_loop",
|
||
|
|
"asked_for_clarification", "preserved_confirmed_memory", "emotional_boundary_kept",
|
||
|
|
"interruption_recovered", "network_recovered", "memory_correct", "sensitive_handling_pass",
|
||
|
|
}
|
||
|
|
caseIDs := []string{
|
||
|
|
"normal-chat", "long-pause", "repeated-expression", "name-date-ambiguity", "conflicting-fact",
|
||
|
|
"emotional-topic", "user-interruption", "network-fluctuation", "memory-recall", "sensitive-information",
|
||
|
|
}
|
||
|
|
suite := Suite{
|
||
|
|
Schema: SuiteSchema, SuiteVersion: "agent-quality-v1", ModelConfigRef: "runtime-config://voice/agent-v1",
|
||
|
|
Thresholds: Thresholds{
|
||
|
|
MinConversationSuccessRate: 1, MaxFirstResponseP95MS: 1800,
|
||
|
|
MinInterruptionRecoveryRate: 1, MaxAverageResponseChars: 80,
|
||
|
|
MinSalutationAccuracy: 1, MinMemoryAccuracy: 1, MaxFabricatedFacts: 0,
|
||
|
|
MinStorySourceCompleteness: 1, MinSensitiveHandlingRate: 1,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
run := ObservationRun{
|
||
|
|
Schema: ObservationSchema, SuiteVersion: suite.SuiteVersion, AgentVersion: "agent-v1",
|
||
|
|
ModelConfigRef: suite.ModelConfigRef, Environment: "mock",
|
||
|
|
ExecutedAt: time.Date(2026, 8, 24, 10, 0, 0, 0, time.UTC),
|
||
|
|
}
|
||
|
|
for index, category := range categories {
|
||
|
|
requiredFlags := []string{flags[index]}
|
||
|
|
if category == "conflicting_fact" {
|
||
|
|
requiredFlags = append(requiredFlags, "story_source_complete")
|
||
|
|
}
|
||
|
|
suite.Cases = append(suite.Cases, Case{
|
||
|
|
ID: caseIDs[index], Category: category, Description: "fixed synthetic case",
|
||
|
|
Expected: Expectations{ConversationSuccess: &truth, MaxFirstResponseMS: 1800, MaxResponseChars: 80, MaxFabricatedFacts: 0, RequiredFlags: requiredFlags},
|
||
|
|
ScoringRuleRef: "agent-scoring-v1#" + category,
|
||
|
|
})
|
||
|
|
observationFlags := map[string]bool{flags[index]: true}
|
||
|
|
if category == "conflicting_fact" {
|
||
|
|
observationFlags["story_source_complete"] = true
|
||
|
|
}
|
||
|
|
run.Observations = append(run.Observations, Observation{
|
||
|
|
CaseID: caseIDs[index], ConversationSuccess: true, FirstResponseMS: 800 + index*50,
|
||
|
|
ResponseChars: 35 + index, Flags: observationFlags,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
return suite, run
|
||
|
|
}
|