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
2026-09-05 00:42:47 +00:00
wantField string
wantReason string
2026-08-29 14:55:24 +00:00
} {
2026-09-05 00:42:47 +00:00
{ name : "empty selector" , references : map [ string ] string { " " : "narratio.input.party" } , wantField : "pipeline.notarius.references" , wantReason : "selector is required" } ,
{ name : "equals in selector" , references : map [ string ] string { "party=x" : "narratio.input.party" } , wantField : "pipeline.notarius.references" , wantReason : "must not contain" } ,
{ name : "invalid stage" , references : map [ string ] string { "lane.prepare.party" : "narratio.input.party" } , wantField : "pipeline.notarius.references" , wantReason : "middle component" } ,
{ name : "empty source" , references : map [ string ] string { "party" : " " } , wantField : "pipeline.notarius.references.party" , wantReason : "source is required" } ,
{ name : "unsupported source" , references : map [ string ] string { "party" : "narratio.input.unknown" } , wantField : "pipeline.notarius.references.party" , wantReason : "not a supported prepared input source" } ,
2026-08-29 14:55:24 +00:00
{
name : "normalized collision" ,
references : map [ string ] string {
"chunk.party" : "narratio.input.party" ,
" chunk . party " : "narratio.input.players" ,
} ,
2026-09-05 00:42:47 +00:00
wantField : "pipeline.notarius.references" ,
wantReason : "normalize to" ,
2026-08-29 14:55:24 +00:00
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
cfg := validNotariusConfig ( )
cfg . References = tt . references
err := validateNotarius ( cfg , nil )
2026-09-05 00:42:47 +00:00
if err == nil || ! strings . Contains ( err . Error ( ) , tt . wantField ) || ! strings . Contains ( err . Error ( ) , tt . wantReason ) {
t . Fatalf ( "validateNotarius() error = %v, want field %q and reason %q" , err , tt . wantField , tt . wantReason )
2026-08-29 14:55:24 +00:00
}
} )
}
}
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 )
}
} )
}
}
2026-09-05 03:38:07 +00:00
func TestNotariusNormalizationProducesCanonicalValues ( t * testing . T ) {
cfg := validNotariusConfig ( )
cfg . References = map [ string ] string {
" party " : " narratio.input.party " ,
"chunk . players" : " narratio.input.players " ,
}
cfg . Outputs = map [ string ] NotariusOutputConfig {
" npc_registry " : {
LaneID : " npc-registry " , MediaType : " application/json " ,
SchemaID : " notarius.dnd.npc_registry " , SchemaVersion : " v1 " ,
} ,
"spells" : {
LaneID : " spells " , MediaType : " application/json " ,
SchemaID : " notarius.dnd.spells " , SchemaVersion : " v1 " ,
} ,
2026-09-05 00:26:54 +00:00
}
2026-09-05 03:38:07 +00:00
if err := validateNotarius ( cfg , nil ) ; err != nil {
t . Fatalf ( "validateNotarius() error = %v" , err )
2026-09-05 00:26:54 +00:00
}
2026-09-05 03:38:07 +00:00
wantReferences := map [ string ] string {
"party" : "narratio.input.party" ,
"chunk.players" : "narratio.input.players" ,
2026-09-05 00:26:54 +00:00
}
2026-09-05 03:38:07 +00:00
if ! reflect . DeepEqual ( cfg . References , wantReferences ) {
t . Fatalf ( "references = %#v, want %#v" , cfg . References , wantReferences )
}
if _ , ok := cfg . Outputs [ " npc_registry " ] ; ok {
t . Fatal ( "outputs retain unnormalized key" )
}
if got := cfg . Outputs [ "npc_registry" ] ; got . LaneID != "npc-registry" || got . MediaType != "application/json" || got . SchemaID != "notarius.dnd.npc_registry" || got . SchemaVersion != "v1" {
t . Fatalf ( "normalized npc_registry output = %#v" , got )
}
}
func TestNotariusDiagnosticsUseSortedMapKeys ( t * testing . T ) {
tests := [ ] struct {
name string
configure func ( * NotariusConfig , [ ] string )
wantField string
} {
{
name : "references" ,
configure : func ( cfg * NotariusConfig , keys [ ] string ) {
cfg . References = make ( map [ string ] string , len ( keys ) )
for _ , key := range keys {
cfg . References [ key ] = "narratio.input.unsupported"
}
} ,
wantField : "pipeline.notarius.references.alpha" ,
} ,
{
name : "outputs" ,
configure : func ( cfg * NotariusConfig , keys [ ] string ) {
cfg . References = nil
cfg . Outputs = make ( map [ string ] NotariusOutputConfig , len ( keys ) )
for _ , key := range keys {
cfg . Outputs [ key ] = NotariusOutputConfig { LaneID : key }
}
} ,
wantField : "pipeline.notarius.outputs.alpha.media_type" ,
} ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
orders := [ ] [ ] string { { "zeta" , "alpha" } , { "alpha" , "zeta" } }
diagnostics := make ( [ ] string , 0 , len ( orders ) )
for _ , keys := range orders {
cfg := validNotariusConfig ( )
test . configure ( cfg , keys )
err := validateNotarius ( cfg , nil )
if err == nil || ! strings . Contains ( err . Error ( ) , test . wantField ) {
t . Fatalf ( "validateNotarius() error = %v, want field %q" , err , test . wantField )
}
diagnostics = append ( diagnostics , err . Error ( ) )
}
if diagnostics [ 0 ] != diagnostics [ 1 ] {
t . Fatalf ( "equivalent maps produced different diagnostics:\nfirst: %s\nsecond: %s" , diagnostics [ 0 ] , diagnostics [ 1 ] )
}
} )
2026-09-05 00:26:54 +00:00
}
}
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 ( )
2026-09-05 03:37:00 +00:00
cfg . Pipeline . Notarius . References = map [ string ] string {
"zeta" : "narratio.input.spell_catalog" ,
"alpha" : "narratio.input.spell_catalog" ,
}
2026-08-29 14:55:24 +00:00
err := Validate ( cfg )
2026-09-05 03:37:00 +00:00
if err == nil || ! strings . Contains ( err . Error ( ) , "pipeline.notarius.references.alpha" ) || ! strings . Contains ( err . Error ( ) , "requires campaign.inputs.spell_catalog_file or session.inputs.spell_catalog_file" ) {
t . Fatalf ( "Validate() error = %v, want sorted selector and missing spell catalog input" , err )
2026-08-29 14:55:24 +00:00
}
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
2026-09-05 00:42:47 +00:00
wantErr [ ] string
2026-08-09 23:30:32 +00:00
} {
{ name : "valid" , section : valid } ,
2026-09-05 00:42:47 +00:00
{ name : "blank binary" , section : strings . Replace ( valid , " enabled: true\n" , " enabled: true\n binary: \" \"\n" , 1 ) , wantErr : [ ] string { "pipeline.notarius.binary" , "required" } } ,
{ name : "missing config path" , section : strings . Replace ( valid , " config_path: ./notarius.yml\n" , "" , 1 ) , wantErr : [ ] string { "pipeline.notarius.config_path" , "required" } } ,
{ name : "missing pipeline id" , section : strings . Replace ( valid , " pipeline_id: dnd-session\n" , "" , 1 ) , wantErr : [ ] string { "pipeline.notarius.pipeline_id" , "required" } } ,
{ name : "missing outputs" , section : strings . Split ( valid , " outputs:\n" ) [ 0 ] , wantErr : [ ] string { "pipeline.notarius.outputs" , "at least one" } } ,
{ name : "zero timeout" , section : strings . Replace ( valid , " timeout: 45m" , " timeout: 0s" , 1 ) , wantErr : [ ] string { "pipeline.notarius.timeout" , "positive" } } ,
{ name : "negative timeout" , section : strings . Replace ( valid , " timeout: 45m" , " timeout: -1s" , 1 ) , wantErr : [ ] string { "pipeline.notarius.timeout" , "positive" } } ,
{ name : "invalid timeout" , section : strings . Replace ( valid , " timeout: 45m" , " timeout: later" , 1 ) , wantErr : [ ] string { "pipeline.notarius.timeout" , "valid duration" } } ,
{ name : "invalid output key" , section : strings . Replace ( valid , " npc_registry:" , " npc-registry:" , 1 ) , wantErr : [ ] string { "pipeline.notarius.outputs keys" , "must match" } } ,
{ name : "missing lane" , section : strings . Replace ( valid , " lane_id: npc-registry\n" , "" , 1 ) , wantErr : [ ] string { "pipeline.notarius.outputs.npc_registry.lane_id" , "required" } } ,
{ name : "missing media type" , section : strings . Replace ( valid , " media_type: application/json\n" , "" , 1 ) , wantErr : [ ] string { "pipeline.notarius.outputs.npc_registry.media_type" , "required" } } ,
{ name : "missing schema id" , section : strings . Replace ( valid , " schema_id: notarius.dnd.npc_registry\n" , "" , 1 ) , wantErr : [ ] string { "pipeline.notarius.outputs.npc_registry.schema_id" , "required" } } ,
{ name : "missing schema version" , section : strings . Replace ( valid , " schema_version: v1\n" , "" , 1 ) , wantErr : [ ] string { "pipeline.notarius.outputs.npc_registry.schema_version" , "required" } } ,
2026-08-09 23:30:32 +00:00
{
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 ) ,
2026-09-05 00:42:47 +00:00
wantErr : [ ] string { "pipeline.notarius.outputs keys" , "duplicate source" } ,
2026-08-09 23:30:32 +00:00
} ,
{
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 ) ,
2026-09-05 00:42:47 +00:00
wantErr : [ ] string { "pipeline.notarius.outputs.spells.lane_id" , "duplicates" } ,
2026-08-09 23:30:32 +00:00
} ,
}
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 )
2026-09-05 00:42:47 +00:00
if len ( tt . wantErr ) == 0 {
2026-08-09 23:30:32 +00:00
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
}
2026-09-05 00:42:47 +00:00
if err == nil {
t . Fatalf ( "Validate() error = nil, want containing %q" , tt . wantErr )
}
for _ , fragment := range tt . wantErr {
if ! strings . Contains ( err . Error ( ) , fragment ) {
t . Fatalf ( "Validate() error = %v, want containing %q" , err , fragment )
}
2026-08-09 23:30:32 +00:00
}
} )
}
}
func TestExtractionReferencesRequireDeclaredOutput ( t * testing . T ) {
declared := ` notarius :
enabled : false
outputs :
npc_registry : { }
`
tests := [ ] struct {
name string
body string
2026-09-05 00:42:47 +00:00
wantErr [ ] string
2026-08-09 23:30:32 +00:00
} {
{
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
` ,
2026-09-05 00:42:47 +00:00
wantErr : [ ] string { "pipeline.scriptorium.artifacts.recap.inputs.npcs.source" , "unknown extraction output" } ,
2026-08-09 23:30:32 +00:00
} ,
{
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
` ,
2026-09-05 00:42:47 +00:00
wantErr : [ ] string { "pipeline.publish.outputs[0].source" , "not defined" } ,
2026-08-09 23:30:32 +00:00
} ,
}
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 )
2026-09-05 00:42:47 +00:00
if len ( tt . wantErr ) == 0 {
2026-08-09 23:30:32 +00:00
if err != nil {
t . Fatalf ( "Validate() error = %v" , err )
}
return
}
2026-09-05 00:42:47 +00:00
if err == nil {
t . Fatalf ( "Validate() error = nil, want containing %q" , tt . wantErr )
}
for _ , fragment := range tt . wantErr {
if ! strings . Contains ( err . Error ( ) , fragment ) {
t . Fatalf ( "Validate() error = %v, want containing %q" , err , fragment )
}
2026-08-09 23:30:32 +00:00
}
} )
}
}