package config import ( "os" "path/filepath" "strings" "testing" ) func TestLoadAndValidate(t *testing.T) { tests := []struct { name string pipelineYAML string sessionYAML string wantLoadErr string wantValidate string checkDefault bool }{ { name: "valid minimal config", pipelineYAML: `workspace: root: /tmp/narratio whisperx: transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe seriatim: timeout: 30s audita: timeout: 1h analyzer: timeout: 20m notification: timeout: 15s `, sessionYAML: `session_id: 2026-05-03 inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `, checkDefault: true, }, { name: "unknown pipeline field fails", pipelineYAML: `workspace: root: /tmp/narratio bogus: true `, sessionYAML: `session_id: 2026-05-03 inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `, wantLoadErr: "pipeline file", }, { name: "unknown whisperx field fails", pipelineYAML: `workspace: root: /tmp/narratio whisperx: transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe bogus: true `, sessionYAML: `session_id: 2026-05-03 inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `, wantLoadErr: "strict decode failed", }, { name: "unknown session field fails", pipelineYAML: `workspace: root: /tmp/narratio `, sessionYAML: `session_id: 2026-05-03 inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml unknown_field: true `, wantLoadErr: "session file", }, { name: "missing required field fails", pipelineYAML: `workspace: root: /tmp/narratio whisperx: transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe `, sessionYAML: `session_id: "" inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `, wantValidate: "session config \"session.yml\" invalid: session.session_id is required", }, { name: "missing transcribe_url fails", pipelineYAML: `workspace: root: /tmp/narratio whisperx: `, sessionYAML: `session_id: 2026-05-03 inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `, wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.whisperx.transcribe_url is required", }, { name: "invalid transcribe_url fails", pipelineYAML: `workspace: root: /tmp/narratio whisperx: transcribe_url: :// `, sessionYAML: `session_id: 2026-05-03 inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `, wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.whisperx.transcribe_url must be a valid URL", }, { name: "invalid timeout fails", pipelineYAML: `workspace: root: /tmp/narratio whisperx: transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe timeout: definitely-not-a-duration `, sessionYAML: `session_id: 2026-05-03 inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `, wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.whisperx.timeout must be a valid duration", }, { name: "invalid retry_delay fails", pipelineYAML: `workspace: root: /tmp/narratio whisperx: transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe retry_delay: not-a-duration `, sessionYAML: `session_id: 2026-05-03 inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `, wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.whisperx.retry_delay must be a valid duration", }, { name: "negative retries fails", pipelineYAML: `workspace: root: /tmp/narratio whisperx: transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe retries: -1 `, sessionYAML: `session_id: 2026-05-03 inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `, wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.whisperx.retries must be >= 0", }, { name: "invalid concurrency fails", pipelineYAML: `workspace: root: /tmp/narratio whisperx: transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe concurrency: 0 `, sessionYAML: `session_id: 2026-05-03 inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `, wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.whisperx.concurrency must be > 0", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pipelinePath, sessionPath := writeConfigFiles(t, tt.pipelineYAML, tt.sessionYAML) cfg, err := Load(pipelinePath, sessionPath) if tt.wantLoadErr != "" { if err == nil { t.Fatalf("expected load error containing %q, got nil", tt.wantLoadErr) } if !strings.Contains(err.Error(), tt.wantLoadErr) { t.Fatalf("load error = %q, want to contain %q", err.Error(), tt.wantLoadErr) } if !strings.Contains(err.Error(), "strict decode failed") { t.Fatalf("load error = %q, want strict decode context", err.Error()) } return } if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.PipelinePath != pipelinePath { t.Fatalf("PipelinePath = %q, want %q", cfg.PipelinePath, pipelinePath) } if cfg.SessionPath != sessionPath { t.Fatalf("SessionPath = %q, want %q", cfg.SessionPath, sessionPath) } if tt.checkDefault { if cfg.Pipeline.WhisperX.Language != "en" { t.Fatalf("whisperx.language = %q, want %q", cfg.Pipeline.WhisperX.Language, "en") } if cfg.Pipeline.WhisperX.Timeout != "30m" { t.Fatalf("whisperx.timeout = %q, want %q", cfg.Pipeline.WhisperX.Timeout, "30m") } if cfg.Pipeline.WhisperX.RetryDelay != "2s" { t.Fatalf("whisperx.retry_delay = %q, want %q", cfg.Pipeline.WhisperX.RetryDelay, "2s") } if cfg.Pipeline.WhisperX.Retries == nil || *cfg.Pipeline.WhisperX.Retries != 3 { t.Fatalf("whisperx.retries = %v, want 3", cfg.Pipeline.WhisperX.Retries) } if cfg.Pipeline.WhisperX.Concurrency == nil || *cfg.Pipeline.WhisperX.Concurrency != 2 { t.Fatalf("whisperx.concurrency = %v, want 2", cfg.Pipeline.WhisperX.Concurrency) } } err = Validate(cfg) if tt.wantValidate != "" { if err == nil { t.Fatalf("expected validation error containing %q, got nil", tt.wantValidate) } if !strings.Contains(err.Error(), tt.wantValidate) { t.Fatalf("validation error = %q, want to contain %q", err.Error(), tt.wantValidate) } return } if err != nil { t.Fatalf("Validate() error = %v", err) } }) } } func TestValidateMissingAudioSource(t *testing.T) { retries := 3 concurrency := 2 cfg := &Config{ Pipeline: &PipelineConfig{ Workspace: WorkspaceConfig{Root: "/tmp/narratio"}, WhisperX: WhisperXConfig{ TranscribeURL: "https://transcription.ai.rakestrawhome.com/transcribe", Language: "en", Timeout: "30m", Retries: &retries, RetryDelay: "2s", Concurrency: &concurrency, }, }, Session: &SessionConfig{ SessionID: "2026-05-03", Inputs: SessionInputsConfig{ SpeakersFile: "speakers.yml", AutocorrectFile: "autocorrect.yml", GlossaryFile: "glossary.yml", }, }, } err := Validate(cfg) if err == nil { t.Fatal("expected validation error, got nil") } if !strings.Contains(err.Error(), "audio_dir or at least one audio_files") { t.Fatalf("error = %q, want audio source guidance", err.Error()) } if !strings.Contains(err.Error(), "session config") { t.Fatalf("error = %q, want session config context", err.Error()) } } func TestExamplesLoadAndValidate(t *testing.T) { pipelinePath := filepath.Join("..", "..", "examples", "pipeline.minimal.yml") sessionPath := filepath.Join("..", "..", "examples", "session.minimal.yml") cfg, err := Load(pipelinePath, sessionPath) if err != nil { t.Fatalf("Load(examples) error = %v", err) } if err := Validate(cfg); err != nil { t.Fatalf("Validate(examples) error = %v", err) } } func writeConfigFiles(t *testing.T, pipelineYAML, sessionYAML string) (string, string) { t.Helper() dir := t.TempDir() pipelinePath := filepath.Join(dir, "pipeline.yml") sessionPath := filepath.Join(dir, "session.yml") if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil { t.Fatalf("write pipeline.yml: %v", err) } if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil { t.Fatalf("write session.yml: %v", err) } return pipelinePath, sessionPath }