2026-08-30 14:01:51 +00:00
package config
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestCanonicalPartyResolvesFromCampaignAndDerivesVirtualPlayers ( t * testing . T ) {
dir := t . TempDir ( )
partyPath := filepath . Join ( dir , "roster" , "party.yml" )
if err := os . Mkdir ( filepath . Dir ( partyPath ) , 0 o755 ) ; err != nil {
t . Fatalf ( "create roster directory: %v" , err )
}
partyYAML := [ ] byte ( ` schema_version : narratio . party . v1
characters :
arannis :
player : { name : Eric }
character :
name : Arannis
classes : [ { name : wizard , level : 8 } ]
` )
if err := os . WriteFile ( partyPath , partyYAML , 0 o644 ) ; err != nil {
t . Fatalf ( "write party: %v" , err )
}
pipelinePath , campaignPath , sessionPath := writePartyResolutionConfig ( t , dir , ` campaign_id : campaign
inputs :
speakers_file : speakers . yml
autocorrect_file : autocorrect . yml
glossary_file : glossary . yml
party_file : roster / party . yml
` , ` session_id : session
campaign : campaign
inputs :
audio_dir : audio
` )
cfg , err := LoadWithSessionOptions ( pipelinePath , campaignPath , sessionPath , SessionLoadOptions { } )
if err != nil {
t . Fatalf ( "LoadWithSessionOptions() error = %v" , err )
}
if err := Validate ( cfg ) ; err != nil {
t . Fatalf ( "Validate() error = %v" , err )
}
2026-09-02 12:04:35 +00:00
if cfg . Party . Canonical == nil {
2026-08-30 14:01:51 +00:00
t . Fatalf ( "party = %#v, want canonical party" , cfg . Party )
}
if got , want := cfg . Party . Source . Path , partyPath ; got != want {
t . Fatalf ( "party source path = %q, want %q" , got , want )
}
if cfg . Party . Source . Source != "campaign_config" || cfg . Party . Source . ConfigPath != campaignPath {
t . Fatalf ( "party provenance = %#v, want campaign source" , cfg . Party . Source )
}
if ! bytes . Equal ( cfg . Party . Canonical . Raw , partyYAML ) {
t . Fatal ( "canonical party bytes were not retained" )
}
}
func TestCanonicalPartyRejectsSeparatePlayersAndSessionPartyOverrides ( t * testing . T ) {
tests := [ ] struct {
name string
campaignExtra string
sessionExtra string
want string
} {
2026-09-02 12:04:35 +00:00
{ name : "campaign players" , campaignExtra : " players_file: players.yml\n" , want : "field players_file not found" } ,
{ name : "session players" , sessionExtra : " players_file: players.yml\n" , want : "field players_file not found" } ,
{ name : "session party" , sessionExtra : " party_file: party-override.yml\n" , want : "field party_file not found" } ,
2026-08-30 14:01:51 +00:00
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
dir := t . TempDir ( )
writePartyResolutionFile ( t , filepath . Join ( dir , "party.yml" ) , canonicalPartyFixture )
pipelinePath , campaignPath , sessionPath := writePartyResolutionConfig ( t , dir , "campaign_id: campaign\ninputs:\n speakers_file: speakers.yml\n autocorrect_file: autocorrect.yml\n glossary_file: glossary.yml\n party_file: party.yml\n" + test . campaignExtra , "session_id: session\ncampaign: campaign\ninputs:\n audio_dir: audio\n" + test . sessionExtra )
_ , err := LoadWithSessionOptions ( pipelinePath , campaignPath , sessionPath , SessionLoadOptions { } )
if err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "LoadWithSessionOptions() error = %v, want %q" , err , test . want )
}
} )
}
}
2026-09-02 11:57:51 +00:00
func TestUnversionedPartyIsRejectedBeforeResolution ( t * testing . T ) {
2026-08-30 14:01:51 +00:00
dir := t . TempDir ( )
writePartyResolutionFile ( t , filepath . Join ( dir , "party.yml" ) , "legacy: campaign\n" )
pipelinePath , campaignPath , sessionPath := writePartyResolutionConfig ( t , dir , ` campaign_id : campaign
inputs :
speakers_file : speakers . yml
autocorrect_file : autocorrect . yml
glossary_file : glossary . yml
party_file : party . yml
` , ` session_id : session
campaign : campaign
inputs :
audio_dir : audio
` )
2026-09-02 11:57:51 +00:00
if _ , err := LoadWithSessionOptions ( pipelinePath , campaignPath , sessionPath , SessionLoadOptions { } ) ; err == nil || ! strings . Contains ( err . Error ( ) , "schema_version is required" ) {
t . Fatalf ( "LoadWithSessionOptions() error = %v, want schema rejection before resolution" , err )
2026-08-30 14:01:51 +00:00
}
}
func TestResolveAndLoadedCampaignShareCanonicalPartyResolution ( t * testing . T ) {
dir := t . TempDir ( )
writePartyResolutionFile ( t , filepath . Join ( dir , "party.yml" ) , canonicalPartyFixture )
pipelinePath , campaignPath , sessionPath := writePartyResolutionConfig ( t , dir , "campaign_id: campaign\ninputs:\n speakers_file: speakers.yml\n autocorrect_file: autocorrect.yml\n glossary_file: glossary.yml\n party_file: party.yml\n" , "session_id: session\ncampaign: campaign\ninputs:\n audio_dir: audio\n" )
pipeline , err := LoadPipeline ( pipelinePath )
if err != nil {
t . Fatalf ( "LoadPipeline() error = %v" , err )
}
campaign , err := LoadCampaign ( campaignPath )
if err != nil {
t . Fatalf ( "LoadCampaign() error = %v" , err )
}
session , err := LoadSession ( sessionPath )
if err != nil {
t . Fatalf ( "LoadSession() error = %v" , err )
}
loaded , err := LoadPipelineCampaign ( pipelinePath , pipeline , campaignPath , campaign )
if err != nil {
t . Fatalf ( "LoadPipelineCampaign() error = %v" , err )
}
partial , err := ResolveLoadedPipelineCampaign ( loaded , "" , nil , SessionSource { } )
2026-09-02 12:04:35 +00:00
if err != nil || partial . Party . Canonical == nil {
2026-08-30 14:01:51 +00:00
t . Fatalf ( "ResolveLoadedPipelineCampaign() = %#v, %v; want canonical partial config" , partial , err )
}
direct , err := Resolve ( pipelinePath , pipeline , campaignPath , campaign , sessionPath , session , SessionSource { Source : "session_config" , LocalPath : sessionPath } )
if err != nil {
t . Fatalf ( "Resolve() error = %v" , err )
}
2026-09-02 12:04:35 +00:00
if direct . Party . Canonical == nil {
t . Fatalf ( "Resolve() party = %#v" , direct . Party )
2026-08-30 14:01:51 +00:00
}
}
const canonicalPartyFixture = ` schema_version : narratio . party . v1
characters :
arannis :
player : { name : Eric }
character :
name : Arannis
classes : [ { name : wizard } ]
`
func writePartyResolutionConfig ( t * testing . T , dir , campaign , session string ) ( string , string , string ) {
t . Helper ( )
pipelinePath := writePartyResolutionFile ( t , filepath . Join ( dir , "pipeline.yml" ) , "workspace:\n root: " + filepath . ToSlash ( filepath . Join ( dir , "work" ) ) + "\nwhisperx:\n transcribe_url: https://example.test/transcribe\nnotification:\n mode: noop\n" )
campaignPath := writePartyResolutionFile ( t , filepath . Join ( dir , "campaign.yml" ) , campaign )
sessionPath := writePartyResolutionFile ( t , filepath . Join ( dir , "session.yml" ) , session )
return pipelinePath , campaignPath , sessionPath
}
func writePartyResolutionFile ( t * testing . T , path , contents string ) string {
t . Helper ( )
if err := os . WriteFile ( path , [ ] byte ( contents ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write %s: %v" , path , err )
}
return path
}