package config import ( "strings" "testing" ) func TestScriptoriumLoadAndValidate(t *testing.T) { tests := []struct { name string scriptoriumYAML string wantLoadErr string wantValidateErr string assert func(t *testing.T, cfg *Config) }{ { name: "valid minimal scriptorium config", scriptoriumYAML: `scriptorium: binary: scriptorium artifacts: session_recap: enabled: true prompt_id: dnd.session_recap output_path: artifacts/session_recap.md inputs: transcript: source: processed_transcript required: true vars: session_id: true output_kind: session_recap `, assert: func(t *testing.T, cfg *Config) { t.Helper() if cfg.Pipeline.Scriptorium == nil { t.Fatal("scriptorium config should be present") } if cfg.Pipeline.Scriptorium.Timeout != "10m" { t.Fatalf("scriptorium.timeout default = %q, want 10m", cfg.Pipeline.Scriptorium.Timeout) } }, }, { name: "unknown scriptorium field fails", scriptoriumYAML: `scriptorium: binary: scriptorium bogus: true `, wantLoadErr: "strict decode failed", }, { name: "missing binary defaults when section present", scriptoriumYAML: `scriptorium: timeout: 10m `, assert: func(t *testing.T, cfg *Config) { t.Helper() if cfg.Pipeline.Scriptorium == nil { t.Fatal("scriptorium config should be present") } if cfg.Pipeline.Scriptorium.Binary != "scriptorium" { t.Fatalf("scriptorium.binary = %q, want scriptorium", cfg.Pipeline.Scriptorium.Binary) } }, }, { name: "enabled artifact missing prompt id fails", scriptoriumYAML: `scriptorium: binary: scriptorium artifacts: session_recap: enabled: true output_path: artifacts/session_recap.md `, wantValidateErr: "pipeline.scriptorium.artifacts.session_recap.prompt_id is required when enabled", }, { name: "enabled artifact missing output path fails", scriptoriumYAML: `scriptorium: binary: scriptorium artifacts: session_recap: enabled: true prompt_id: dnd.session_recap `, wantValidateErr: "pipeline.scriptorium.artifacts.session_recap.output_path is required when enabled", }, { name: "invalid timeout fails", scriptoriumYAML: `scriptorium: binary: scriptorium timeout: definitely-not-a-duration `, wantValidateErr: "pipeline.scriptorium.timeout must be a valid duration", }, { name: "optional previous recap input is accepted", scriptoriumYAML: `scriptorium: binary: scriptorium artifacts: session_recap: enabled: true prompt_id: dnd.session_recap output_path: artifacts/session_recap.md inputs: transcript: source: processed_transcript required: true previous_recap: source: previous_session_artifact artifact: session_recap path: "" required: false vars: session_id: true output_kind: session_recap `, }, { name: "canonical artifact source is accepted", scriptoriumYAML: `scriptorium: binary: scriptorium artifacts: session_recap: enabled: true prompt_id: dnd.session_recap output_path: artifacts/session_recap.md inputs: transcript: source: narratio.transcript.trimmed required: true `, }, { name: "unknown artifact source fails validation", scriptoriumYAML: `scriptorium: binary: scriptorium artifacts: session_recap: enabled: true prompt_id: dnd.session_recap output_path: artifacts/session_recap.md inputs: transcript: source: narratio.unknown required: true `, wantValidateErr: `pipeline.scriptorium.artifacts.session_recap.inputs.transcript.source "narratio.unknown" is unsupported`, }, { name: "artifact render_debug override is accepted", scriptoriumYAML: `scriptorium: binary: scriptorium render_debug: false artifacts: session_recap: enabled: true render_debug: true prompt_id: dnd.session_recap output_path: artifacts/session_recap.md inputs: transcript: source: processed_transcript required: true `, assert: func(t *testing.T, cfg *Config) { t.Helper() artifact := cfg.Pipeline.Scriptorium.Artifacts["session_recap"] if artifact.RenderDebug == nil || *artifact.RenderDebug != true { t.Fatalf("artifact render_debug = %#v, want true", artifact.RenderDebug) } }, }, { name: "multiple artifact definitions can be decoded", scriptoriumYAML: `scriptorium: binary: scriptorium artifacts: session_recap: enabled: true prompt_id: dnd.session_recap output_path: artifacts/session_recap.md inputs: transcript: source: processed_transcript required: true player_summary: enabled: true prompt_id: dnd.player_summary profile_id: local-fast output_path: artifacts/player_summary.md timeout: 3m inputs: transcript: source: processed_transcript required: true `, assert: func(t *testing.T, cfg *Config) { t.Helper() if cfg.Pipeline.Scriptorium == nil { t.Fatal("scriptorium config should be present") } if len(cfg.Pipeline.Scriptorium.Artifacts) != 2 { t.Fatalf("scriptorium.artifacts count = %d, want 2", len(cfg.Pipeline.Scriptorium.Artifacts)) } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pipelineYAML := testPipelineBaseYAML + "\n" + tt.scriptoriumYAML pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML) 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) } return } if err != nil { t.Fatalf("Load() error = %v", err) } if tt.assert != nil { tt.assert(t, cfg) } err = Validate(cfg) if tt.wantValidateErr != "" { if err == nil { t.Fatalf("expected validation error containing %q, got nil", tt.wantValidateErr) } if !strings.Contains(err.Error(), tt.wantValidateErr) { t.Fatalf("validation error = %q, want to contain %q", err.Error(), tt.wantValidateErr) } return } if err != nil { t.Fatalf("Validate() error = %v", err) } }) } } const testPipelineBaseYAML = `workspace: root: /tmp/narratio whisperx: transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe seriatim: binary: seriatim audita: binary: audita ` const testSessionBaseYAML = `session_id: 2026-05-03 inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `