2026-05-02 10:43:45 -05:00
package config
import (
2026-08-30 15:17:46 +00:00
"io/fs"
2026-05-02 10:43:45 -05:00
"os"
"path/filepath"
2026-08-10 00:42:44 +00:00
"reflect"
2026-08-30 15:17:46 +00:00
"sort"
2026-05-02 10:43:45 -05:00
"strings"
"testing"
)
2026-08-10 21:32:15 +00:00
func TestRepresentativeLoadDefaultsAndValidation ( t * testing . T ) {
pipelinePath , sessionPath := writeConfigFiles ( t , ` whisperx :
transcribe_url : https : //transcription.example.com/transcribe
` , ` session_id : 2026 - 05 - 03
2026-05-02 10:43:45 -05:00
inputs :
audio_dir : . / audio
2026-08-10 21:32:15 +00:00
` )
2026-05-02 10:43:45 -05:00
2026-08-10 21:32:15 +00:00
cfg , err := Load ( pipelinePath , sessionPath )
if err != nil {
t . Fatalf ( "Load() error = %v" , err )
}
if cfg . Pipeline . Workspace . Root != DefaultWorkspaceRoot {
t . Fatalf ( "workspace.root = %q, want %q" , cfg . Pipeline . Workspace . Root , DefaultWorkspaceRoot )
}
if cfg . Pipeline . Storage . Backend != StorageBackendLocal || cfg . Pipeline . Storage . S3 != nil {
t . Fatalf ( "storage = %#v, want local backend without S3 configuration" , cfg . Pipeline . Storage )
}
if cfg . Pipeline . WhisperX . Timeout != DefaultWhisperXTimeout || cfg . Pipeline . WhisperX . RetryDelay != DefaultWhisperXRetryDelay {
t . Fatalf ( "whisperx defaults = %#v, want timeout and retry-delay defaults" , cfg . Pipeline . WhisperX )
}
if err := Validate ( cfg ) ; err != nil {
t . Fatalf ( "Validate() error = %v" , err )
2026-05-02 10:43:45 -05:00
}
}
2026-08-30 14:43:25 +00:00
func TestMarshalEffectivePipelineRendersStablePublicConfiguration ( t * testing . T ) {
dir := t . TempDir ( )
path := filepath . Join ( dir , "pipeline.yml" )
if err := os . WriteFile ( path , [ ] byte ( ` scriptorium :
artifacts :
zeta :
enabled : false
output_path : artifacts / zeta . md
alpha :
enabled : false
output_path : artifacts / alpha . md
workspace :
root : / srv / narratio
whisperx :
transcribe_url : https : //transcription.example.com/transcribe
` ) , 0 o644 ) ; err != nil {
t . Fatal ( err )
}
first , err := LoadPipeline ( path )
if err != nil {
t . Fatal ( err )
}
second , err := LoadPipeline ( path )
if err != nil {
t . Fatal ( err )
}
firstYAML , err := MarshalEffectivePipeline ( first )
if err != nil {
t . Fatal ( err )
}
secondYAML , err := MarshalEffectivePipeline ( second )
if err != nil {
t . Fatal ( err )
}
if string ( firstYAML ) != string ( secondYAML ) || ! strings . HasSuffix ( string ( firstYAML ) , "\n" ) {
t . Fatalf ( "effective YAML is not stable: first=%q second=%q" , firstYAML , secondYAML )
}
output := string ( firstYAML )
if strings . Contains ( output , "artifact_families" ) || strings . Contains ( output , "resolution" ) {
t . Fatalf ( "effective YAML leaked runtime fields: %q" , output )
}
if strings . Index ( output , "alpha:" ) > strings . Index ( output , "zeta:" ) {
t . Fatalf ( "configured artifacts are not sorted: %q" , output )
}
}
2026-05-02 10:43:45 -05:00
func TestValidateMissingAudioSource ( t * testing . T ) {
2026-08-10 21:32:15 +00:00
cfg := loadedValidConfig ( t )
cfg . Session . Inputs . AudioDir = ""
cfg . Session . Inputs . AudioFiles = nil
cfg . Session . Inputs . AudioS3 = nil
2026-05-02 10:43:45 -05:00
err := Validate ( cfg )
if err == nil {
t . Fatal ( "expected validation error, got nil" )
}
2026-05-16 14:11:59 +00:00
if ! strings . Contains ( err . Error ( ) , "audio_dir, at least one audio_files entry, or audio_s3" ) {
2026-05-02 10:43:45 -05:00
t . Fatalf ( "error = %q, want audio source guidance" , err . Error ( ) )
}
2026-05-02 11:44:15 -05:00
if ! strings . Contains ( err . Error ( ) , "session config" ) {
t . Fatalf ( "error = %q, want session config context" , err . Error ( ) )
}
}
func TestExamplesLoadAndValidate ( t * testing . T ) {
2026-05-19 19:46:40 -05:00
examplesDir := filepath . Join ( ".." , ".." , "examples" )
2026-08-30 15:17:46 +00:00
if got , want := maintainedPipelineRoots ( t , examplesDir ) , [ ] string {
"pipeline.extraction-subset.yml" ,
"pipeline.full.annotated.yml" ,
"pipeline.minimal.yml" ,
"pipeline.production.yml" ,
filepath . Join ( "production-testing" , "pipeline.yml" ) ,
} ; ! reflect . DeepEqual ( got , want ) {
t . Fatalf ( "maintained pipeline roots = %#v, want %#v" , got , want )
}
2026-05-18 22:03:59 -05:00
tests := [ ] struct {
name string
pipelineFile string
sessionFile string
} {
{
name : "minimal pipeline with local audio session" ,
pipelineFile : "pipeline.minimal.yml" ,
sessionFile : "session.local-audio.yml" ,
} ,
{
name : "production pipeline with s3 audio session" ,
pipelineFile : "pipeline.production.yml" ,
sessionFile : "session.s3-audio.yml" ,
} ,
{
name : "full annotated pipeline with local audio session" ,
pipelineFile : "pipeline.full.annotated.yml" ,
sessionFile : "session.local-audio.yml" ,
} ,
2026-08-10 00:42:44 +00:00
{
name : "extraction subset pipeline with local audio session" ,
pipelineFile : "pipeline.extraction-subset.yml" ,
sessionFile : "session.local-audio.yml" ,
} ,
2026-05-02 11:44:15 -05:00
}
2026-05-18 22:03:59 -05:00
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
pipelinePath := filepath . Join ( examplesDir , tt . pipelineFile )
2026-05-22 23:01:27 -05:00
campaignPath := filepath . Join ( examplesDir , "campaigns" , "sample-campaign" , "campaign.yml" )
2026-05-18 22:03:59 -05:00
sessionPath := filepath . Join ( examplesDir , tt . sessionFile )
2026-05-22 17:38:23 -05:00
cfg , err := Load ( pipelinePath , campaignPath , sessionPath )
2026-05-18 22:03:59 -05:00
if err != nil {
t . Fatalf ( "load example config error = %v" , err )
}
if err := Validate ( cfg ) ; err != nil {
t . Fatalf ( "validate example config error = %v" , err )
}
} )
2026-05-02 11:44:15 -05:00
}
2026-08-30 15:17:46 +00:00
t . Run ( "split production and testing bundle" , func ( t * testing . T ) {
pipelinePath := filepath . Join ( examplesDir , "production-testing" , "pipeline.yml" )
campaignPath := filepath . Join ( examplesDir , "campaigns" , "sample-campaign" , "campaign.yml" )
sessionPath := filepath . Join ( examplesDir , "session.local-audio.yml" )
campaign , err := LoadCampaign ( campaignPath )
if err != nil {
t . Fatalf ( "load split bundle campaign: %v" , err )
}
for _ , profile := range [ ] struct {
name string
model string
} {
{ name : "production" , model : "narratio-production-model-placeholder" } ,
{ name : "testing" , model : "narratio-testing-model-placeholder" } ,
} {
t . Run ( profile . name , func ( t * testing . T ) {
selected := profile . name
pipeline , err := LoadPipelineWithOptions ( pipelinePath , PipelineLoadOptions { Profile : & selected } )
if err != nil {
t . Fatalf ( "load split bundle pipeline: %v" , err )
}
loaded , err := LoadPipelineCampaign ( pipelinePath , pipeline , campaignPath , campaign )
if err != nil {
t . Fatalf ( "resolve split bundle campaign: %v" , err )
}
cfg , err := LoadSessionWithPipelineCampaignOptions ( loaded , sessionPath , SessionLoadOptions { } )
if err != nil {
t . Fatalf ( "load split bundle session: %v" , err )
}
if err := Validate ( cfg ) ; err != nil {
t . Fatalf ( "validate split bundle: %v" , err )
}
if cfg . Pipeline . Audita . Model != profile . model {
t . Fatalf ( "audita model = %q, want %q" , cfg . Pipeline . Audita . Model , profile . model )
}
if cfg . Pipeline . Secrets != nil || cfg . Pipeline . Audita . LLMAPIKeyEnv != "" || cfg . Pipeline . Storage . Backend != StorageBackendLocal || cfg . Pipeline . Storage . S3 != nil {
t . Fatalf ( "split bundle must remain offline and secret-free: secrets=%#v audita=%#v storage=%#v" , cfg . Pipeline . Secrets , cfg . Pipeline . Audita , cfg . Pipeline . Storage )
}
if selectedProfile , ok := SelectedPipelineProfile ( cfg . Pipeline ) ; ! ok || selectedProfile . Name != profile . name {
t . Fatalf ( "selected profile = %#v, want %q" , selectedProfile , profile . name )
}
for _ , key := range [ ] string {
"character_meta_arannis" ,
"character_meta_brenna" ,
"character_items_arannis" ,
"character_items_brenna" ,
} {
if _ , exists := cfg . Pipeline . Scriptorium . Artifacts [ key ] ; ! exists {
t . Fatalf ( "expanded artifact %q is absent" , key )
}
}
if got := cfg . Pipeline . Scriptorium . Artifacts [ "character_items_arannis" ] . Inputs [ "character_meta" ] . Source ; got != "narratio.artifact.character_meta_arannis" {
t . Fatalf ( "same-member input source = %q" , got )
}
2026-09-02 12:04:35 +00:00
if cfg . Party . Canonical == nil || len ( cfg . Party . Canonical . Characters ) != 2 {
2026-08-30 15:17:46 +00:00
t . Fatalf ( "canonical party = %#v" , cfg . Party )
}
characters := cfg . Party . Canonical . Characters
if characters [ 0 ] . Player . Name != characters [ 1 ] . Player . Name || len ( characters [ 0 ] . Character . Aliases ) != 2 || len ( characters [ 1 ] . Character . Classes ) != 2 {
t . Fatalf ( "canonical party does not preserve repeated player, aliases, and multiclass data: %#v" , characters )
}
if len ( cfg . Pipeline . Publish . Outputs ) != 3 {
t . Fatalf ( "publish outputs = %#v, want transcript plus two family outputs" , cfg . Pipeline . Publish . Outputs )
}
if profile . name == "testing" {
artifact , exists := cfg . Pipeline . Scriptorium . Artifacts [ "testing_notes" ]
if ! exists || artifact . Enabled {
t . Fatalf ( "testing-only disabled artifact = %#v" , artifact )
}
}
} )
}
} )
}
func maintainedPipelineRoots ( t * testing . T , examplesDir string ) [ ] string {
t . Helper ( )
var roots [ ] string
err := filepath . WalkDir ( examplesDir , func ( path string , entry fs . DirEntry , walkErr error ) error {
if walkErr != nil {
return walkErr
}
if entry . IsDir ( ) {
return nil
}
name := entry . Name ( )
if name != "pipeline.yml" && ! ( strings . HasPrefix ( name , "pipeline." ) && ( strings . HasSuffix ( name , ".yml" ) || strings . HasSuffix ( name , ".yaml" ) ) ) {
return nil
}
relative , err := filepath . Rel ( examplesDir , path )
if err != nil {
return err
}
roots = append ( roots , relative )
return nil
} )
if err != nil {
t . Fatalf ( "discover maintained pipeline roots: %v" , err )
}
sort . Strings ( roots )
return roots
2026-05-02 10:43:45 -05:00
}
2026-08-10 00:42:44 +00:00
func TestMaintainedExtractionExamplesPreservePublishedContracts ( t * testing . T ) {
examplesDir := filepath . Join ( ".." , ".." , "examples" )
full , err := LoadPipeline ( filepath . Join ( examplesDir , "pipeline.full.annotated.yml" ) )
if err != nil {
t . Fatalf ( "load full example error = %v" , err )
}
want := map [ string ] NotariusOutputConfig {
"item_registry" : { LaneID : "item-registry" , MediaType : "application/json" , SchemaID : "notarius.dnd.item_registry" , SchemaVersion : "v1" , ModuleKey : "dnd/item-registry" } ,
"npc_registry" : { LaneID : "npc-registry" , MediaType : "application/json" , SchemaID : "notarius.dnd.npc_registry" , SchemaVersion : "v1" , ModuleKey : "dnd/npc-registry" } ,
"location_registry" : { LaneID : "location-registry" , MediaType : "application/json" , SchemaID : "notarius.dnd.location_registry" , SchemaVersion : "v1" , ModuleKey : "dnd/location-registry" } ,
"scene_descriptions" : { LaneID : "scene-descriptions" , MediaType : "application/json" , SchemaID : "notarius.dnd.scene_descriptions" , SchemaVersion : "v1" , ModuleKey : "dnd/scene-descriptions" } ,
"item_occurrences" : { LaneID : "item-occurrences" , MediaType : "application/json" , SchemaID : "notarius.dnd.item_occurrences" , SchemaVersion : "v1" , ModuleKey : "dnd/item-occurrences" } ,
"spells" : { LaneID : "spells" , MediaType : "application/json" , SchemaID : "notarius.dnd.spells" , SchemaVersion : "v1" , ModuleKey : "dnd/spells" } ,
"combat_turns" : { LaneID : "combat-turns" , MediaType : "application/json" , SchemaID : "notarius.dnd.combat_turns" , SchemaVersion : "v1" , ModuleKey : "dnd/combat-turns" } ,
"npc_occurrences" : { LaneID : "npc-occurrences" , MediaType : "application/json" , SchemaID : "notarius.dnd.npc_occurrences" , SchemaVersion : "v1" , ModuleKey : "dnd/npc-occurrences" } ,
"location_occurrences" : { LaneID : "location-occurrences" , MediaType : "application/json" , SchemaID : "notarius.dnd.location_occurrences" , SchemaVersion : "v1" , ModuleKey : "dnd/location-occurrences" } ,
"enemy_events" : { LaneID : "enemy-events" , MediaType : "application/json" , SchemaID : "notarius.dnd.enemy_events" , SchemaVersion : "v1" , ModuleKey : "dnd/enemy-events" } ,
}
if full . Notarius == nil || ! reflect . DeepEqual ( full . Notarius . Outputs , want ) {
t . Fatalf ( "full example outputs = %#v, want %#v" , full . Notarius , want )
}
subset , err := LoadPipeline ( filepath . Join ( examplesDir , "pipeline.extraction-subset.yml" ) )
if err != nil {
t . Fatalf ( "load subset example error = %v" , err )
}
brief := subset . Scriptorium . Artifacts [ "session_brief" ]
wantSources := map [ string ] string {
"npcs" : "narratio.extraction.npc_registry" ,
"locations" : "narratio.extraction.location_registry" ,
"scenes" : "narratio.extraction.scene_descriptions" ,
}
gotSources := make ( map [ string ] string , len ( brief . Inputs ) )
for name , input := range brief . Inputs {
gotSources [ name ] = input . Source
}
if ! reflect . DeepEqual ( gotSources , wantSources ) {
t . Fatalf ( "subset example sources = %#v, want %#v" , gotSources , wantSources )
}
}
2026-05-02 10:43:45 -05:00
func writeConfigFiles ( t * testing . T , pipelineYAML , sessionYAML string ) ( string , string ) {
t . Helper ( )
2026-05-04 07:48:59 -05:00
if ! strings . Contains ( pipelineYAML , "\naudita:" ) && ! strings . HasPrefix ( pipelineYAML , "audita:" ) {
if ! strings . HasSuffix ( pipelineYAML , "\n" ) {
pipelineYAML += "\n"
}
pipelineYAML += "audita:\n binary: audita\n"
}
2026-05-16 14:11:59 +00:00
if ! strings . Contains ( sessionYAML , "\ncampaign:" ) && ! strings . HasPrefix ( sessionYAML , "campaign:" ) {
if ! strings . HasSuffix ( sessionYAML , "\n" ) {
sessionYAML += "\n"
}
sessionYAML += "campaign: sample-campaign\n"
}
2026-05-02 10:43:45 -05:00
dir := t . TempDir ( )
pipelinePath := filepath . Join ( dir , "pipeline.yml" )
2026-05-20 20:41:28 -05:00
campaignPath := filepath . Join ( dir , "campaign.yml" )
2026-05-02 10:43:45 -05:00
sessionPath := filepath . Join ( dir , "session.yml" )
if err := os . WriteFile ( pipelinePath , [ ] byte ( pipelineYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write pipeline.yml: %v" , err )
}
2026-05-22 23:01:27 -05:00
campaignYAML := ` campaign_id: ` + campaignNameFromSessionYAML ( sessionYAML ) + `
2026-05-20 20:41:28 -05:00
inputs :
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
2026-05-27 09:34:05 -05:00
party_file : . / party . yml
2026-05-20 20:41:28 -05:00
`
if err := os . WriteFile ( campaignPath , [ ] byte ( campaignYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write campaign.yml: %v" , err )
}
2026-05-02 10:43:45 -05:00
if err := os . WriteFile ( sessionPath , [ ] byte ( sessionYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write session.yml: %v" , err )
}
2026-09-02 11:57:51 +00:00
if err := os . WriteFile ( filepath . Join ( dir , "party.yml" ) , [ ] byte ( canonicalPartyFixture ) , 0 o644 ) ; err != nil {
2026-08-30 14:01:51 +00:00
t . Fatalf ( "write party.yml: %v" , err )
}
2026-05-02 10:43:45 -05:00
return pipelinePath , sessionPath
}
2026-05-20 20:41:28 -05:00
func campaignNameFromSessionYAML ( sessionYAML string ) string {
for _ , line := range strings . Split ( sessionYAML , "\n" ) {
trimmed := strings . TrimSpace ( line )
if strings . HasPrefix ( trimmed , "campaign:" ) {
return strings . Trim ( strings . TrimSpace ( strings . TrimPrefix ( trimmed , "campaign:" ) ) , ` "' ` )
}
}
return "sample-campaign"
}