579 lines
16 KiB
Go
579 lines
16 KiB
Go
package config
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestScriptoriumLoadAndValidate(t *testing.T) {
|
|
legacyPreviousSource := "previous_session_" + "artifact"
|
|
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: narratio.transcript.polished
|
|
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: "legacy previous session 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.transcript.polished
|
|
required: true
|
|
previous_recap:
|
|
source: ` + legacyPreviousSource + `
|
|
artifact: session_recap
|
|
path: ""
|
|
required: false
|
|
vars:
|
|
session_id: true
|
|
output_kind: session_recap
|
|
`,
|
|
wantValidateErr: `pipeline.scriptorium.artifacts.session_recap.inputs.previous_recap.source "` + legacyPreviousSource + `" is unsupported`,
|
|
},
|
|
{
|
|
name: "canonical previous-session 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.polished
|
|
required: true
|
|
previous_recap:
|
|
source: narratio.previous_session.artifact.session_recap
|
|
required: false
|
|
vars:
|
|
session_id: true
|
|
output_kind: session_recap
|
|
`,
|
|
},
|
|
{
|
|
name: "canonical previous-session source missing artifact key fails validation",
|
|
scriptoriumYAML: `scriptorium:
|
|
binary: scriptorium
|
|
artifacts:
|
|
session_recap:
|
|
enabled: true
|
|
prompt_id: dnd.session_recap
|
|
output_path: artifacts/session_recap.md
|
|
inputs:
|
|
previous_recap:
|
|
source: narratio.previous_session.artifact.
|
|
required: false
|
|
`,
|
|
wantValidateErr: `pipeline.scriptorium.artifacts.session_recap.inputs.previous_recap.source "narratio.previous_session.artifact." must reference configured artifact key matching ^[a-z][a-z0-9_]*$`,
|
|
},
|
|
{
|
|
name: "canonical previous-session source invalid artifact key fails validation",
|
|
scriptoriumYAML: `scriptorium:
|
|
binary: scriptorium
|
|
artifacts:
|
|
session_recap:
|
|
enabled: true
|
|
prompt_id: dnd.session_recap
|
|
output_path: artifacts/session_recap.md
|
|
inputs:
|
|
previous_recap:
|
|
source: narratio.previous_session.artifact.session-recap
|
|
required: false
|
|
`,
|
|
wantValidateErr: `pipeline.scriptorium.artifacts.session_recap.inputs.previous_recap.source "narratio.previous_session.artifact.session-recap" must reference configured artifact key matching ^[a-z][a-z0-9_]*$`,
|
|
},
|
|
{
|
|
name: "canonical previous-session source unknown artifact fails validation",
|
|
scriptoriumYAML: `scriptorium:
|
|
binary: scriptorium
|
|
artifacts:
|
|
session_recap:
|
|
enabled: true
|
|
prompt_id: dnd.session_recap
|
|
output_path: artifacts/session_recap.md
|
|
inputs:
|
|
previous_recap:
|
|
source: narratio.previous_session.artifact.quest_log
|
|
required: false
|
|
`,
|
|
wantValidateErr: `pipeline.scriptorium.artifacts.session_recap.inputs.previous_recap.source "narratio.previous_session.artifact.quest_log" references unknown artifact "quest_log"`,
|
|
},
|
|
{
|
|
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.final_trimmed
|
|
required: true
|
|
`,
|
|
},
|
|
{
|
|
name: "markdown built in 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_markdown:
|
|
source: narratio.transcript.final_markdown
|
|
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: narratio.transcript.polished
|
|
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: narratio.transcript.polished
|
|
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: narratio.transcript.polished
|
|
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))
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "valid artifact dependency 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.final_trimmed
|
|
required: true
|
|
player_handout:
|
|
enabled: true
|
|
depends_on:
|
|
- session_recap
|
|
prompt_id: dnd.player_handout
|
|
output_path: artifacts/player_handout.md
|
|
inputs:
|
|
recap:
|
|
source: narratio.artifact.session_recap
|
|
required: true
|
|
`,
|
|
},
|
|
{
|
|
name: "valid dependency on disabled artifact with output path is accepted",
|
|
scriptoriumYAML: `scriptorium:
|
|
binary: scriptorium
|
|
artifacts:
|
|
session_recap:
|
|
enabled: false
|
|
output_path: artifacts/session_recap.md
|
|
player_handout:
|
|
enabled: true
|
|
depends_on:
|
|
- session_recap
|
|
prompt_id: dnd.player_handout
|
|
output_path: artifacts/player_handout.md
|
|
inputs:
|
|
recap:
|
|
source: narratio.artifact.session_recap
|
|
required: true
|
|
`,
|
|
},
|
|
{
|
|
name: "invalid artifact name fails validation",
|
|
scriptoriumYAML: `scriptorium:
|
|
binary: scriptorium
|
|
artifacts:
|
|
SessionRecap:
|
|
enabled: true
|
|
prompt_id: dnd.session_recap
|
|
output_path: artifacts/session_recap.md
|
|
`,
|
|
wantValidateErr: "pipeline.scriptorium.artifacts keys must match ^[a-z][a-z0-9_]*$",
|
|
},
|
|
{
|
|
name: "artifact output path outside artifacts root fails validation",
|
|
scriptoriumYAML: `scriptorium:
|
|
binary: scriptorium
|
|
artifacts:
|
|
session_recap:
|
|
enabled: true
|
|
prompt_id: dnd.session_recap
|
|
output_path: transcripts/session_recap.md
|
|
`,
|
|
wantValidateErr: "pipeline.scriptorium.artifacts.session_recap.output_path must be under artifacts/",
|
|
},
|
|
{
|
|
name: "missing depends_on for 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.transcript.polished
|
|
required: true
|
|
player_handout:
|
|
enabled: true
|
|
prompt_id: dnd.player_handout
|
|
output_path: artifacts/player_handout.md
|
|
inputs:
|
|
recap:
|
|
source: narratio.artifact.session_recap
|
|
required: true
|
|
`,
|
|
wantValidateErr: `pipeline.scriptorium.artifacts.player_handout.inputs.recap.source "narratio.artifact.session_recap" requires depends_on entry "session_recap"`,
|
|
},
|
|
{
|
|
name: "dependency on unknown artifact fails validation",
|
|
scriptoriumYAML: `scriptorium:
|
|
binary: scriptorium
|
|
artifacts:
|
|
player_handout:
|
|
enabled: true
|
|
depends_on:
|
|
- session_recap
|
|
prompt_id: dnd.player_handout
|
|
output_path: artifacts/player_handout.md
|
|
inputs:
|
|
recap:
|
|
source: narratio.artifact.session_recap
|
|
required: true
|
|
`,
|
|
wantValidateErr: `pipeline.scriptorium.artifacts.player_handout.depends_on[0] "session_recap" is not a configured artifact key`,
|
|
},
|
|
{
|
|
name: "self dependency fails validation",
|
|
scriptoriumYAML: `scriptorium:
|
|
binary: scriptorium
|
|
artifacts:
|
|
session_recap:
|
|
enabled: true
|
|
depends_on:
|
|
- session_recap
|
|
prompt_id: dnd.session_recap
|
|
output_path: artifacts/session_recap.md
|
|
`,
|
|
wantValidateErr: "pipeline.scriptorium.artifacts.session_recap.depends_on must not include itself",
|
|
},
|
|
{
|
|
name: "enabled dependency cycle fails validation",
|
|
scriptoriumYAML: `scriptorium:
|
|
binary: scriptorium
|
|
artifacts:
|
|
artifact_a:
|
|
enabled: true
|
|
depends_on:
|
|
- artifact_b
|
|
prompt_id: dnd.a
|
|
output_path: artifacts/a.md
|
|
inputs:
|
|
b:
|
|
source: narratio.artifact.artifact_b
|
|
required: true
|
|
artifact_b:
|
|
enabled: true
|
|
depends_on:
|
|
- artifact_a
|
|
prompt_id: dnd.b
|
|
output_path: artifacts/b.md
|
|
inputs:
|
|
a:
|
|
source: narratio.artifact.artifact_a
|
|
required: true
|
|
`,
|
|
wantValidateErr: "pipeline.scriptorium.artifacts enabled dependencies must not contain cycles",
|
|
},
|
|
{
|
|
name: "artifact source typo fails validation",
|
|
scriptoriumYAML: `scriptorium:
|
|
binary: scriptorium
|
|
artifacts:
|
|
player_handout:
|
|
enabled: true
|
|
prompt_id: dnd.player_handout
|
|
output_path: artifacts/player_handout.md
|
|
inputs:
|
|
recap:
|
|
source: narratio.artifact.session-recap
|
|
required: true
|
|
`,
|
|
wantValidateErr: `pipeline.scriptorium.artifacts.player_handout.inputs.recap.source "narratio.artifact.session-recap" is unsupported`,
|
|
},
|
|
{
|
|
name: "referenced disabled artifact missing output path fails validation",
|
|
scriptoriumYAML: `scriptorium:
|
|
binary: scriptorium
|
|
artifacts:
|
|
session_recap:
|
|
enabled: false
|
|
player_handout:
|
|
enabled: true
|
|
depends_on:
|
|
- session_recap
|
|
prompt_id: dnd.player_handout
|
|
output_path: artifacts/player_handout.md
|
|
inputs:
|
|
recap:
|
|
source: narratio.artifact.session_recap
|
|
required: true
|
|
`,
|
|
wantValidateErr: "pipeline.scriptorium.artifacts.session_recap.output_path is required when artifact is referenced",
|
|
},
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScriptoriumLegacyTranscriptSourcesRejected(t *testing.T) {
|
|
legacyTranscriptSources := []string{
|
|
"narratio.transcript." + "merged",
|
|
"narratio.transcript." + "full",
|
|
"narratio.transcript." + "trimmed",
|
|
}
|
|
|
|
for _, source := range legacyTranscriptSources {
|
|
t.Run(source, func(t *testing.T) {
|
|
pipelineYAML := testPipelineBaseYAML + `
|
|
scriptorium:
|
|
binary: scriptorium
|
|
artifacts:
|
|
session_recap:
|
|
enabled: true
|
|
prompt_id: dnd.session_recap
|
|
output_path: artifacts/session_recap.md
|
|
inputs:
|
|
transcript:
|
|
source: ` + source + `
|
|
required: true
|
|
`
|
|
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
|
|
cfg, err := Load(pipelinePath, sessionPath)
|
|
if err != nil {
|
|
t.Fatalf("Load() error = %v", err)
|
|
}
|
|
err = Validate(cfg)
|
|
wantErr := `pipeline.scriptorium.artifacts.session_recap.inputs.transcript.source "` + source + `" is unsupported`
|
|
if err == nil || !strings.Contains(err.Error(), wantErr) {
|
|
t.Fatalf("Validate() error = %v, want to contain %q", err, wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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
|
|
campaign: test-campaign
|
|
inputs:
|
|
audio_dir: ./audio
|
|
speakers_file: ./speakers.yml
|
|
autocorrect_file: ./autocorrect.yml
|
|
glossary_file: ./glossary.yml
|
|
`
|