Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,8 @@ bus.
- Use `github.com/evalops/proto/eventhelpers` to build and unpack canonical
envelopes instead of re-implementing `Any` packing, protojson marshaling, and
type assertions in each service. The package exports `NewCloudEvent`,
`NewChange`, `MarshalProtoJSON`, `UnpackChange`, and
`UnpackTapEventData`. `NewCloudEvent` also stamps
`NewChange`, `MarshalProtoJSON`, `UnpackChange`,
`UnpackEvaluationCompleted`, and `UnpackTapEventData`. `NewCloudEvent` also stamps
`extensions.dataschema=buf.build/evalops/proto/<message>` so published
envelopes stay self-describing across service boundaries.
- Use `config/v1.FeatureFlagSnapshot` when a repo needs a shared on-disk or
Expand Down Expand Up @@ -278,7 +278,9 @@ generated types.
High-risk boundary fixtures belong there too. The current catalog includes
canonical `events/v1.CloudEvent` examples for `pipeline.changes.activity.create`
with `outcome=replied` and `parker.changes.work_relationship.update` with
`status=terminated`. It also includes a Tap -> Pipeline boundary fixture for
`status=terminated`, plus `evaluation.completed` with a typed
`events/v1.EvaluationCompleted` payload for the Fermata -> Pipeline capability
signal seam. It also includes a Tap -> Pipeline boundary fixture for
`ensemble.tap.hubspot.deal.updated` with a qualified stage change and a real
UUID tenant, so downstream consumers can pin the semantics they depend on
instead of only the wire shape.
Expand Down
37 changes: 37 additions & 0 deletions contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,43 @@ func TestCloudEventPipelineActivityCreateRepliedFixtureMatchesProtoContract(t *t
}
}

func TestCloudEventEvaluationCompletedTechnicalCapabilityFixtureMatchesProtoContract(t *testing.T) {
t.Parallel()

var message eventsv1.CloudEvent
loadProtoJSONFixture(t, filepath.Join("proto", "events", "v1", "testdata", "cloud_event_evaluation_completed_technical_capability.json"), &message)

if message.GetType() != "evaluation.completed" {
t.Fatalf("expected type evaluation.completed, got %q", message.GetType())
}
if message.GetSubject() != "product.evaluation.completed" {
t.Fatalf("expected subject product.evaluation.completed, got %q", message.GetSubject())
}
if message.GetTenantId() != "11111111-1111-1111-1111-111111111111" {
t.Fatalf("expected tenant_id 11111111-1111-1111-1111-111111111111, got %q", message.GetTenantId())
}
if got := message.GetExtensions()["dataschema"].GetStringValue(); got != "buf.build/evalops/proto/events.v1.EvaluationCompleted" {
t.Fatalf("expected dataschema buf.build/evalops/proto/events.v1.EvaluationCompleted, got %q", got)
}

var unpacked eventsv1.EvaluationCompleted
if err := message.GetData().UnmarshalTo(&unpacked); err != nil {
t.Fatalf("unpack EvaluationCompleted payload: %v", err)
}
if unpacked.GetSignalType() != "technical_capability" {
t.Fatalf("expected signal_type technical_capability, got %q", unpacked.GetSignalType())
}
if unpacked.GetRun().GetId() != "run-1" {
t.Fatalf("expected run.id run-1, got %q", unpacked.GetRun().GetId())
}
if unpacked.GetMetrics().GetSuccessRate() != 0.9 {
t.Fatalf("expected metrics.success_rate 0.9, got %v", unpacked.GetMetrics().GetSuccessRate())
}
if got := unpacked.GetCompanyDomains(); len(got) != 1 || got[0] != "acme.com" {
t.Fatalf("expected company_domains [acme.com], got %#v", got)
}
}

