2026-05-02 10:43:45 -05:00
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
2026-05-03 15:23:47 -05:00
checkDefault bool
2026-05-19 07:07:19 -05:00
wantRoot string
2026-05-02 10:43:45 -05:00
} {
{
name : "valid minimal config" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
2026-05-03 15:23:47 -05:00
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
2026-05-02 10:43:45 -05:00
seriatim :
2026-05-03 19:13:50 -05:00
binary : seriatim
2026-05-02 10:43:45 -05:00
audita :
2026-05-04 07:48:59 -05:00
binary : audita
2026-05-02 10:43:45 -05:00
notification :
timeout : 15 s
` ,
sessionYAML : ` session_id : 2026 - 05 - 03
inputs :
audio_dir : . / audio
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
2026-05-16 23:30:40 +00:00
` ,
checkDefault : true ,
2026-05-19 07:07:19 -05:00
wantRoot : "/tmp/narratio" ,
2026-05-16 23:30:40 +00:00
} ,
{
name : "seriatim and audita sections can be omitted" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
notification :
timeout : 15 s
` ,
sessionYAML : ` session_id : 2026 - 05 - 03
inputs :
audio_dir : . / audio
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
2026-05-02 10:43:45 -05:00
` ,
2026-05-03 15:23:47 -05:00
checkDefault : true ,
2026-05-19 07:07:19 -05:00
wantRoot : "/tmp/narratio" ,
} ,
{
name : "workspace root defaults when omitted" ,
pipelineYAML : ` whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
notification :
timeout : 15 s
` ,
sessionYAML : ` session_id : 2026 - 05 - 03
inputs :
audio_dir : . / audio
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
` ,
checkDefault : true ,
wantRoot : DefaultWorkspaceRoot ,
2026-05-02 10:43:45 -05:00
} ,
{
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
` ,
2026-05-02 11:44:15 -05:00
wantLoadErr : "pipeline file" ,
2026-05-02 10:43:45 -05:00
} ,
2026-05-22 18:32:14 -05:00
{
name : "legacy analyzer section fails strict decode" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
analyzer :
timeout : 20 m
` ,
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" ,
} ,
2026-05-03 15:23:47 -05:00
{
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" ,
} ,
2026-05-16 07:56:22 -05:00
{
name : "unknown secrets field fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
secrets :
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 : "empty secrets env_dir fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
secrets :
env_dir : " "
` ,
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.secrets.env_dir must be non-empty when pipeline.secrets is configured" ,
} ,
2026-05-02 10:43:45 -05:00
{
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
` ,
2026-05-02 11:44:15 -05:00
wantLoadErr : "session file" ,
2026-05-02 10:43:45 -05:00
} ,
{
name : "missing required field fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
2026-05-03 15:23:47 -05:00
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
2026-05-03 19:13:50 -05:00
seriatim :
binary : seriatim
2026-05-02 10:43:45 -05:00
` ,
sessionYAML : ` session_id : ""
inputs :
audio_dir : . / audio
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
` ,
2026-05-02 11:44:15 -05:00
wantValidate : "session config \"session.yml\" invalid: session.session_id is required" ,
2026-05-02 10:43:45 -05:00
} ,
2026-05-20 14:26:32 +00:00
{
name : "valid previous_session_id passes" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
` ,
sessionYAML : ` session_id : 2026 - 05 - 03
previous_session_id : 2026 - 04 - 26
inputs :
audio_dir : . / audio
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
` ,
} ,
{
name : "previous_session_id equal to session_id fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
` ,
sessionYAML : ` session_id : 2026 - 05 - 03
previous_session_id : 2026 - 05 - 03
inputs :
audio_dir : . / audio
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
` ,
wantValidate : "session config \"session.yml\" invalid: session.previous_session_id must not equal session.session_id" ,
} ,
2026-05-03 15:23:47 -05:00
{
name : "missing transcribe_url fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
2026-05-03 19:13:50 -05:00
seriatim :
binary : seriatim
2026-05-03 15:23:47 -05:00
` ,
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 : : //
2026-05-03 19:13:50 -05:00
seriatim :
binary : seriatim
2026-05-03 15:23:47 -05:00
` ,
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" ,
} ,
2026-05-02 10:43:45 -05:00
{
name : "invalid timeout fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
2026-05-03 15:23:47 -05:00
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
2026-05-02 10:43:45 -05:00
timeout : definitely - not - a - duration
2026-05-03 19:13:50 -05:00
seriatim :
binary : seriatim
2026-05-02 10:43:45 -05:00
` ,
sessionYAML : ` session_id : 2026 - 05 - 03
inputs :
audio_dir : . / audio
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
` ,
2026-05-02 11:44:15 -05:00
wantValidate : "pipeline config \"pipeline.yml\" invalid: pipeline.whisperx.timeout must be a valid duration" ,
2026-05-02 10:43:45 -05:00
} ,
2026-05-03 15:23:47 -05:00
{
name : "invalid retry_delay fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
retry_delay : not - a - duration
2026-05-03 19:13:50 -05:00
seriatim :
binary : seriatim
2026-05-03 15:23:47 -05:00
` ,
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
2026-05-03 19:13:50 -05:00
seriatim :
binary : seriatim
2026-05-03 15:23:47 -05:00
` ,
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
2026-05-03 19:13:50 -05:00
seriatim :
binary : seriatim
2026-05-03 15:23:47 -05:00
` ,
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" ,
} ,
2026-05-03 19:13:50 -05:00
{
name : "unknown seriatim field fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
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 seriatim env field fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
env :
unknown_knob : 1.0
` ,
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" ,
} ,
{
2026-05-16 23:30:40 +00:00
name : "missing seriatim binary uses default" ,
2026-05-03 19:13:50 -05:00
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
timeout : 10 m
` ,
sessionYAML : ` session_id : 2026 - 05 - 03
inputs :
audio_dir : . / audio
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
` ,
} ,
{
name : "invalid seriatim timeout fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
timeout : 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.seriatim.timeout must be a valid duration" ,
} ,
{
name : "invalid seriatim output schema fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
output_schema : invalid - schema
` ,
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.seriatim.output_schema must be one of: seriatim-minimal, seriatim-intermediate, seriatim-full" ,
} ,
{
name : "negative seriatim coalesce gap fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
coalesce_gap : - 0.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.seriatim.coalesce_gap must be >= 0" ,
} ,
{
name : "invalid seriatim env float fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
env :
overlap_word_run_gap : 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.seriatim.env.overlap_word_run_gap must be > 0 when provided" ,
} ,
2026-05-04 07:48:59 -05:00
{
name : "unknown audita field fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
audita :
binary : audita
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" ,
} ,
{
2026-05-16 23:30:40 +00:00
name : "missing audita binary uses default" ,
2026-05-04 07:48:59 -05:00
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
audita :
timeout : 3 h
` ,
sessionYAML : ` session_id : 2026 - 05 - 03
inputs :
audio_dir : . / audio
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
` ,
} ,
{
name : "invalid audita timeout fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
audita :
binary : audita
timeout : bad - timeout
` ,
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.audita.timeout must be a valid duration" ,
} ,
{
2026-05-16 08:46:48 -05:00
name : "empty audita modules is valid override" ,
2026-05-04 07:48:59 -05:00
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
audita :
binary : audita
modules : [ ]
` ,
sessionYAML : ` session_id : 2026 - 05 - 03
inputs :
audio_dir : . / audio
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
` ,
} ,
{
name : "empty audita module item fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
audita :
binary : audita
modules :
- glossary
- ""
` ,
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.audita.modules[1] must be non-empty" ,
} ,
{
name : "invalid audita module item fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
audita :
binary : audita
modules :
- glossary
- bad_module
` ,
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.audita.modules[1] must be one of: glossary, homophones, spoken_word, grammar" ,
} ,
{
name : "invalid audita base_url fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
audita :
binary : audita
base_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.audita.base_url must be a valid URL" ,
} ,
{
2026-05-16 08:46:48 -05:00
name : "legacy audita llm_concurrency field fails strict decode" ,
2026-05-04 07:48:59 -05:00
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
audita :
binary : audita
2026-05-16 08:46:48 -05:00
llm_concurrency : 1
2026-05-04 07:48:59 -05:00
` ,
sessionYAML : ` session_id : 2026 - 05 - 03
inputs :
audio_dir : . / audio
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
` ,
2026-05-16 08:46:48 -05:00
wantLoadErr : "strict decode failed" ,
} ,
{
name : "invalid audita total_llm_concurrency fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
audita :
binary : audita
total_llm_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.audita.total_llm_concurrency must be > 0" ,
} ,
{
name : "invalid audita proposal_llm_concurrency fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
audita :
binary : audita
proposal_llm_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.audita.proposal_llm_concurrency must be > 0" ,
2026-05-04 07:48:59 -05:00
} ,
{
name : "invalid audita validation_llm_concurrency fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
audita :
binary : audita
validation_llm_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.audita.validation_llm_concurrency must be > 0" ,
} ,
2026-05-16 08:46:48 -05:00
{
name : "invalid audita output_schema fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
audita :
binary : audita
output_schema : bad
` ,
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.audita.output_schema must be one of: bare-segments, audita-v1" ,
} ,
{
name : "invalid audita work_dir_retention fails" ,
pipelineYAML : ` workspace :
root : / tmp / narratio
whisperx :
transcribe_url : https : //transcription.ai.rakestrawhome.com/transcribe
seriatim :
binary : seriatim
audita :
binary : audita
work_dir_retention : sometimes
` ,
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.audita.work_dir_retention must be one of: always, auto, never" ,
} ,
2026-05-02 10:43:45 -05:00
}
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 )
}
2026-05-02 11:44:15 -05:00
if ! strings . Contains ( err . Error ( ) , "strict decode failed" ) {
t . Fatalf ( "load error = %q, want strict decode context" , err . Error ( ) )
}
2026-05-02 10:43:45 -05:00
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 )
}
2026-05-03 15:23:47 -05:00
if tt . checkDefault {
2026-05-19 07:07:19 -05:00
if tt . wantRoot != "" && cfg . Pipeline . Workspace . Root != tt . wantRoot {
t . Fatalf ( "workspace.root = %q, want %q" , cfg . Pipeline . Workspace . Root , tt . wantRoot )
}
2026-05-03 15:23:47 -05:00
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 )
}
2026-05-03 19:13:50 -05:00
if cfg . Pipeline . Seriatim . Timeout != "10m" {
t . Fatalf ( "seriatim.timeout = %q, want %q" , cfg . Pipeline . Seriatim . Timeout , "10m" )
}
2026-05-16 23:30:40 +00:00
if cfg . Pipeline . Seriatim . Binary != "seriatim" {
t . Fatalf ( "seriatim.binary = %q, want %q" , cfg . Pipeline . Seriatim . Binary , "seriatim" )
}
2026-05-03 19:13:50 -05:00
if cfg . Pipeline . Seriatim . OutputSchema != "seriatim-intermediate" {
t . Fatalf ( "seriatim.output_schema = %q, want %q" , cfg . Pipeline . Seriatim . OutputSchema , "seriatim-intermediate" )
}
if cfg . Pipeline . Seriatim . CoalesceGap == nil || * cfg . Pipeline . Seriatim . CoalesceGap != 3.0 {
t . Fatalf ( "seriatim.coalesce_gap = %v, want 3.0" , cfg . Pipeline . Seriatim . CoalesceGap )
}
if cfg . Pipeline . Seriatim . Report == nil || * cfg . Pipeline . Seriatim . Report != true {
t . Fatalf ( "seriatim.report = %v, want true" , cfg . Pipeline . Seriatim . Report )
}
2026-05-04 07:48:59 -05:00
if cfg . Pipeline . Audita . Timeout != "3h" {
t . Fatalf ( "audita.timeout = %q, want %q" , cfg . Pipeline . Audita . Timeout , "3h" )
}
2026-05-16 23:30:40 +00:00
if cfg . Pipeline . Audita . Binary != "audita" {
t . Fatalf ( "audita.binary = %q, want %q" , cfg . Pipeline . Audita . Binary , "audita" )
}
2026-05-04 11:46:57 -05:00
if cfg . Pipeline . Audita . LLMAPIKeyEnv != "" {
t . Fatalf ( "audita.llm_api_key_env = %q, want empty by default" , cfg . Pipeline . Audita . LLMAPIKeyEnv )
2026-05-04 07:48:59 -05:00
}
2026-05-16 08:46:48 -05:00
if cfg . Pipeline . Audita . Modules != nil {
t . Fatalf ( "audita.modules = %#v, want nil default (optional override)" , cfg . Pipeline . Audita . Modules )
2026-05-04 07:48:59 -05:00
}
2026-05-16 23:30:40 +00:00
if cfg . Pipeline . Audita . BaseURL != "" {
t . Fatalf ( "audita.base_url = %q, want empty default" , cfg . Pipeline . Audita . BaseURL )
2026-05-04 07:48:59 -05:00
}
2026-05-16 23:30:40 +00:00
if cfg . Pipeline . Audita . Model != "" {
t . Fatalf ( "audita.model = %q, want empty default" , cfg . Pipeline . Audita . Model )
2026-05-04 07:48:59 -05:00
}
if cfg . Pipeline . Audita . ValidationModel != "" {
t . Fatalf ( "audita.validation_model = %q, want empty default" , cfg . Pipeline . Audita . ValidationModel )
}
2026-05-16 08:46:48 -05:00
if cfg . Pipeline . Audita . TotalLLMConcurrency != nil {
t . Fatalf ( "audita.total_llm_concurrency = %v, want nil default" , cfg . Pipeline . Audita . TotalLLMConcurrency )
}
if cfg . Pipeline . Audita . ProposalLLMConcurrency != nil {
t . Fatalf ( "audita.proposal_llm_concurrency = %v, want nil default" , cfg . Pipeline . Audita . ProposalLLMConcurrency )
}
if cfg . Pipeline . Audita . ValidationLLMConcurrency != nil {
t . Fatalf ( "audita.validation_llm_concurrency = %v, want nil default" , cfg . Pipeline . Audita . ValidationLLMConcurrency )
2026-05-04 07:48:59 -05:00
}
if cfg . Pipeline . Audita . Report == nil || * cfg . Pipeline . Audita . Report != true {
t . Fatalf ( "audita.report = %v, want true" , cfg . Pipeline . Audita . Report )
}
2026-05-10 21:30:35 +00:00
if cfg . Pipeline . Normalize == nil {
t . Fatal ( "normalize config should be present via defaults" )
}
2026-05-22 19:05:23 -05:00
if cfg . Pipeline . Normalize . OutputPath != "transcripts/final.json" {
t . Fatalf ( "normalize.output_path = %q, want %q" , cfg . Pipeline . Normalize . OutputPath , "transcripts/final.json" )
2026-05-10 21:30:35 +00:00
}
if cfg . Pipeline . Normalize . OutputSchema != "seriatim-intermediate" {
t . Fatalf ( "normalize.output_schema = %q, want %q" , cfg . Pipeline . Normalize . OutputSchema , "seriatim-intermediate" )
}
if cfg . Pipeline . Normalize . Report == nil || * cfg . Pipeline . Normalize . Report != true {
t . Fatalf ( "normalize.report = %v, want true" , cfg . Pipeline . Normalize . Report )
}
2026-05-03 15:23:47 -05:00
}
2026-05-02 10:43:45 -05:00
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 ) {
2026-05-03 15:23:47 -05:00
retries := 3
concurrency := 2
2026-05-02 10:43:45 -05:00
cfg := & Config {
2026-05-03 15:23:47 -05:00
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 ,
} ,
2026-05-03 19:13:50 -05:00
Seriatim : SeriatimConfig {
Binary : "seriatim" ,
Timeout : "10m" ,
OutputSchema : "seriatim-intermediate" ,
CoalesceGap : float64Ptr ( 3.0 ) ,
Report : boolPtr ( true ) ,
} ,
2026-05-04 07:48:59 -05:00
Audita : AuditaConfig {
Binary : "audita" ,
Timeout : "3h" ,
LLMAPIKeyEnv : "AUDITA_LLM_API_KEY" ,
Modules : [ ] string { "glossary" , "homophones" } ,
BaseURL : "https://openrouter.ai/api/v1" ,
Model : "openrouter/google/gemma-4-31b-it" ,
2026-05-16 08:46:48 -05:00
TotalLLMConcurrency : intPtr ( 1 ) ,
ProposalLLMConcurrency : intPtr ( 1 ) ,
2026-05-04 07:48:59 -05:00
ValidationModel : "" ,
ValidationLLMConcurrency : intPtr ( 1 ) ,
Report : boolPtr ( true ) ,
} ,
2026-05-03 15:23:47 -05:00
} ,
2026-05-22 23:01:27 -05:00
Campaign : & CampaignConfig { CampaignID : "sample-campaign" } ,
2026-05-02 10:43:45 -05:00
Session : & SessionConfig {
SessionID : "2026-05-03" ,
2026-05-16 14:11:59 +00:00
Campaign : "sample-campaign" ,
2026-05-02 10:43:45 -05:00
Inputs : SessionInputsConfig {
SpeakersFile : "speakers.yml" ,
AutocorrectFile : "autocorrect.yml" ,
GlossaryFile : "glossary.yml" ,
} ,
} ,
}
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-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-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-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
`
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 )
}
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"
}