Files

95 lines
4.0 KiB
Go
Raw Permalink Normal View History

2026-08-26 06:41:07 +08:00
package agenteval
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
)
func TestValidateArtifactRejectsTamperingAndImplicitFailedRegistration(t *testing.T) {
suite, run := validFixture()
report, err := Evaluate(suite, run, time.Now())
if err != nil {
t.Fatalf("Evaluate() error = %v", err)
}
artifact, err := NewArtifact(report, "artifact://agent-eval/pass.json")
if err != nil {
t.Fatalf("NewArtifact() error = %v", err)
}
if err = ValidateArtifact(suite, run, artifact, false); err != nil {
t.Fatalf("ValidateArtifact() error = %v", err)
}
artifact.RegistrationPayload.Metrics["cases_passed"] = 999
if err = ValidateArtifact(suite, run, artifact, false); err == nil || (!strings.Contains(err.Error(), "does not match report") && !strings.Contains(err.Error(), "does not match the supplied observations")) {
t.Fatalf("expected tampering error, got %v", err)
}
suite, run = validFixture()
run.Observations[0].Flags["salutation_correct"] = false
failedReport, err := Evaluate(suite, run, time.Now())
if err != nil {
t.Fatalf("Evaluate() failed report error = %v", err)
}
failedArtifact, _ := NewArtifact(failedReport, "artifact://agent-eval/failed.json")
if err = ValidateArtifact(suite, run, failedArtifact, false); err == nil || !strings.Contains(err.Error(), "allow-failed") {
t.Fatalf("expected explicit failed registration guard, got %v", err)
}
if err = ValidateArtifact(suite, run, failedArtifact, true); err != nil {
t.Fatalf("explicit failed registration should be valid: %v", err)
}
}
func TestRegistrarUsesDeterministicIdempotencyAndAcceptsReplay(t *testing.T) {
suite, run := validFixture()
report, _ := Evaluate(suite, run, time.Now())
artifact, _ := NewArtifact(report, "artifact://agent-eval/pass.json")
tenantID := "10000000-0000-0000-0000-000000000010"
configID := "20000000-0000-0000-0000-000000000020"
expectedKey, err := RegistrationIdempotencyKey(configID, artifact.RegistrationPayload)
if err != nil {
t.Fatalf("RegistrationIdempotencyKey() error = %v", err)
}
var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodPost || request.URL.Path != "/api/v1/ops/config-versions/"+configID+"/evaluations" {
http.Error(writer, "unexpected endpoint", http.StatusNotFound)
return
}
if request.Header.Get("Authorization") != "Bearer test-token" || request.Header.Get("X-Tenant-Id") != tenantID || request.Header.Get("Idempotency-Key") != expectedKey {
http.Error(writer, "invalid headers", http.StatusBadRequest)
return
}
var payload RegistrationPayload
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil || payload.Result != "PASSED" || payload.Metrics["config_version_id"] != configID {
http.Error(writer, "invalid body", http.StatusBadRequest)
return
}
writer.Header().Set("Content-Type", "application/json")
writer.Header().Set("X-Request-Id", "registration-request")
if calls.Add(1) > 1 {
writer.Header().Set("Idempotency-Replayed", "true")
writer.WriteHeader(http.StatusOK)
} else {
writer.WriteHeader(http.StatusCreated)
}
_ = json.NewEncoder(writer).Encode(map[string]any{"request_id": "registration-request", "data": map[string]any{"id": "30000000-0000-0000-0000-000000000030"}})
}))
defer server.Close()
registrar, err := NewRegistrar(server.URL, "test-token", server.Client())
if err != nil {
t.Fatalf("NewRegistrar() error = %v", err)
}
first, err := registrar.Register(t.Context(), tenantID, configID, artifact.RegistrationPayload)
if err != nil || first.StatusCode != http.StatusCreated || first.Replayed {
t.Fatalf("first registration = %+v, err = %v", first, err)
}
second, err := registrar.Register(t.Context(), tenantID, configID, artifact.RegistrationPayload)
if err != nil || second.StatusCode != http.StatusOK || !second.Replayed || second.EvaluationID != first.EvaluationID {
t.Fatalf("replayed registration = %+v, err = %v", second, err)
}
}