func TestCloudEventParkerWorkRelationshipUpdateTerminatedFixtureMatchesProtoContract(t *testing.T) {
t.Parallel()

Expand Down
63 changes: 63 additions & 0 deletions contractfixtures/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

const (
ConfigFeatureFlagSnapshot = "config/v1/testdata/feature_flag_snapshot.json"
EventEvaluationCompletedTechnicalCapability = "events/v1/testdata/cloud_event_evaluation_completed_technical_capability.json"
EventPipelineActivityCreateReplied = "events/v1/testdata/cloud_event_pipeline_activity_create_replied.json"
EventParkerWorkRelationshipUpdateTerminated = "events/v1/testdata/cloud_event_parker_work_relationship_update_terminated.json"
EventTapHubspotDealQualified = "events/v1/testdata/cloud_event_tap_hubspot_deal_qualified.json"
Expand All @@ -30,6 +31,56 @@ const (
)

var embeddedFixtures = map[string][]byte{
EventEvaluationCompletedTechnicalCapability: []byte(`{
"spec_version": "1.0",
"id": "evt_eval_completed_technical_capability_1",
"type": "evaluation.completed",
"source": "fermata",
"subject": "product.evaluation.completed",
"time": "2026-04-13T12:00:00Z",
"data_content_type": "application/protobuf",
"tenant_id": "11111111-1111-1111-1111-111111111111",
"data": {
"@type": "type.googleapis.com/events.v1.EvaluationCompleted",
"signal_type": "technical_capability",
"summary": "Claude beats GPT-4 on latency by 40% on enterprise workloads.",
"success_rate": 0.9,
"company_domains": [
"acme.com"
],
"company_names": [
"Acme Corporation"
],
"deal_ids": [
"deal-123"
],
"run": {
"id": "run-1",
"test_suite_id": "suite-1",
"test_suite_name": "Latency benchmark",
"name": "Enterprise latency proof",
"description": "Claude beats GPT-4 on latency by 40% on enterprise workloads.",
"tags": [
"pipeline:signal_type=technical_capability",
"pipeline:company_domain=acme.com",
"pipeline:company_name=Acme Corporation",
"pipeline:deal_id=deal-123"
],
"completed_at": "2026-04-13T12:00:00Z"
},
"metrics": {
"total_tests": "20",
"passed_tests": "18",
"failed_tests": "2",
"total_cost": 4.2,
"duration": 12.5,
"success_rate": 0.9
}
},
"extensions": {
"dataschema": "buf.build/evalops/proto/events.v1.EvaluationCompleted"
}
}`),
MeterRecordUsageRequestLLMGatewayResponses: []byte(`{
"team_id": "team_eng",
"agent_id": "agent_456",
Expand Down Expand Up @@ -130,6 +181,18 @@ func LoadChangeFixture(name string) (*eventsv1.CloudEvent, *eventsv1.Change, err
return envelope, change, nil
}

func LoadEvaluationCompletedFixture(name string) (*eventsv1.CloudEvent, *eventsv1.EvaluationCompleted, error) {
envelope, err := LoadCloudEvent(name)
if err != nil {
return nil, nil, err
}
message, err := eventhelpers.UnpackEvaluationCompleted(envelope)
if err != nil {
return nil, nil, fmt.Errorf("unmarshal evaluation fixture %q: %w", name, err)
}
return envelope, message, nil
}

func LoadTapFixture(name string) (*eventsv1.CloudEvent, *tapv1.TapEventData, error) {
envelope, err := LoadCloudEvent(name)
if err != nil {
Expand Down
32 changes: 32 additions & 0 deletions contractfixtures/fixtures_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

configv1 "github.com/evalops/proto/gen/go/config/v1"
eventsv1 "github.com/evalops/proto/gen/go/events/v1"
meterv1 "github.com/evalops/proto/gen/go/meter/v1"
)

Expand Down Expand Up @@ -61,6 +62,37 @@ func TestLoadTapFixture(t *testing.T) {
}
}

func TestLoadEvaluationCompletedFixture(t *testing.T) {
t.Parallel()

envelope, message, err := LoadEvaluationCompletedFixture(EventEvaluationCompletedTechnicalCapability)
if err != nil {
t.Fatalf("load evaluation fixture: %v", err)
}
if envelope.GetType() != "evaluation.completed" {
t.Fatalf("unexpected type %q", envelope.GetType())
}
if got := envelope.GetExtensions()["dataschema"].GetStringValue(); got != "buf.build/evalops/proto/events.v1.EvaluationCompleted" {
t.Fatalf("unexpected dataschema %q", got)
}
if message.GetSignalType() != "technical_capability" {
t.Fatalf("unexpected signal_type %q", message.GetSignalType())
}
if message.GetRun().GetId() != "run-1" {
t.Fatalf("unexpected run.id %q", message.GetRun().GetId())
}
if message.GetMetrics().GetSuccessRate() != 0.9 {
t.Fatalf("unexpected metrics.success_rate %v", message.GetMetrics().GetSuccessRate())
}
if envelope.GetData().GetTypeUrl() != "type.googleapis.com/events.v1.EvaluationCompleted" {
t.Fatalf("unexpected type URL %q", envelope.GetData().GetTypeUrl())
}
var direct eventsv1.EvaluationCompleted
if err := envelope.GetData().UnmarshalTo(&direct); err != nil {
t.Fatalf("unmarshal direct evaluation payload: %v", err)
}
}

func TestUnmarshalProtoJSONLoadsMeterFixture(t *testing.T) {
t.Parallel()

Expand Down
8 changes: 8 additions & 0 deletions eventhelpers/eventhelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ func UnpackChange(envelope *eventsv1.CloudEvent) (*eventsv1.Change, error) {
return message, nil
}

func UnpackEvaluationCompleted(envelope *eventsv1.CloudEvent) (*eventsv1.EvaluationCompleted, error) {
message := &eventsv1.EvaluationCompleted{}
if err := UnpackData(envelope, message); err != nil {
return nil, err
}
return message, nil
}

func UnpackTapEventData(envelope *eventsv1.CloudEvent) (*tapv1.TapEventData, error) {
message := &tapv1.TapEventData{}
if err := UnpackData(envelope, message); err != nil {
Expand Down
64 changes: 64 additions & 0 deletions eventhelpers/eventhelpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,62 @@ func TestUnpackTapEventDataRoundTrip(t *testing.T) {
}
}

func TestUnpackEvaluationCompletedRoundTrip(t *testing.T) {
t.Parallel()

message := &eventsv1.EvaluationCompleted{
SignalType: "technical_capability",
Summary: "Claude beats GPT-4 on latency by 40% on enterprise workloads.",
SuccessRate: float64ptr(0.9),
CompanyDomains: []string{"acme.com"},
CompanyNames: []string{"Acme Corporation"},
DealIds: []string{"deal-123"},
Run: &eventsv1.EvaluationRun{
Id: "run-1",
TestSuiteId: "suite-1",
TestSuiteName: "Latency benchmark",
Name: "Enterprise latency proof",
Description: "Claude beats GPT-4 on latency by 40% on enterprise workloads.",
Tags: []string{"pipeline:signal_type=technical_capability"},
},
Metrics: &eventsv1.EvaluationMetrics{
TotalTests: int64ptr(20),
PassedTests: int64ptr(18),
FailedTests: int64ptr(2),
TotalCost: float64ptr(4.2),
Duration: float64ptr(12.5),
SuccessRate: float64ptr(0.9),
},
}

envelope, err := NewCloudEvent(
"evt_eval_123",
"evaluation.completed",
"fermata",
"product.evaluation.completed",
"11111111-1111-1111-1111-111111111111",
time.Date(2026, 4, 13, 12, 0, 0, 0, time.UTC),
message,
)
if err != nil {
t.Fatalf("NewCloudEvent() error = %v", err)
}

unpacked, err := UnpackEvaluationCompleted(envelope)
if err != nil {
t.Fatalf("UnpackEvaluationCompleted() error = %v", err)
}
if unpacked.GetSignalType() != "technical_capability" {
t.Fatalf("unexpected signal_type %q", unpacked.GetSignalType())
}
if unpacked.GetMetrics().GetSuccessRate() != 0.9 {
t.Fatalf("unexpected success_rate %v", unpacked.GetMetrics().GetSuccessRate())
}
if got := envelope.GetExtensions()["dataschema"].GetStringValue(); got != "buf.build/evalops/proto/events.v1.EvaluationCompleted" {
t.Fatalf("unexpected dataschema %q", got)
}
}

func TestNewChangeBuildsCanonicalMessageFromJSONPayload(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -159,3 +215,11 @@ func mustStruct(t *testing.T, fields map[string]any) *structpb.Struct {
}
return message
}

func int64ptr(value int64) *int64 {
return &value
}

func float64ptr(value float64) *float64 {
return &value
}
Loading
Loading