2026-07-27 18:17:54 +00:00
package pipeline
import (
"context"
"errors"
"reflect"
"strings"
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/evidencecontext"
)
type capturingEvidenceOutput struct {
requests [ ] contracts . OutputRequest
}
func ( * capturingEvidenceOutput ) Key ( ) string { return "capture/evidence-context" }
func ( output * capturingEvidenceOutput ) Encode ( _ context . Context , request contracts . OutputRequest ) ( contracts . OutputResult , error ) {
request . EvidenceContext = contracts . CloneSerializedArtifactPointer ( request . EvidenceContext )
output . requests = append ( output . requests , request )
return contracts . OutputResult { } , nil
}
func installEvidencePlan ( prepared * PreparedPipeline , window int , selected [ ] string , project func ( codecNotes ) ( [ ] source . SourceRef , error ) ) {
lanes := make ( [ ] preparedEvidenceLane , 0 , len ( selected ) )
for _ , laneID := range selected {
for _ , step := range prepared . Steps {
for _ , lane := range step . lanes {
if lane . resolved . ID != laneID {
continue
}
lanes = append ( lanes , preparedEvidenceLane {
laneID : laneID ,
kind : lane . resolved . ArtifactKind ,
project : func ( value any ) ( [ ] source . SourceRef , error ) {
notes , ok := value . ( codecNotes )
if ! ok {
return nil , errors . New ( "unexpected artifact type" )
}
return project ( notes )
} ,
} )
}
}
}
prepared . evidencePlan = & preparedEvidencePlan { policy : EvidenceContextPolicy { Enabled : true , WindowUnits : window , LaneIDs : append ( [ ] string ( nil ) , selected ... ) } , lanes : lanes }
}
func setNormalizedNotes ( prepared * PreparedPipeline , values map [ string ] codecNotes ) {
for stepIndex := range prepared . Steps {
for laneIndex := range prepared . Steps [ stepIndex ] . lanes {
lane := & prepared . Steps [ stepIndex ] . lanes [ laneIndex ]
value , ok := values [ lane . resolved . ID ]
if ! ok {
continue
}
lane . typed . normalize = func ( context . Context , any , contracts . TypedNormalizeRequest [ any ] ) ( erasedTypedResult , error ) {
return erasedTypedResult { Value : value } , nil
}
}
}
}
func decodeCapturedEvidence ( t * testing . T , output * capturingEvidenceOutput ) evidencecontext . Document {
t . Helper ( )
if len ( output . requests ) != 1 || output . requests [ 0 ] . EvidenceContext == nil {
t . Fatalf ( "output requests = %#v, want one evidence context" , output . requests )
}
value , err := evidencecontext . New ( ) . Decode ( output . requests [ 0 ] . EvidenceContext . Content )
if err != nil {
t . Fatalf ( "Decode(evidence context): %v" , err )
}
return value
}
func TestRunnerBuildsEvidenceContextFromSelectedNormalizedOutputs ( t * testing . T ) {
prepared := preparedOrderedPipeline ( t , 3 , orderedLaneSpec { id : "alpha" , profile : "notes" } , orderedLaneSpec { id : "beta" , profile : "notes" } )
encoder := & capturingEvidenceOutput { }
prepared . output = encoder
setNormalizedNotes ( prepared , map [ string ] codecNotes { "alpha" : { Items : [ ] string { "one" } } , "beta" : { Items : [ ] string { "two" } } } )
installEvidencePlan ( prepared , 1 , [ ] string { "alpha" , "beta" , "inactive" } , func ( notes codecNotes ) ( [ ] source . SourceRef , error ) {
switch notes . Items [ 0 ] {
case "one" :
return [ ] source . SourceRef { { SourceID : "source" , StartUnitID : 1 , EndUnitID : 1 } } , nil
case "two" :
return [ ] source . SourceRef { { SourceID : "source" , StartUnitID : 2 , EndUnitID : 2 } } , nil
default :
return nil , nil
}
} )
debug := newCapturedDebugRecorder ( )
if _ , err := New ( ) . Run ( context . Background ( ) , RunInput { Prepared : prepared , RawInput : [ ] byte ( "input" ) , Debug : debug } ) ; err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
value := decodeCapturedEvidence ( t , encoder )
if ! reflect . DeepEqual ( value . SelectedLanes , [ ] string { "alpha" , "beta" , "inactive" } ) || len ( value . Contexts ) != 1 || len ( value . Contexts [ 0 ] . Units ) != 3 {
t . Fatalf ( "evidence context = %#v, want selected union" , value )
}
if got := value . Contexts [ 0 ] . EvidenceRefs ; len ( got ) != 2 || got [ 0 ] . LaneID != "alpha" || got [ 1 ] . LaneID != "beta" {
t . Fatalf ( "evidence refs = %#v, want both selected lanes" , got )
}
debugJSON := string ( debug . json [ "output/evidence-context.json" ] )
2026-07-27 18:23:48 +00:00
if strings . Contains ( debugJSON , "text-1" ) || strings . Contains ( debugJSON , "metadata" ) || ! strings . Contains ( debugJSON , ` "artifact_kind":"source/evidence-context" ` ) || ! strings . Contains ( debugJSON , ` "schema_id":"notarius.source.evidence_context" ` ) || ! strings . Contains ( debugJSON , ` "context_count":1 ` ) || ! strings . Contains ( debugJSON , ` "unit_count":3 ` ) {
2026-07-27 18:17:54 +00:00
t . Fatalf ( "evidence debug envelope = %s, want only allowlisted summary" , debugJSON )
}
}
func TestRunnerEvidenceContextOmitsAbsentAndRejectedLanes ( t * testing . T ) {
prepared := preparedOrderedPipeline ( t , 2 , orderedLaneSpec { id : "present" , profile : "notes" } , orderedLaneSpec { id : "rejected" , profile : "notes" } )
encoder := & capturingEvidenceOutput { }
prepared . output = encoder
setNormalizedNotes ( prepared , map [ string ] codecNotes { "present" : { Items : [ ] string { "present" } } , "rejected" : { Items : [ ] string { "rejected" } } } )
prepared . Steps [ 1 ] . lanes [ 0 ] . mergeValidators . validators = [ ] preparedValidator { {
resolved : ResolvedValidator { Binding : Binding ( "reject" ) , Target : ValidatorTargetTyped , ArtifactKind : "test/notes" } ,
typedValidate : func ( context . Context , any , typedValidationTarget ) ( contracts . ValidationResult , error ) {
return contracts . ValidationResult { Approved : false , ReasonCode : "rejected" , Message : "not accepted" } , nil
} ,
} }
installEvidencePlan ( prepared , 0 , [ ] string { "absent" , "present" , "rejected" } , func ( notes codecNotes ) ( [ ] source . SourceRef , error ) {
if len ( notes . Items ) > 0 && notes . Items [ 0 ] == "present" {
return [ ] source . SourceRef { { SourceID : "source" , StartUnitID : 1 , EndUnitID : 1 } } , nil
}
return [ ] source . SourceRef { { SourceID : "source" , StartUnitID : 2 , EndUnitID : 2 } } , nil
} )
result , err := New ( ) . Run ( context . Background ( ) , RunInput { Prepared : prepared , RawInput : [ ] byte ( "input" ) } )
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
if len ( result . Rejected ) != 1 || result . Rejected [ 0 ] . LaneID != "rejected" {
t . Fatalf ( "rejections = %#v, want rejected lane unchanged" , result . Rejected )
}
value := decodeCapturedEvidence ( t , encoder )
if len ( value . Contexts ) != 1 || len ( value . Contexts [ 0 ] . EvidenceRefs ) != 1 || value . Contexts [ 0 ] . EvidenceRefs [ 0 ] . LaneID != "present" {
t . Fatalf ( "evidence context = %#v, want present lane only" , value )
}
}
func TestRunnerEvidenceContextFailurePreventsOutputEncoding ( t * testing . T ) {
prepared := preparedConcurrentPipeline ( t , 1 )
encoder := & capturingEvidenceOutput { }
prepared . output = encoder
setNormalizedNotes ( prepared , map [ string ] codecNotes { "notes" : { Items : [ ] string { "invalid" } } } )
installEvidencePlan ( prepared , 0 , [ ] string { "notes" } , func ( codecNotes ) ( [ ] source . SourceRef , error ) {
return [ ] source . SourceRef { { SourceID : "source" , StartUnitID : 99 , EndUnitID : 99 } } , nil
} )
result , err := New ( ) . Run ( context . Background ( ) , RunInput { Prepared : prepared , RawInput : [ ] byte ( "input" ) } )
if err == nil || ! strings . Contains ( err . Error ( ) , "accepted evidence references are invalid" ) {
t . Fatalf ( "Run() error = %v, want evidence context failure" , err )
}
if len ( encoder . requests ) != 0 || result . Manifest . ValidationStatus != "failed" || len ( result . Rejected ) != 0 {
t . Fatalf ( "output requests = %#v manifest = %#v rejected = %#v, want failed run before output encoding" , encoder . requests , result . Manifest , result . Rejected )
}
}
func TestBuildOutputEvidenceContextRejectsIncompatibleAcceptedOutputs ( t * testing . T ) {
prepared := preparedConcurrentPipeline ( t , 1 )
doc := prepared . input . ( * typedTestInput ) . doc
installEvidencePlan ( prepared , 0 , [ ] string { "notes" } , func ( codecNotes ) ( [ ] source . SourceRef , error ) {
return [ ] source . SourceRef { { SourceID : doc . ID , StartUnitID : 1 , EndUnitID : 1 } } , nil
} )
artifact , err := prepared . artifactCodecs . Encode ( "test/notes" , codecNotes { Items : [ ] string { "valid" } } )
if err != nil {
t . Fatal ( err )
}
valid := contracts . SerializedOutput { LaneID : "notes" , SourceID : doc . ID , Artifact : artifact }
for _ , test := range [ ] struct {
name string
outputs [ ] contracts . SerializedOutput
mutate func ( * contracts . SerializedOutput )
want string
} {
{ name : "duplicate lane" , outputs : [ ] contracts . SerializedOutput { valid , valid } , want : "duplicate lane" } ,
{ name : "foreign source" , outputs : [ ] contracts . SerializedOutput { valid } , mutate : func ( output * contracts . SerializedOutput ) { output . SourceID = "other" } , want : "source is incompatible" } ,
{ name : "wrong artifact kind" , outputs : [ ] contracts . SerializedOutput { valid } , mutate : func ( output * contracts . SerializedOutput ) { output . Artifact . Kind = "test/score" } , want : "artifact kind is incompatible" } ,
{ name : "invalid artifact payload" , outputs : [ ] contracts . SerializedOutput { valid } , mutate : func ( output * contracts . SerializedOutput ) { output . Artifact . Content = [ ] byte ( "not JSON" ) } , want : "cannot be decoded" } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
outputs := append ( [ ] contracts . SerializedOutput ( nil ) , test . outputs ... )
for index := range outputs {
outputs [ index ] = contracts . CloneSerializedOutput ( outputs [ index ] )
}
if test . mutate != nil {
test . mutate ( & outputs [ 0 ] )
}
_ , _ , err := buildOutputEvidenceContext ( prepared , doc , outputs )
if err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "buildOutputEvidenceContext() error = %v, want %q" , err , test . want )
}
if strings . Contains ( err . Error ( ) , "not JSON" ) {
t . Fatalf ( "buildOutputEvidenceContext() exposed artifact payload: %v" , err )
}
} )
}
}
func TestEvidenceContextOutputRequestOwnsArtifactBytes ( t * testing . T ) {
artifact := & contracts . SerializedArtifact { Content : [ ] byte ( "original" ) , Metadata : map [ string ] any { "source" : "original" } }
request := contracts . OutputRequest { EvidenceContext : contracts . CloneSerializedArtifactPointer ( artifact ) }
request . EvidenceContext . Content [ 0 ] = 'X'
request . EvidenceContext . Metadata [ "source" ] = "changed"
if string ( artifact . Content ) != "original" || artifact . Metadata [ "source" ] != "original" {
t . Fatalf ( "output request evidence context aliases source artifact: %#v" , artifact )
}
}
func TestRunnerEvidenceContextRebuildsFromAcceptedCheckpoint ( t * testing . T ) {
prepared := preparedConcurrentPipeline ( t , 1 )
encoder := & capturingEvidenceOutput { }
prepared . output = encoder
doc := prepared . input . ( * typedTestInput ) . doc
lane := prepared . Steps [ 0 ] . lanes [ 0 ]
stored , err := checkpointArtifact ( lane . typed . codec , lane . resolved . ID , lane . resolved . Normalize . Module , doc . ID , codecNotes { Items : [ ] string { "stored" } } )
if err != nil {
t . Fatal ( err )
}
loader := newAcceptedCheckpointLoader ( )
key := CheckpointLaneKey ( lane . resolved . StepID , lane . resolved . ID )
loader . accepted [ key ] = NormalizeCheckpoint { Output : stored }
loader . acceptedDecision [ key ] = NewCheckpointDecision ( CheckpointDecisionReused , CheckpointReasonAcceptedArtifactReused )
installEvidencePlan ( prepared , 0 , [ ] string { "notes" } , func ( notes codecNotes ) ( [ ] source . SourceRef , error ) {
if ! reflect . DeepEqual ( notes . Items , [ ] string { "stored" } ) {
return nil , errors . New ( "checkpoint artifact was not projected" )
}
return [ ] source . SourceRef { { SourceID : doc . ID , StartUnitID : 1 , EndUnitID : 1 } } , nil
} )
policy := CheckpointExecutionPolicy { RequireReusableLanes : map [ string ] struct { } { key : { } } }
if _ , err := New ( ) . Run ( context . Background ( ) , RunInput { Prepared : prepared , RawInput : [ ] byte ( "input" ) , Checkpoint : loader , CheckpointPolicy : policy } ) ; err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
value := decodeCapturedEvidence ( t , encoder )
if len ( value . Contexts ) != 1 || value . Contexts [ 0 ] . EvidenceRefs [ 0 ] . LaneID != "notes" {
t . Fatalf ( "evidence context = %#v, want checkpointed normalized output" , value )
}
}
func TestRunnerSkipsEvidenceContextWhenOutputDoesNotOptIn ( t * testing . T ) {
prepared := preparedConcurrentPipeline ( t , 1 )
encoder := & capturingEvidenceOutput { }
prepared . output = encoder
projected := 0
prepared . evidencePlan = nil
if _ , err := New ( ) . Run ( context . Background ( ) , RunInput { Prepared : prepared , RawInput : [ ] byte ( "input" ) } ) ; err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
if projected != 0 || len ( encoder . requests ) != 1 || encoder . requests [ 0 ] . EvidenceContext != nil {
t . Fatalf ( "projected = %d requests = %#v, want no evidence work" , projected , encoder . requests )
}
}