287 lines
13 KiB
Go
287 lines
13 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestPipelineCampaignRegistryStrictDecode(t *testing.T) {
|
|
dir := t.TempDir()
|
|
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
|
pipelineYAML := `workspace:
|
|
root: /tmp/narratio-work
|
|
campaigns:
|
|
root: /srv/narratio/campaigns
|
|
default_campaign_id: dilfs
|
|
whisperx:
|
|
transcribe_url: https://example.com/transcribe
|
|
notification:
|
|
mode: noop
|
|
`
|
|
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
|
t.Fatalf("write pipeline.yml: %v", err)
|
|
}
|
|
cfg, err := LoadPipeline(pipelinePath)
|
|
if err != nil {
|
|
t.Fatalf("LoadPipeline() error = %v", err)
|
|
}
|
|
if cfg.Campaigns.Root != "/srv/narratio/campaigns" {
|
|
t.Fatalf("campaigns.root = %q", cfg.Campaigns.Root)
|
|
}
|
|
if cfg.Campaigns.DefaultCampaignID != "dilfs" {
|
|
t.Fatalf("campaigns.default_campaign_id = %q", cfg.Campaigns.DefaultCampaignID)
|
|
}
|
|
}
|
|
|
|
func TestCampaignStrictDecodeAcceptsCampaignID(t *testing.T) {
|
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
|
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
|
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
|
)
|
|
|
|
cfg, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
|
if err != nil {
|
|
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
|
}
|
|
if CampaignID(cfg.Campaign) != "sample-campaign" {
|
|
t.Fatalf("CampaignID() = %q, want sample-campaign", CampaignID(cfg.Campaign))
|
|
}
|
|
}
|
|
|
|
func TestCampaignStrictDecodeRejectsLegacyCampaignField(t *testing.T) {
|
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
|
"campaign: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
|
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
|
)
|
|
|
|
_, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
|
if err == nil {
|
|
t.Fatal("expected load error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "campaign file") || !strings.Contains(err.Error(), "strict decode failed") {
|
|
t.Fatalf("error = %q, want campaign strict decode context", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestCampaignStrictDecodeRejectsUnknownFields(t *testing.T) {
|
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
|
"campaign_id: sample-campaign\nunknown: true\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
|
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
|
)
|
|
|
|
_, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
|
if err == nil {
|
|
t.Fatal("expected load error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "campaign file") || !strings.Contains(err.Error(), "strict decode failed") {
|
|
t.Fatalf("error = %q, want campaign strict decode context", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestCampaignStrictDecodeAcceptsSessionTemplateFile(t *testing.T) {
|
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
|
"campaign_id: sample-campaign\nsession_template_file: ./session.template.yml\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
|
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
|
)
|
|
|
|
cfg, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
|
if err != nil {
|
|
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
|
}
|
|
if cfg.Campaign.SessionTemplateFile != "./session.template.yml" {
|
|
t.Fatalf("SessionTemplateFile = %q, want ./session.template.yml", cfg.Campaign.SessionTemplateFile)
|
|
}
|
|
}
|
|
|
|
func TestCampaignSessionMergeFillsStableInputs(t *testing.T) {
|
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
|
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.yml\n players_file: ./campaign-players.yml\n party_file: ./campaign-party.yml\n",
|
|
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
|
)
|
|
|
|
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)
|
|
}
|
|
if cfg.Session.Campaign != "sample-campaign" {
|
|
t.Fatalf("session campaign = %q, want campaign config value", cfg.Session.Campaign)
|
|
}
|
|
assertResolvedStableInput(t, cfg.StableInputs.SpeakersFile, "./campaign-speakers.yml", campaignPath, "campaign_config")
|
|
assertResolvedStableInput(t, cfg.StableInputs.AutocorrectFile, "./campaign-autocorrect.yml", campaignPath, "campaign_config")
|
|
assertResolvedStableInput(t, cfg.StableInputs.GlossaryFile, "./campaign-glossary.yml", campaignPath, "campaign_config")
|
|
assertResolvedStableInput(t, cfg.StableInputs.PlayersFile, "./campaign-players.yml", campaignPath, "campaign_config")
|
|
assertResolvedStableInput(t, cfg.StableInputs.PartyFile, "./campaign-party.yml", campaignPath, "campaign_config")
|
|
}
|
|
|
|
func TestCampaignSessionMergeSessionOverridesStableInputs(t *testing.T) {
|
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
|
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.yml\n players_file: ./campaign-players.yml\n party_file: ./campaign-party.yml\n",
|
|
"session_id: 2026-05-03\ncampaign: sample-campaign\ninputs:\n audio_dir: ./audio\n speakers_file: ./session-speakers.yml\n players_file: ./session-players.yml\n party_file: ./session-party.yml\n",
|
|
)
|
|
|
|
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)
|
|
}
|
|
assertResolvedStableInput(t, cfg.StableInputs.SpeakersFile, "./session-speakers.yml", sessionPath, "session_config")
|
|
assertResolvedStableInput(t, cfg.StableInputs.AutocorrectFile, "./campaign-autocorrect.yml", campaignPath, "campaign_config")
|
|
assertResolvedStableInput(t, cfg.StableInputs.GlossaryFile, "./campaign-glossary.yml", campaignPath, "campaign_config")
|
|
assertResolvedStableInput(t, cfg.StableInputs.PlayersFile, "./session-players.yml", sessionPath, "session_config")
|
|
assertResolvedStableInput(t, cfg.StableInputs.PartyFile, "./session-party.yml", sessionPath, "session_config")
|
|
}
|
|
|
|
func TestCampaignSessionMergeSpellCatalog(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
campaignValue string
|
|
sessionValue string
|
|
wantPath string
|
|
wantSource string
|
|
}{
|
|
{name: "omitted", wantPath: "", wantSource: "campaign_config"},
|
|
{name: "campaign inherited", campaignValue: "./campaign-spells.json", wantPath: "./campaign-spells.json", wantSource: "campaign_config"},
|
|
{name: "session override", campaignValue: "./campaign-spells.json", sessionValue: "./session-spells.json", wantPath: "./session-spells.json", wantSource: "session_config"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
campaignSpell := ""
|
|
if tt.campaignValue != "" {
|
|
campaignSpell = " spell_catalog_file: " + tt.campaignValue + "\n"
|
|
}
|
|
sessionSpell := ""
|
|
if tt.sessionValue != "" {
|
|
sessionSpell = " spell_catalog_file: " + tt.sessionValue + "\n"
|
|
}
|
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
|
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n"+campaignSpell,
|
|
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n"+sessionSpell,
|
|
)
|
|
|
|
cfg, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
|
if err != nil {
|
|
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
|
}
|
|
wantConfigPath := campaignPath
|
|
if tt.wantSource == "session_config" {
|
|
wantConfigPath = sessionPath
|
|
}
|
|
assertResolvedStableInput(t, cfg.StableInputs.SpellCatalogFile, tt.wantPath, wantConfigPath, tt.wantSource)
|
|
if cfg.Session.Inputs.SpellCatalogFile != tt.wantPath {
|
|
t.Fatalf("session spell_catalog_file = %q, want %q", cfg.Session.Inputs.SpellCatalogFile, tt.wantPath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCampaignSessionMergeRejectsWhitespaceSpellCatalog(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
campaignLine string
|
|
sessionLine string
|
|
wantErr string
|
|
}{
|
|
{name: "campaign", campaignLine: " spell_catalog_file: ' '\n", wantErr: "campaign.inputs.spell_catalog_file"},
|
|
{name: "session", sessionLine: " spell_catalog_file: ' '\n", wantErr: "session.inputs.spell_catalog_file"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
|
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n"+tt.campaignLine,
|
|
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n"+tt.sessionLine,
|
|
)
|
|
_, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Fatalf("LoadWithSessionOptions() error = %v, want containing %q", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCampaignRequiresPlayersAndPartyInputs(t *testing.T) {
|
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
|
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
|
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
|
)
|
|
|
|
cfg, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
|
if err != nil {
|
|
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
|
}
|
|
err = Validate(cfg)
|
|
if err == nil {
|
|
t.Fatal("expected validation error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "campaign.inputs.players_file is required") {
|
|
t.Fatalf("error = %q, want players_file required", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestCampaignSessionMismatchFails(t *testing.T) {
|
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
|
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
|
"session_id: 2026-05-03\ncampaign: other-campaign\ninputs:\n audio_dir: ./audio\n",
|
|
)
|
|
|
|
_, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
|
if err == nil {
|
|
t.Fatal("expected load error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "does not match campaign config") {
|
|
t.Fatalf("error = %q, want campaign mismatch context", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestLoadMissingCampaignFileFails(t *testing.T) {
|
|
pipelinePath, _, sessionPath := writeCampaignConfigTestFiles(t,
|
|
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
|
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
|
)
|
|
missingCampaignPath := filepath.Join(filepath.Dir(sessionPath), "missing-campaign.yml")
|
|
|
|
_, err := LoadWithSessionOptions(pipelinePath, missingCampaignPath, sessionPath, SessionLoadOptions{})
|
|
if err == nil {
|
|
t.Fatal("expected load error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "load campaign config") {
|
|
t.Fatalf("error = %q, want campaign load context", err.Error())
|
|
}
|
|
}
|
|
|
|
func writeCampaignConfigTestFiles(t *testing.T, campaignYAML, sessionYAML string) (string, string, string) {
|
|
t.Helper()
|
|
|
|
dir := t.TempDir()
|
|
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
|
campaignPath := filepath.Join(dir, "campaign.yml")
|
|
sessionPath := filepath.Join(dir, "session.yml")
|
|
|
|
pipelineYAML := "workspace:\n root: " + filepath.ToSlash(filepath.Join(dir, "work")) + "\nwhisperx:\n transcribe_url: https://example.com/transcribe\nnotification:\n mode: noop\n"
|
|
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
|
t.Fatalf("write pipeline.yml: %v", err)
|
|
}
|
|
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
|
t.Fatalf("write campaign.yml: %v", err)
|
|
}
|
|
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
|
t.Fatalf("write session.yml: %v", err)
|
|
}
|
|
|
|
return pipelinePath, campaignPath, sessionPath
|
|
}
|
|
|
|
func assertResolvedStableInput(t *testing.T, got ResolvedInputFile, wantPath, wantConfigPath, wantSource string) {
|
|
t.Helper()
|
|
if got.Path != wantPath || got.ConfigPath != wantConfigPath || got.Source != wantSource {
|
|
t.Fatalf("resolved input = %#v, want path=%q config_path=%q source=%q", got, wantPath, wantConfigPath, wantSource)
|
|
}
|
|
}
|