@@ -35,6 +35,7 @@ const {
3535 buildCurrentWorkflowCallId,
3636 buildEpisodeAttributesFromContext,
3737 buildExperimentAttributes,
38+ buildGraderTelemetry,
3839 hasProxyConfigured,
3940 resolveEngineId,
4041 parseOTLPCustomAttributes,
@@ -2644,6 +2645,30 @@ describe("sendJobConclusionSpan", () => {
26442645 expect ( span . spanId ) . toMatch ( / ^ [ 0 - 9 a - f ] { 16 } $ / ) ;
26452646 } ) ;
26462647
2648+ it ( "emits graders only on the agent job conclusion span" , async ( ) => {
2649+ const mockFetch = vi . fn ( ) . mockResolvedValue ( { ok : true , status : 200 , statusText : "OK" } ) ;
2650+ vi . stubGlobal ( "fetch" , mockFetch ) ;
2651+ process . env . GH_AW_OTLP_ENDPOINTS = JSON . stringify ( [ { url : "https://traces.example.com" } ] ) ;
2652+ process . env . INPUT_JOB_NAME = "agent" ;
2653+ const readFileSpy = vi . spyOn ( fs , "readFileSync" ) . mockImplementation ( filePath => {
2654+ if ( filePath === "/tmp/gh-aw/agent/graders/grader_results.json" ) {
2655+ return JSON . stringify ( { results : [ { id : "quality" , status : "pass" , value : 0.9 } ] } ) ;
2656+ }
2657+ throw Object . assign ( new Error ( "ENOENT" ) , { code : "ENOENT" } ) ;
2658+ } ) ;
2659+
2660+ await sendJobConclusionSpan ( "gh-aw.agent.conclusion" ) ;
2661+ process . env . INPUT_JOB_NAME = "conclusion" ;
2662+ await sendJobConclusionSpan ( "gh-aw.conclusion.conclusion" ) ;
2663+ readFileSpy . mockRestore ( ) ;
2664+
2665+ const spans = mockFetch . mock . calls . map ( ( [ , request ] ) => JSON . parse ( request . body ) . resourceSpans [ 0 ] . scopeSpans [ 0 ] . spans [ 0 ] ) ;
2666+ expect ( spans [ 0 ] . attributes ) . toContainEqual ( buildAttr ( "gh-aw.graders.count" , 1 ) ) ;
2667+ expect ( spans [ 0 ] . events ) . toContainEqual ( expect . objectContaining ( { name : "grader.result" } ) ) ;
2668+ expect ( spans [ 1 ] . attributes . map ( attribute => attribute . key ) ) . not . toContain ( "gh-aw.graders.count" ) ;
2669+ expect ( ( spans [ 1 ] . events ?? [ ] ) . map ( event => event . name ) ) . not . toContain ( "grader.result" ) ;
2670+ } ) ;
2671+
26472672 it ( "emits live episode attributes on conclusion spans from aw_info workflow_call context" , async ( ) => {
26482673 const mockFetch = vi . fn ( ) . mockResolvedValue ( { ok : true , status : 200 , statusText : "OK" } ) ;
26492674 vi . stubGlobal ( "fetch" , mockFetch ) ;
@@ -2692,6 +2717,9 @@ describe("sendJobConclusionSpan", () => {
26922717 if ( filePath === "/tmp/gh-aw/agent_output.json" ) {
26932718 return JSON . stringify ( { items : [ { type : "issue" } , { type : "pull_request" } ] } ) ;
26942719 }
2720+ if ( filePath === "/tmp/gh-aw/agent/graders/grader_results.json" ) {
2721+ return JSON . stringify ( { results : [ { id : "quality" , status : "pass" } ] } ) ;
2722+ }
26952723 throw Object . assign ( new Error ( "ENOENT" ) , { code : "ENOENT" } ) ;
26962724 } ) ;
26972725
@@ -2716,6 +2744,9 @@ describe("sendJobConclusionSpan", () => {
27162744 expect ( conclusionSpan . parentSpanId ) . toBe ( "abcdef1234567890" ) ;
27172745 expect ( agentSpan . attributes ) . toContainEqual ( { key : "gh-aw.output.item_count" , value : { intValue : 2 } } ) ;
27182746 expect ( conclusionSpan . attributes ) . toContainEqual ( { key : "gh-aw.output.item_count" , value : { intValue : 2 } } ) ;
2747+ expect ( agentSpan . attributes . map ( attribute => attribute . key ) ) . not . toContain ( "gh-aw.graders.count" ) ;
2748+ expect ( conclusionSpan . attributes ) . toContainEqual ( buildAttr ( "gh-aw.graders.count" , 1 ) ) ;
2749+ expect ( conclusionSpan . events ) . toContainEqual ( expect . objectContaining ( { name : "grader.result" } ) ) ;
27192750 const agentKeys = agentSpan . attributes . map ( a => a . key ) ;
27202751 const conclusionKeys = conclusionSpan . attributes . map ( a => a . key ) ;
27212752 expect ( agentKeys ) . not . toContain ( "gh-aw.max_ai_credits" ) ;
@@ -6449,6 +6480,94 @@ describe("sendJobConclusionSpan", () => {
64496480 } ) ;
64506481} ) ;
64516482
6483+ // ---------------------------------------------------------------------------
6484+ // buildGraderTelemetry
6485+ // ---------------------------------------------------------------------------
6486+
6487+ describe ( "buildGraderTelemetry" , ( ) => {
6488+ it ( "builds summary attributes and one event per grader result" , ( ) => {
6489+ const telemetry = buildGraderTelemetry (
6490+ {
6491+ results : [
6492+ {
6493+ id : "quality" ,
6494+ name : "Quality" ,
6495+ value : 0.75 ,
6496+ unit : "ratio" ,
6497+ passed : true ,
6498+ status : "pass" ,
6499+ source : "builtin" ,
6500+ severity : "info" ,
6501+ baselineValue : 0.5 ,
6502+ deltaFromBaseline : 0.25 ,
6503+ } ,
6504+ { id : "reliability" , name : "Reliability" , value : null , passed : false , status : "fail" , source : "inline" } ,
6505+ { id : "broken" , status : "error" , source : "inline" } ,
6506+ { id : "missing" , status : "unavailable" , source : "builtin" } ,
6507+ ] ,
6508+ } ,
6509+ 1700000000000
6510+ ) ;
6511+
6512+ expect ( telemetry . attributes ) . toEqual ( [
6513+ buildAttr ( "gh-aw.graders.count" , 4 ) ,
6514+ buildAttr ( "gh-aw.graders.passed" , 1 ) ,
6515+ buildAttr ( "gh-aw.graders.failed" , 1 ) ,
6516+ buildAttr ( "gh-aw.graders.errors" , 1 ) ,
6517+ buildAttr ( "gh-aw.graders.unavailable" , 1 ) ,
6518+ buildAttr ( "gh-aw.graders.other" , 0 ) ,
6519+ ] ) ;
6520+ expect ( telemetry . events ) . toHaveLength ( 4 ) ;
6521+ expect ( telemetry . events [ 0 ] ) . toEqual ( {
6522+ timeUnixNano : toNanoString ( 1700000000000 ) ,
6523+ name : "grader.result" ,
6524+ attributes : [
6525+ buildAttr ( "gh-aw.grader.id" , "quality" ) ,
6526+ buildAttr ( "gh-aw.grader.name" , "Quality" ) ,
6527+ buildAttr ( "gh-aw.grader.status" , "pass" ) ,
6528+ buildAttr ( "gh-aw.grader.source" , "builtin" ) ,
6529+ buildAttr ( "gh-aw.grader.unit" , "ratio" ) ,
6530+ buildDoubleAttr ( "gh-aw.grader.value" , 0.75 ) ,
6531+ buildAttr ( "gh-aw.grader.passed" , true ) ,
6532+ buildAttr ( "gh-aw.grader.severity" , "info" ) ,
6533+ buildDoubleAttr ( "gh-aw.grader.baseline_value" , 0.5 ) ,
6534+ buildDoubleAttr ( "gh-aw.grader.delta_from_baseline" , 0.25 ) ,
6535+ ] ,
6536+ } ) ;
6537+ } ) ;
6538+
6539+ it ( "omits free-form and non-finite grader values" , ( ) => {
6540+ const telemetry = buildGraderTelemetry (
6541+ {
6542+ results : [
6543+ {
6544+ id : "custom" ,
6545+ status : "error" ,
6546+ value : Number . NaN ,
6547+ message : "sensitive message" ,
6548+ details : "sensitive details" ,
6549+ error : "sensitive error" ,
6550+ } ,
6551+ ] ,
6552+ } ,
6553+ 1
6554+ ) ;
6555+
6556+ const eventKeys = telemetry . events [ 0 ] . attributes . map ( attribute => attribute . key ) ;
6557+ expect ( eventKeys ) . toEqual ( [ "gh-aw.grader.id" , "gh-aw.grader.status" ] ) ;
6558+ expect ( JSON . stringify ( telemetry ) ) . not . toContain ( "sensitive" ) ;
6559+ } ) ;
6560+
6561+ it . each ( [ null , { } , { results : [ ] } , { results : [ null , { } , { id : "" } ] } ] ) ( "returns empty telemetry for output without valid results" , output => {
6562+ expect ( buildGraderTelemetry ( output , 1 ) ) . toEqual ( { attributes : [ ] , events : [ ] } ) ;
6563+ } ) ;
6564+
6565+ it ( "counts unrecognized statuses as other" , ( ) => {
6566+ const telemetry = buildGraderTelemetry ( { results : [ { id : "skipped" , status : "skipped" } ] } , 1 ) ;
6567+ expect ( telemetry . attributes ) . toContainEqual ( buildAttr ( "gh-aw.graders.other" , 1 ) ) ;
6568+ } ) ;
6569+ } ) ;
6570+
64526571// ---------------------------------------------------------------------------
64536572// parseOTLPEndpoints
64546573// ---------------------------------------------------------------------------
0 commit comments