2026-08-09 23:30:32 +00:00
package config
import (
2026-08-29 14:55:24 +00:00
"fmt"
2026-08-09 23:30:32 +00:00
"os"
"path/filepath"
2026-09-05 00:26:54 +00:00
"reflect"
2026-08-09 23:30:32 +00:00
"strings"
"testing"
)
func TestNotariusOmittedAndDisabledBehavior ( t * testing . T ) {
dir := t . TempDir ( )
omittedPath := filepath . Join ( dir , "omitted.yml" )
if err := os . WriteFile ( omittedPath , [ ] byte ( testPipelineBaseYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write omitted pipeline: %v" , err )
}
omitted , err := LoadPipeline ( omittedPath )
if err != nil {
t . Fatalf ( "LoadPipeline(omitted) error = %v" , err )
}
if omitted . Notarius != nil {
t . Fatalf ( "Notarius = %#v, want nil when omitted" , omitted . Notarius )
}
disabledPath := filepath . Join ( dir , "disabled.yml" )
disabledYAML := testPipelineBaseYAML + `
notarius :
enabled : false
config_path : relative / notarius . yml
`
if err := os . WriteFile ( disabledPath , [ ] byte ( disabledYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write disabled pipeline: %v" , err )
}
disabled , err := LoadPipeline ( disabledPath )
if err != nil {
t . Fatalf ( "LoadPipeline(disabled) error = %v" , err )
}
if disabled . Notarius == nil {
t . Fatal ( "Notarius = nil, want configured disabled section" )
}
if disabled . Notarius . Enabled {
t . Fatal ( "Notarius.Enabled = true, want false" )
}
if disabled . Notarius . Binary != DefaultNotariusBinary || disabled . Notarius . Timeout != DefaultNotariusTimeout {
t . Fatalf ( "disabled defaults = %#v" , disabled . Notarius )
}
if disabled . Notarius . ConfigPath != "relative/notarius.yml" || disabled . Notarius . WorkingDirectory != "" {
t . Fatalf ( "disabled paths were resolved unexpectedly: %#v" , disabled . Notarius )
}
}
func TestNotariusEnabledDefaultsAndPathResolution ( t * testing . T ) {
dir := t . TempDir ( )
pipelinePath := filepath . Join ( dir , "deployment" , "pipeline.yml" )
if err := os . MkdirAll ( filepath . Dir ( pipelinePath ) , 0 o755 ) ; err != nil {
t . Fatalf ( "MkdirAll() error = %v" , err )
}
pipelineYAML := testPipelineBaseYAML + `
notarius :
enabled : true
config_path : notarius / config . yml
pipeline_id : dnd - session
outputs :
npc_registry :
lane_id : npc - registry
media_type : application / json
schema_id : notarius . dnd . npc_registry
schema_version : v1
`
if err := os . WriteFile ( pipelinePath , [ ] byte ( pipelineYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write pipeline: %v" , err )
}
cfg , err := LoadPipeline ( pipelinePath )
if err != nil {
t . Fatalf ( "LoadPipeline() error = %v" , err )
}
wantConfigPath := filepath . Join ( filepath . Dir ( pipelinePath ) , "notarius" , "config.yml" )
if cfg . Notarius . Binary != "notarius" || cfg . Notarius . Timeout != "3h" {
t . Fatalf ( "defaults = %#v" , cfg . Notarius )
}
if cfg . Notarius . ConfigPath != wantConfigPath {
t . Fatalf ( "config_path = %q, want %q" , cfg . Notarius . ConfigPath , wantConfigPath )
}
if cfg . Notarius . WorkingDirectory != filepath . Dir ( wantConfigPath ) {
t . Fatalf ( "working_directory = %q, want %q" , cfg . Notarius . WorkingDirectory , filepath . Dir ( wantConfigPath ) )
}
explicitPath := filepath . Join ( dir , "explicit.yml" )
explicitYAML := strings . Replace ( pipelineYAML , " pipeline_id: dnd-session\n" , " pipeline_id: dnd-session\n working_directory: runtime\n" , 1 )
if err := os . WriteFile ( explicitPath , [ ] byte ( explicitYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write explicit pipeline: %v" , err )
}
explicit , err := LoadPipeline ( explicitPath )
if err != nil {
t . Fatalf ( "LoadPipeline(explicit working directory) error = %v" , err )
}
if explicit . Notarius . WorkingDirectory != filepath . Join ( dir , "runtime" ) {
t . Fatalf ( "explicit working_directory = %q, want %q" , explicit . Notarius . WorkingDirectory , filepath . Join ( dir , "runtime" ) )
}
}
2026-08-30 18:51:20 +00:00
func TestNotariusRelativePathsDoNotMakeEffectiveDigestLocationDependent ( t * testing . T ) {
pipelineYAML := testPipelineBaseYAML + `
notarius :
enabled : true
config_path : notarius / config . yml
pipeline_id : dnd - session
`
paths := make ( [ ] string , 2 )
configs := make ( [ ] * PipelineConfig , 2 )
for index := range paths {
dir := t . TempDir ( )
paths [ index ] = filepath . Join ( dir , "pipeline.yml" )
if err := os . WriteFile ( paths [ index ] , [ ] byte ( pipelineYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write pipeline %d: %v" , index , err )
}
loaded , err := LoadPipeline ( paths [ index ] )
if err != nil {
t . Fatalf ( "LoadPipeline(%d) error = %v" , index , err )
}
configs [ index ] = loaded
}
if configs [ 0 ] . Notarius . ConfigPath == configs [ 1 ] . Notarius . ConfigPath ||
configs [ 0 ] . Notarius . WorkingDirectory == configs [ 1 ] . Notarius . WorkingDirectory {
t . Fatalf ( "runtime Notarius paths should remain location-specific: %#v / %#v" , configs [ 0 ] . Notarius , configs [ 1 ] . Notarius )
}
if first , second := EffectivePipelineDigest ( configs [ 0 ] ) , EffectivePipelineDigest ( configs [ 1 ] ) ; first == "" || first != second {
t . Fatalf ( "relocated logical configuration digests = %q / %q, want equal non-empty values" , first , second )
}
}
2026-08-09 23:30:32 +00:00
func TestNotariusStrictYAML ( t * testing . T ) {
tests := [ ] struct {
name string
yaml string
} {
{ name : "unknown section field" , yaml : "notarius:\n unknown: true\n" } ,
{ name : "unknown output field" , yaml : "notarius:\n outputs:\n npc_registry:\n lane_id: npc-registry\n unknown: true\n" } ,
{ name : "unsupported session id" , yaml : "notarius:\n session_id: forbidden\n" } ,
{ name : "unsupported model" , yaml : "notarius:\n model: forbidden\n" } ,
2026-08-29 14:55:24 +00:00
{ name : "duplicate reference selector" , yaml : "notarius:\n references:\n party: narratio.input.party\n party: narratio.input.players\n" } ,
2026-08-09 23:30:32 +00:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "pipeline.yml" )
if err := os . WriteFile ( path , [ ] byte ( testPipelineBaseYAML + "\n" + tt . yaml ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write pipeline: %v" , err )
}
2026-08-30 12:33:43 +00:00
_ , err := LoadPipeline ( path )
want := "strict decode failed"
if tt . name == "duplicate reference selector" {
want = "duplicate YAML key"
}
if err == nil || ! strings . Contains ( err . Error ( ) , want ) {
t . Fatalf ( "LoadPipeline() error = %v, want containing %q" , err , want )
2026-08-09 23:30:32 +00:00
}
} )
}
}
2026-08-29 14:55:24 +00:00
func TestNotariusReferenceValidationAndNormalization ( t * testing . T ) {
cfg := validNotariusConfig ( )
cfg . References = map [ string ] string {
" party " : " narratio.input.party " ,
" chunk . players " : "narratio.input.players" ,
" npc-registry . extract . glossary " : "narratio.input.glossary" ,
"spells" : "narratio.input.spell_catalog" ,
}
if err := validateNotarius ( cfg , nil ) ; err != nil {
t . Fatalf ( "validateNotarius() error = %v" , err )
}
want := map [ string ] string {
"party" : "narratio.input.party" ,
"chunk.players" : "narratio.input.players" ,
"npc-registry.extract.glossary" : "narratio.input.glossary" ,
"spells" : "narratio.input.spell_catalog" ,
}
if len ( cfg . References ) != len ( want ) {
t . Fatalf ( "normalized references = %#v, want %#v" , cfg . References , want )
}
for selector , source := range want {
if cfg . References [ selector ] != source {
t . Fatalf ( "references[%q] = %q, want %q" , selector , cfg . References [ selector ] , source )
}
}
}
func TestNotariusReferenceValidationRejectsInvalidBindings ( t * testing . T ) {
tests := [ ] struct {
name string
references map [ string ] string
wantErr string
} {
{ name : "empty selector" , references : map [ string ] string { " " : "narratio.input.party" } , wantErr : "selector is required" } ,
{ name : "equals in selector" , references : map [ string ] string { "party=x" : "narratio.input.party" } , wantErr : "must not contain" } ,
{ name : "invalid stage" , references : map [ string ] string { "lane.prepare.party" : "narratio.input.party" } , wantErr : "middle component" } ,
{ name : "empty source" , references : map [ string ] string { "party" : " " } , wantErr : "source is required" } ,
{ name : "unsupported source" , references : map [ string ] string { "party" : "narratio.input.unknown" } , wantErr : "not a supported prepared input source" } ,
{
name : "normalized collision" ,
references : map [ string ] string {
"chunk.party" : "narratio.input.party" ,
" chunk . party " : "narratio.input.players" ,
} ,
wantErr : "normalize to" ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
cfg := validNotariusConfig ( )
cfg . References = tt . references
err := validateNotarius ( cfg , nil )
if err == nil || ! strings . Contains ( err . Error ( ) , tt . wantErr ) {
t . Fatalf ( "validateNotarius() error = %v, want containing %q" , err , tt . wantErr )
}
} )
}
}
func TestNotariusReferenceLimit ( t * testing . T ) {
for _ , count := range [ ] int { MaxNotariusReferenceBindings , MaxNotariusReferenceBindings + 1 } {
t . Run ( fmt . Sprintf ( "count_%d" , count ) , func ( t * testing . T ) {
cfg := validNotariusConfig ( )
cfg . References = make ( map [ string ] string , count )
for i := 0 ; i < count ; i ++ {
cfg . References [ fmt . Sprintf ( "lane-%03d.party" , i ) ] = "narratio.input.party"
}
err := validateNotarius ( cfg , nil )
if count == MaxNotariusReferenceBindings {
if err != nil {
t . Fatalf ( "validateNotarius() at limit error = %v" , err )
}
return
}
if err == nil || ! strings . Contains ( err . Error ( ) , "at most 256 bindings" ) {
t . Fatalf ( "validateNotarius() above limit error = %v" , err )
}
} )
}
}
func TestNotariusNilAndEmptyReferencesAreValid ( t * testing . T ) {
for _ , references := range [ ] map [ string ] string { nil , { } } {
cfg := validNotariusConfig ( )
cfg . References = references
if err := validateNotarius ( cfg , nil ) ; err != nil {
t . Fatalf ( "validateNotarius(%#v) error = %v" , references , err )
}
}
}
2026-09-05 00:26:54 +00:00
func TestNotariusValidationFailureDoesNotMutateConfig ( t * testing . T ) {
tests := [ ] struct {
name string
mutate func ( * NotariusConfig )
want string
} {
{
name : "reference" ,
mutate : func ( cfg * NotariusConfig ) {
cfg . Binary = " notarius "
cfg . References = map [ string ] string { " party " : "unsupported" }
} ,
want : "pipeline.notarius.references.party" ,
} ,
{
name : "output" ,
mutate : func ( cfg * NotariusConfig ) {
cfg . Binary = " notarius "
cfg . References = map [ string ] string { " party " : " narratio.input.party " }
cfg . Outputs [ "npc_registry" ] = NotariusOutputConfig { LaneID : " missing-schema " }
} ,
want : "pipeline.notarius.outputs.npc_registry.media_type" ,
} ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
cfg := validNotariusConfig ( )
test . mutate ( cfg )
before := cloneNotariusConfigForTest ( cfg )
err := validateNotarius ( cfg , nil )
if err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "validateNotarius() error = %v, want field containing %q" , err , test . want )
}
if ! reflect . DeepEqual ( cfg , before ) {
t . Fatalf ( "config mutated after validation failure:\n got: %#v\nwant: %#v" , cfg , before )
}
} )
}
}
func TestNotariusNormalizationIsIndependentOfMapInsertionOrder ( t * testing . T ) {
first := validNotariusConfig ( )
first . References = make ( map [ string ] string )
first . References [ " party " ] = " narratio.input.party "
first . References [ "chunk . players" ] = " narratio.input.players "
first . Outputs = make ( map [ string ] NotariusOutputConfig )
first . Outputs [ " npc_registry " ] = NotariusOutputConfig {
LaneID : " npc-registry " , MediaType : " application/json " ,
SchemaID : " notarius.dnd.npc_registry " , SchemaVersion : " v1 " ,
}
first . Outputs [ "spells" ] = NotariusOutputConfig {
LaneID : " spells " , MediaType : " application/json " ,
SchemaID : " notarius.dnd.spells " , SchemaVersion : " v1 " ,
}
second := validNotariusConfig ( )
second . References = make ( map [ string ] string )
second . References [ "chunk . players" ] = " narratio.input.players "
second . References [ " party " ] = " narratio.input.party "
second . Outputs = make ( map [ string ] NotariusOutputConfig )
second . Outputs [ "spells" ] = first . Outputs [ "spells" ]
second . Outputs [ " npc_registry " ] = first . Outputs [ " npc_registry " ]
if err := validateNotarius ( first , nil ) ; err != nil {
t . Fatalf ( "validateNotarius(first) error = %v" , err )
}
if err := validateNotarius ( second , nil ) ; err != nil {
t . Fatalf ( "validateNotarius(second) error = %v" , err )
}
if ! reflect . DeepEqual ( first , second ) {
t . Fatalf ( "normalized configs differ:\nfirst: %#v\nsecond: %#v" , first , second )
}
}
func cloneNotariusConfigForTest ( cfg * NotariusConfig ) * NotariusConfig {
clone := * cfg
if cfg . References != nil {
clone . References = make ( map [ string ] string , len ( cfg . References ) )
for key , value := range cfg . References {
clone . References [ key ] = value
}
}
if cfg . Outputs != nil {
clone . Outputs = make ( map [ string ] NotariusOutputConfig , len ( cfg . Outputs ) )
for key , value := range cfg . Outputs {
clone . Outputs [ key ] = value
}
}
return & clone
}
2026-08-29 14:55:24 +00:00
func TestNotariusSpellCatalogReferenceRequiresEffectiveInput ( t * testing . T ) {
cfg := loadedValidConfig ( t )
cfg . Pipeline . Notarius = validNotariusConfig ( )
cfg . Pipeline . Notarius . References = map [ string ] string { "spells" : "narratio.input.spell_catalog" }
err := Validate ( cfg )
if err == nil || ! strings . Contains ( err . Error ( ) , "requires campaign.inputs.spell_catalog_file or session.inputs.spell_catalog_file" ) {
t . Fatalf ( "Validate() error = %v, want missing spell catalog input" , err )
}
cfg . StableInputs . SpellCatalogFile = ResolvedInputFile { Path : "./spells.json" , Source : "campaign_config" }
if err := Validate ( cfg ) ; err != nil {
t . Fatalf ( "Validate() with spell catalog error = %v" , err )
}
}
func validNotariusConfig ( ) * NotariusConfig {
return & NotariusConfig {
Enabled : true ,
Binary : "notarius" ,
ConfigPath : "./notarius.yml" ,
PipelineID : "dnd-session" ,
Timeout : "45m" ,
WorkingDirectory : "." ,
Outputs : map [ string ] NotariusOutputConfig {
"npc_registry" : {
LaneID : "npc-registry" ,
MediaType : "application/json" ,
SchemaID : "notarius.dnd.npc_registry" ,
SchemaVersion : "v1" ,
} ,
} ,
}
}
2026-08-09 23:30:32 +00:00
func TestNotariusEnabledValidation ( t * testing . T ) {
valid := ` notarius :
enabled : true
config_path : . / notarius . yml
pipeline_id : dnd - session
timeout : 45 m
outputs :
npc_registry :
lane_id : npc - registry
media_type : application / json
schema_id : notarius . dnd . npc_registry
schema_version : v1
module_key : dnd / npc - registry
`
tests := [ ] struct {
name string
section string
wantErr string
} {
{ name : "valid" , section : valid } ,
{ name : "blank binary" , section : strings . Replace ( valid , " enabled: true\n" , " enabled: true\n binary: \" \"\n" , 1 ) , wantErr : "pipeline.notarius.binary is required" } ,
{ name : "missing config path" , section : strings . Replace ( valid , " config_path: ./notarius.yml\n" , "" , 1 ) , wantErr : "pipeline.notarius.config_path is required" } ,
{ name : "missing pipeline id" , section : strings . Replace ( valid , " pipeline_id: dnd-session\n" , "" , 1 ) , wantErr : "pipeline.notarius.pipeline_id is required" } ,
{ name : "missing outputs" , section : strings . Split ( valid , " outputs:\n" ) [ 0 ] , wantErr : "pipeline.notarius.outputs must contain at least one output" } ,
{ name : "zero timeout" , section : strings . Replace ( valid , " timeout: 45m" , " timeout: 0s" , 1 ) , wantErr : "pipeline.notarius.timeout must be positive" } ,
{ name : "negative timeout" , section : strings . Replace ( valid , " timeout: 45m" , " timeout: -1s" , 1 ) , wantErr : "pipeline.notarius.timeout must be positive" } ,
{ name : "invalid timeout" , section : strings . Replace ( valid , " timeout: 45m" , " timeout: later" , 1 ) , wantErr : "pipeline.notarius.timeout must be a valid duration" } ,
{ name : "invalid output key" , section : strings . Replace ( valid , " npc_registry:" , " npc-registry:" , 1 ) , wantErr : "outputs keys must match" } ,
{ name : "missing lane" , section : strings . Replace ( valid , " lane_id: npc-registry\n" , "" , 1 ) , wantErr : "lane_id is required" } ,
{ name : "missing media type" , section : strings . Replace ( valid , " media_type: application/json\n" , "" , 1 ) , wantErr : "media_type is required" } ,
{ name : "missing schema id" , section : strings . Replace ( valid , " schema_id: notarius.dnd.npc_registry\n" , "" , 1 ) , wantErr : "schema_id is required" } ,
{ name : "missing schema version" , section : strings . Replace ( valid , " schema_version: v1\n" , "" , 1 ) , wantErr : "schema_version is required" } ,
{
name : "normalized key collision" ,
section : strings . Replace ( valid , " module_key: dnd/npc-registry\n" , ` module_key : dnd / npc - registry
" npc_registry " :
lane_id : npc - registry - two
media_type : application / json
schema_id : two
schema_version : v1
` , 1 ) ,
wantErr : "normalize to duplicate source" ,
} ,
{
name : "duplicate normalized lane" ,
section : strings . Replace ( valid , " module_key: dnd/npc-registry\n" , ` module_key : dnd / npc - registry
spells :
lane_id : " npc-registry "
media_type : application / json
schema_id : two
schema_version : v1
` , 1 ) ,
wantErr : "duplicates pipeline.notarius.outputs.npc_registry.lane_id" ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
pipelinePath , sessionPath := writeConfigFiles ( t , testPipelineBaseYAML + "\n" + tt . section , testSessionBaseYAML )
cfg , err := Load ( pipelinePath , sessionPath )
if err != nil {
t . Fatalf ( "Load() error = %v" , err )
}
err = Validate ( cfg )
if tt . wantErr == "" {
if err != nil {
t . Fatalf ( "Validate() error = %v" , err )
}
output := cfg . Pipeline . Notarius . Outputs [ "npc_registry" ]
if output . LaneID != "npc-registry" || output . ModuleKey != "dnd/npc-registry" {
t . Fatalf ( "normalized output = %#v" , output )
}
return
}
if err == nil || ! strings . Contains ( err . Error ( ) , tt . wantErr ) {
t . Fatalf ( "Validate() error = %v, want containing %q" , err , tt . wantErr )
}
} )
}
}
func TestExtractionReferencesRequireDeclaredOutput ( t * testing . T ) {
declared := ` notarius :
enabled : false
outputs :
npc_registry : { }
`
tests := [ ] struct {
name string
body string
wantErr string
} {
{
name : "scriptorium declared extraction" ,
body : declared + ` scriptorium :
artifacts :
recap :
inputs :
npcs :
source : narratio . extraction . npc_registry
` ,
} ,
{
name : "scriptorium unknown extraction" ,
body : declared + ` scriptorium :
artifacts :
recap :
inputs :
npcs :
source : narratio . extraction . unknown
` ,
wantErr : ` references unknown extraction output "unknown" ` ,
} ,
{
name : "publish declared extraction" ,
body : declared + ` publish :
outputs :
- source : narratio . extraction . npc_registry
dest : artifacts / npc - registry . json
` ,
} ,
{
name : "publish unknown extraction" ,
body : declared + ` publish :
outputs :
- source : narratio . extraction . unknown
dest : artifacts / unknown . json
` ,
wantErr : ` extraction output "unknown" is not defined ` ,
} ,
{
name : "publish lock declared extraction" ,
body : declared + ` publish :
locks :
- source : narratio . extraction . npc_registry
` ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
pipelinePath , sessionPath := writeConfigFiles ( t , testPipelineBaseYAML + "\n" + tt . body , testSessionBaseYAML )
cfg , err := Load ( pipelinePath , sessionPath )
if err != nil {
t . Fatalf ( "Load() error = %v" , err )
}
err = Validate ( cfg )
if tt . wantErr == "" {
if err != nil {
t . Fatalf ( "Validate() error = %v" , err )
}
return
}
if err == nil || ! strings . Contains ( err . Error ( ) , tt . wantErr ) {
t . Fatalf ( "Validate() error = %v, want containing %q" , err , tt . wantErr )
}
} )
}
}