2026-07-26 17:12:57 +00:00
package cli
import (
"bytes"
"encoding/json"
"errors"
"io"
"path/filepath"
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)
func TestRunResultEncodesRequiredFieldsAndCounts ( t * testing . T ) {
result , err := newRunResult ( testResolvedPipeline ( pipeline . DefaultOutputModule ) , testRunOutput ( ) , "relative-output" , "relative-debug" )
if err != nil {
t . Fatal ( err )
}
encoded , err := encodeRunResult ( result )
if err != nil {
t . Fatal ( err )
}
if encoded [ len ( encoded ) - 1 ] != '\n' || bytes . Count ( encoded , [ ] byte { '\n' } ) != 1 {
t . Fatalf ( "encoded result is not one newline-terminated object: %q" , encoded )
}
var decoded map [ string ] any
if err := json . Unmarshal ( encoded , & decoded ) ; err != nil {
t . Fatal ( err )
}
if got := decoded [ "schema_version" ] ; got != runResultSchemaVersion {
t . Fatalf ( "schema_version = %q" , got )
}
if got := decoded [ "run_id" ] ; got != "run-123" {
t . Fatalf ( "run_id = %q" , got )
}
if got := decoded [ "pipeline_id" ] ; got != "sample" {
t . Fatalf ( "pipeline_id = %q" , got )
}
if got := decoded [ "validation_status" ] ; got != "rejected" {
t . Fatalf ( "validation_status = %q" , got )
}
if got := decoded [ "index_file" ] ; got != "index.json" {
t . Fatalf ( "index_file = %q" , got )
}
if got := decoded [ "normalized_output_count" ] ; got != float64 ( 2 ) {
t . Fatalf ( "normalized_output_count = %v" , got )
}
if got := decoded [ "rejected_output_count" ] ; got != float64 ( 1 ) {
t . Fatalf ( "rejected_output_count = %v" , got )
}
2026-08-27 16:50:32 +00:00
if got := decoded [ "warning_group_count" ] ; got != float64 ( 1 ) || decoded [ "warning_occurrence_count" ] != float64 ( 1 ) || decoded [ "diagnostic_group_count" ] != float64 ( 0 ) || decoded [ "diagnostic_occurrence_count" ] != float64 ( 0 ) || decoded [ "diagnostics_truncated" ] != false {
t . Fatalf ( "diagnostic counts = %#v" , decoded )
2026-07-26 17:12:57 +00:00
}
2026-08-27 01:27:25 +00:00
if got := decoded [ "validation_summaries" ] ; got != nil {
t . Fatalf ( "validation_summaries = %#v, want omitted when empty" , got )
}
2026-07-26 17:12:57 +00:00
if got := decoded [ "output_directory" ] ; got != filepath . Join ( mustWorkingDirectory ( t ) , "relative-output" ) {
t . Fatalf ( "output_directory = %q" , got )
}
if got := decoded [ "debug_directory" ] ; got != filepath . Join ( mustWorkingDirectory ( t ) , "relative-debug" ) {
t . Fatalf ( "debug_directory = %q" , got )
}
}
func TestRunResultRejectsInvalidRequiredValues ( t * testing . T ) {
tests := [ ] struct {
name string
resolved pipeline . ResolvedPipeline
output pipeline . RunOutput
directory string
} {
{ name : "blank run ID" , resolved : testResolvedPipeline ( pipeline . DefaultOutputModule ) , output : testRunOutputWithout ( func ( output * pipeline . RunOutput ) { output . Manifest . RunID = " " } ) , directory : "output" } ,
{ name : "blank resolved pipeline ID" , resolved : pipeline . ResolvedPipeline { Output : pipeline . ModuleBinding { Module : pipeline . DefaultOutputModule } } , output : testRunOutput ( ) , directory : "output" } ,
{ name : "blank manifest pipeline ID" , resolved : testResolvedPipeline ( pipeline . DefaultOutputModule ) , output : testRunOutputWithout ( func ( output * pipeline . RunOutput ) { output . Manifest . PipelineID = "" } ) , directory : "output" } ,
{ name : "mismatched pipeline IDs" , resolved : testResolvedPipeline ( pipeline . DefaultOutputModule ) , output : testRunOutputWithout ( func ( output * pipeline . RunOutput ) { output . Manifest . PipelineID = "other" } ) , directory : "output" } ,
{ name : "blank validation status" , resolved : testResolvedPipeline ( pipeline . DefaultOutputModule ) , output : testRunOutputWithout ( func ( output * pipeline . RunOutput ) { output . Manifest . ValidationStatus = " " } ) , directory : "output" } ,
{ name : "blank output directory" , resolved : testResolvedPipeline ( pipeline . DefaultOutputModule ) , output : testRunOutput ( ) , directory : " " } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
if _ , err := newRunResult ( tt . resolved , tt . output , tt . directory , "" ) ; err == nil {
t . Fatal ( "newRunResult() succeeded" )
}
} )
}
}
func TestRunResultOmitsIndexFileForOtherOutputModules ( t * testing . T ) {
result , err := newRunResult ( testResolvedPipeline ( "test/output" ) , testRunOutputWithout ( func ( output * pipeline . RunOutput ) {
output . OutputFiles = nil
} ) , "output" , "" )
if err != nil {
t . Fatal ( err )
}
if result . IndexFile != "" {
t . Fatalf ( "index_file = %q" , result . IndexFile )
}
encoded , err := encodeRunResult ( result )
if err != nil {
t . Fatal ( err )
}
var decoded map [ string ] any
if err := json . Unmarshal ( encoded , & decoded ) ; err != nil {
t . Fatal ( err )
}
if _ , ok := decoded [ "index_file" ] ; ok {
t . Fatalf ( "encoded non-JSON result contains index_file: %s" , encoded )
}
if _ , ok := decoded [ "debug_directory" ] ; ok {
t . Fatalf ( "encoded result without debug capture contains debug_directory: %s" , encoded )
}
}
2026-08-27 01:27:25 +00:00
func TestRunResultProjectsOwnedValidationSummaries ( t * testing . T ) {
output := testRunOutput ( )
output . Manifest . ValidationSummaries = [ ] artifacts . ValidationSummary { { Status : "incomplete" , IncompleteValidators : [ ] string { "validator" } , ProducerAttemptCount : 1 , TerminalAction : "warn_continue" } }
result , err := newRunResult ( testResolvedPipeline ( pipeline . DefaultOutputModule ) , output , "output" , "" )
if err != nil {
t . Fatal ( err )
}
output . Manifest . ValidationSummaries [ 0 ] . IncompleteValidators [ 0 ] = "caller mutation"
if got := result . ValidationSummaries [ 0 ] . IncompleteValidators ; len ( got ) != 1 || got [ 0 ] != "validator" {
t . Fatalf ( "result validation summaries = %#v" , result . ValidationSummaries )
}
encoded , err := encodeRunResult ( result )
if err != nil {
t . Fatal ( err )
}
if ! bytes . Contains ( encoded , [ ] byte ( ` "validation_summaries":[ { "status":"incomplete","incomplete_validators":["validator"],"producer_attempt_count":1,"terminal_action":"warn_continue"}] ` ) ) {
t . Fatalf ( "encoded result = %s" , encoded )
}
}
2026-07-26 17:12:57 +00:00
func TestRunResultRequiresOneProductionIndexFile ( t * testing . T ) {
tests := [ ] struct {
name string
files [ ] contracts . OutputFile
} {
{ name : "missing" , files : nil } ,
{ name : "duplicate" , files : [ ] contracts . OutputFile { { Name : "index.json" } , { Name : "index.json" } } } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
output := testRunOutput ( )
output . OutputFiles = tt . files
if _ , err := newRunResult ( testResolvedPipeline ( pipeline . DefaultOutputModule ) , output , "output" , "" ) ; err == nil {
t . Fatal ( "newRunResult() succeeded" )
}
} )
}
}
func TestWriteRunResultCompletesAndReportsWriterFailure ( t * testing . T ) {
content := [ ] byte ( "result\n" )
var target bytes . Buffer
if err := writeRunResult ( partialResultWriter { writer : & target , limit : 2 } , content ) ; err != nil {
t . Fatal ( err )
}
if got := target . String ( ) ; got != string ( content ) {
t . Fatalf ( "written result = %q" , got )
}
writerErr := errors . New ( "result writer failed" )
if err := writeRunResult ( failingResultWriter { err : writerErr } , content ) ; ! errors . Is ( err , writerErr ) {
t . Fatalf ( "writeRunResult() error = %v" , err )
}
if err := writeRunResult ( zeroResultWriter { } , content ) ; ! errors . Is ( err , io . ErrShortWrite ) {
t . Fatalf ( "zero-progress error = %v" , err )
}
}
func testResolvedPipeline ( outputModule string ) pipeline . ResolvedPipeline {
return pipeline . ResolvedPipeline { ID : "sample" , Output : pipeline . ModuleBinding { Module : outputModule } }
}
func testRunOutput ( ) pipeline . RunOutput {
return pipeline . RunOutput {
Manifest : artifacts . RunManifest { RunID : "run-123" , PipelineID : "sample" , ValidationStatus : "rejected" } ,
NormalizeOutputs : [ ] contracts . SerializedOutput { { } , { } } ,
Rejected : [ ] contracts . RejectedOutput { { } } ,
2026-08-27 16:40:17 +00:00
Diagnostics : contracts . DiagnosticCollection { Groups : [ ] contracts . DiagnosticGroup { {
2026-08-27 16:50:32 +00:00
Disposition : contracts . DiagnosticDispositionWarning ,
2026-08-27 18:41:59 +00:00
Category : contracts . DiagnosticCategoryFallback ,
ReasonCode : "fallback" ,
Origin : contracts . DiagnosticOrigin { Stage : contracts . DiagnosticOriginStageNormalize , StepID : "step" , LaneID : "lane" , ModuleKey : "module" } ,
2026-08-27 16:50:32 +00:00
OccurrenceCount : 1 ,
2026-08-27 18:41:59 +00:00
Samples : [ ] contracts . DiagnosticSample { { Scope : "scope" , Message : "message" } } ,
2026-08-27 16:40:17 +00:00
} } } ,
OutputFiles : [ ] contracts . OutputFile { { Name : "index.json" } } ,
2026-07-26 17:12:57 +00:00
}
}
func testRunOutputWithout ( change func ( * pipeline . RunOutput ) ) pipeline . RunOutput {
output := testRunOutput ( )
change ( & output )
return output
}
func mustWorkingDirectory ( t * testing . T ) string {
t . Helper ( )
workingDirectory , err := filepath . Abs ( "." )
if err != nil {
t . Fatal ( err )
}
return workingDirectory
}
type partialResultWriter struct {
writer io . Writer
limit int
}
func ( w partialResultWriter ) Write ( content [ ] byte ) ( int , error ) {
if len ( content ) > w . limit {
content = content [ : w . limit ]
}
return w . writer . Write ( content )
}
type failingResultWriter struct { err error }
func ( w failingResultWriter ) Write ( [ ] byte ) ( int , error ) { return 0 , w . err }
type zeroResultWriter struct { }
func ( zeroResultWriter ) Write ( [ ] byte ) ( int , error ) { return 0 , nil }