Files
narratio/internal/config/storage_publish_test.go
Eric Rakestraw 442469b9f5
All checks were successful
ci/woodpecker/push/verify Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
Fix derived publish destinations with explicit requiredness
2026-09-21 03:06:13 +00:00

764 lines
22 KiB
Go

package config
import (
"strings"
"testing"
"gopkg.in/yaml.v3"
)
func TestStorageS3DefaultsAndValidation(t *testing.T) {
pipelineYAML := testPipelineBaseYAML + `
storage:
backend: s3
s3:
bucket: my-dnd-archive
`
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
cfg, err := Load(pipelinePath, sessionPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.Pipeline.Storage.S3 == nil {
t.Fatal("storage.s3 should be initialized")
}
if cfg.Pipeline.Storage.S3.RootPrefix != "dnd" {
t.Fatalf("storage.s3.root_prefix = %q, want dnd", cfg.Pipeline.Storage.S3.RootPrefix)
}
if cfg.Pipeline.Storage.S3.AccessKeyID != nil || cfg.Pipeline.Storage.S3.SecretAccessKey != nil {
t.Fatal("storage.s3 credentials should be optional without implicit references")
}
if cfg.Pipeline.Storage.S3.ForcePathStyle {
t.Fatalf("storage.s3.force_path_style = true, want false default")
}
if err := Validate(cfg); err != nil {
t.Fatalf("Validate() error = %v", err)
}
}
func TestStorageLegacyTopLevelFieldsFailStrictDecode(t *testing.T) {
tests := []struct {
name string
storageYAML string
wantField string
}{
{
name: "bucket",
storageYAML: `
storage:
bucket: my-dnd-archive
`,
wantField: "bucket",
},
{
name: "prefix",
storageYAML: `
storage:
prefix: dnd
`,
wantField: "prefix",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pipelineYAML := testPipelineBaseYAML + tt.storageYAML
pipelinePath, _ := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
_, err := LoadPipeline(pipelinePath)
if err == nil {
t.Fatal("LoadPipeline() error = nil, want strict decode error")
}
if !strings.Contains(err.Error(), "strict decode failed") {
t.Fatalf("LoadPipeline() error = %v, want strict decode failed", err)
}
if !strings.Contains(err.Error(), tt.wantField) {
t.Fatalf("LoadPipeline() error = %v, want field %q", err, tt.wantField)
}
})
}
}
func TestLegacyCredentialReferenceFieldsFailStrictDecode(t *testing.T) {
for _, field := range []string{"access_key_id_env", "secret_access_key_env", "llm_api_key_env"} {
t.Run(field, func(t *testing.T) {
pipelineYAML := strings.Replace(testPipelineBaseYAML, "audita:\n binary: audita\n", "audita:\n binary: audita\n "+field+": LEGACY_SECRET\n", 1)
if strings.HasPrefix(field, "access_") || strings.HasPrefix(field, "secret_access_") {
pipelineYAML = testPipelineBaseYAML + "storage:\n backend: s3\n s3:\n bucket: test-bucket\n " + field + ": LEGACY_SECRET\n"
}
pipelinePath, _ := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
_, err := LoadPipeline(pipelinePath)
if err == nil || !strings.Contains(err.Error(), field) || !strings.Contains(err.Error(), "strict decode") {
t.Fatalf("LoadPipeline() error = %v, want strict rejection of %s", err, field)
}
})
}
}
func TestStorageS3CredentialEnvNamesLoadAndValidate(t *testing.T) {
t.Setenv("CUSTOM_KEY_ID", "synthetic-access-key")
t.Setenv("CUSTOM_SECRET", "synthetic-secret-key")
pipelineYAML := testPipelineBaseYAML + `
storage:
backend: s3
s3:
bucket: my-dnd-archive
access_key_id: CUSTOM_KEY_ID
secret_access_key: CUSTOM_SECRET
`
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
cfg, err := Load(pipelinePath, sessionPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.Pipeline.Storage.S3.AccessKeyID == nil || cfg.Pipeline.Storage.S3.AccessKeyID.EnvName() != "CUSTOM_KEY_ID" || cfg.Pipeline.Storage.S3.AccessKeyID.Value() != "synthetic-access-key" {
t.Fatal("storage.s3.access_key_id did not retain its resolved typed reference")
}
if cfg.Pipeline.Storage.S3.SecretAccessKey == nil || cfg.Pipeline.Storage.S3.SecretAccessKey.EnvName() != "CUSTOM_SECRET" || cfg.Pipeline.Storage.S3.SecretAccessKey.Value() != "synthetic-secret-key" {
t.Fatal("storage.s3.secret_access_key did not retain its resolved typed reference")
}
if err := Validate(cfg); err != nil {
t.Fatalf("Validate() error = %v", err)
}
}
func TestStorageS3CredentialReferenceValidation(t *testing.T) {
tests := []struct {
name string
pipelineYML string
wantErr string
}{
{
name: "invalid access key reference",
pipelineYML: testPipelineBaseYAML + `
storage:
backend: s3
s3:
bucket: my-dnd-archive
access_key_id: "123BAD"
secret_access_key: CUSTOM_SECRET
`,
wantErr: "secret.invalid_name",
},
{
name: "partial pair",
pipelineYML: testPipelineBaseYAML + `
storage:
backend: s3
s3:
bucket: my-dnd-archive
access_key_id: CUSTOM_KEY_ID
`,
wantErr: "must be configured together",
},
{
name: "missing effective reference",
pipelineYML: testPipelineBaseYAML + `
storage:
backend: s3
s3:
bucket: my-dnd-archive
access_key_id: MISSING_CUSTOM_KEY_ID
secret_access_key: MISSING_CUSTOM_SECRET
`,
wantErr: "secret.unset",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pipelinePath, _ := writeConfigFiles(t, tt.pipelineYML, testSessionBaseYAML)
t.Setenv("CUSTOM_KEY_ID", "synthetic-access-key")
t.Setenv("CUSTOM_SECRET", "synthetic-secret-key")
cfg, err := LoadPipeline(pipelinePath)
if err == nil {
err = ValidatePipelineConfig(cfg)
}
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("Validate() error = %v, want to contain %q", err, tt.wantErr)
}
})
}
}
func TestStorageBackendSelectionValidation(t *testing.T) {
tests := []struct {
name string
storageYAML string
sessionYAML string
wantErr string
wantBackend string
}{
{
name: "omitted storage defaults local",
wantBackend: StorageBackendLocal,
},
{
name: "explicit local backend",
storageYAML: `storage:
backend: local
`,
wantBackend: StorageBackendLocal,
},
{
name: "case insensitive s3 backend normalizes",
storageYAML: `storage:
backend: S3
s3:
bucket: my-dnd-archive
`,
wantBackend: StorageBackendS3,
},
{
name: "unknown backend",
storageYAML: `storage:
backend: s33
`,
wantErr: "pipeline.storage.backend must be one of: local, s3",
},
{
name: "s3 block requires s3 backend",
storageYAML: `storage:
backend: local
s3:
bucket: my-dnd-archive
`,
wantErr: "pipeline.storage.s3 is only supported when pipeline.storage.backend is s3",
},
{
name: "s3 backend requires s3 block",
storageYAML: `storage:
backend: s3
`,
wantErr: "pipeline.storage.s3 is required when pipeline.storage.backend is s3",
},
{
name: "s3 backend requires bucket for publish",
storageYAML: `storage:
backend: s3
s3: {}
`,
wantErr: "pipeline.storage.s3.bucket is required when S3 session audio or publish upload is enabled",
},
{
name: "s3 audio requires s3 backend",
sessionYAML: `session_id: 2026-05-03
inputs:
audio_s3:
prefix: audio/
`,
wantErr: "pipeline.storage.backend must be s3 when session.inputs.audio_s3 is configured",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pipelineYAML := testPipelineBaseYAML
if tt.storageYAML != "" {
pipelineYAML += "\n" + tt.storageYAML
}
sessionYAML := testSessionBaseYAML
if tt.sessionYAML != "" {
sessionYAML = tt.sessionYAML
}
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, sessionYAML)
cfg, err := Load(pipelinePath, sessionPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
err = Validate(cfg)
if tt.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("Validate() error = %v, want %q", err, tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("Validate() error = %v", err)
}
if cfg.Pipeline.Storage.Backend != tt.wantBackend {
t.Fatalf("storage.backend = %q, want %q", cfg.Pipeline.Storage.Backend, tt.wantBackend)
}
})
}
}
func TestSpoolAndPublishDefaults(t *testing.T) {
pipelinePath, sessionPath := writeConfigFiles(t, testPipelineBaseYAML, testSessionBaseYAML)
cfg, err := Load(pipelinePath, sessionPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.Pipeline.Spool.Root != "/var/spool/narratio" {
t.Fatalf("spool.root = %q, want /var/spool/narratio", cfg.Pipeline.Spool.Root)
}
if cfg.Pipeline.Spool.DeleteAudioAfterPublish {
t.Fatalf("spool.delete_audio_after_publish = true, want false")
}
if cfg.Pipeline.Workspace.CleanupAfterPublish {
t.Fatalf("workspace.cleanup_after_publish = true, want false")
}
if cfg.Pipeline.Publish == nil {
t.Fatal("publish should be initialized by defaults")
}
if cfg.Pipeline.Publish.Enabled == nil || !*cfg.Pipeline.Publish.Enabled {
t.Fatalf("publish.enabled = %#v, want true", cfg.Pipeline.Publish.Enabled)
}
if cfg.Pipeline.Publish.UploadRun == nil || !*cfg.Pipeline.Publish.UploadRun {
t.Fatalf("publish.upload_run = %#v, want true", cfg.Pipeline.Publish.UploadRun)
}
if len(cfg.Pipeline.Publish.Outputs) != 3 {
t.Fatalf("publish.outputs len = %d, want 3 defaults", len(cfg.Pipeline.Publish.Outputs))
}
wantBySource := map[string]string{
"narratio.transcript.final_trimmed": "transcripts/final.trimmed.json",
"narratio.transcript.final_markdown": "transcripts/final.md",
"narratio.transcript.final_trimmed_markdown": "transcripts/final.trimmed.md",
}
for i, item := range cfg.Pipeline.Publish.Outputs {
if item.Required == nil || !*item.Required {
t.Fatalf("publish.outputs[%d].required = %#v, want true", i, item.Required)
}
wantDest, ok := wantBySource[item.Source]
if !ok {
t.Fatalf("publish.outputs[%d].source = %q, want known default source", i, item.Source)
}
if item.Dest != wantDest {
t.Fatalf("publish.outputs[%d].dest = %q, want %q", i, item.Dest, wantDest)
}
delete(wantBySource, item.Source)
}
if len(wantBySource) != 0 {
t.Fatalf("missing default publish outputs for sources: %#v", wantBySource)
}
}
func TestCleanupPoliciesRequireEnabledRunPublication(t *testing.T) {
tests := []struct {
name string
spoolCleanup bool
workspaceCleanup bool
publishEnabled bool
uploadRun bool
wantError bool
}{
{name: "spool cleanup rejects disabled publish", spoolCleanup: true, uploadRun: true, wantError: true},
{name: "workspace cleanup rejects disabled publish", workspaceCleanup: true, uploadRun: true, wantError: true},
{name: "spool cleanup rejects disabled run upload", spoolCleanup: true, publishEnabled: true, wantError: true},
{name: "workspace cleanup rejects disabled run upload", workspaceCleanup: true, publishEnabled: true, wantError: true},
{name: "spool cleanup accepts enabled run publication", spoolCleanup: true, publishEnabled: true, uploadRun: true},
{name: "workspace cleanup accepts enabled run publication", workspaceCleanup: true, publishEnabled: true, uploadRun: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pipelinePath, sessionPath := writeConfigFiles(t, testPipelineBaseYAML, testSessionBaseYAML)
cfg, err := Load(pipelinePath, sessionPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
cfg.Pipeline.Spool.DeleteAudioAfterPublish = test.spoolCleanup
cfg.Pipeline.Workspace.CleanupAfterPublish = test.workspaceCleanup
cfg.Pipeline.Publish.Enabled = &test.publishEnabled
cfg.Pipeline.Publish.UploadRun = &test.uploadRun
err = Validate(cfg)
if test.wantError && err == nil {
t.Fatal("Validate() error = nil")
}
if !test.wantError && err != nil {
t.Fatalf("Validate() error = %v", err)
}
})
}
}
func TestPublishOutputValidation(t *testing.T) {
tests := []struct {
name string
ruleYML string
wantErr string
}{
{
name: "absolute dest path rejected",
ruleYML: `publish:
outputs:
- source: "narratio.transcript.final_trimmed"
dest: "/transcripts/final.trimmed.json"
`,
wantErr: "must be a relative path",
},
{
name: "traversal dest path rejected",
ruleYML: `publish:
outputs:
- source: "narratio.transcript.final_trimmed"
dest: "../trimmed.json"
`,
wantErr: "must not contain path traversal",
},
{
name: "invalid source rejected",
ruleYML: `publish:
outputs:
- source: "narratio.unknown"
dest: "transcripts/final.trimmed.json"
`,
wantErr: "source \"narratio.unknown\" is unsupported",
},
{
name: "prepared input source rejected",
ruleYML: `publish:
outputs:
- source: "narratio.input.players"
dest: "inputs/players.yml"
`,
wantErr: "source \"narratio.input.players\" is unsupported",
},
{
name: "duplicate destination rejected",
ruleYML: `publish:
outputs:
- source: "narratio.transcript.final_trimmed"
dest: "artifacts/shared.md"
- source: "narratio.transcript.final"
dest: "artifacts/shared.md"
`,
wantErr: "duplicates another publish output destination",
},
{
name: "configured source requires configured artifact key",
ruleYML: `publish:
outputs:
- source: "narratio.artifact.session_recap"
dest: "artifacts/session_recap.md"
`,
wantErr: "configured artifact \"session_recap\" is not defined in pipeline.scriptorium.artifacts",
},
{
name: "configured source without output path fails when dest omitted",
ruleYML: `scriptorium:
artifacts:
session_recap:
enabled: false
publish:
outputs:
- source: "narratio.artifact.session_recap"
`,
wantErr: "destination cannot be derived",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pipelineYAML := testPipelineBaseYAML + "\n" + tt.ruleYML
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
cfg, err := Load(pipelinePath, sessionPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
err = Validate(cfg)
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("Validate() error = %v, want to contain %q", err, tt.wantErr)
}
})
}
}
func TestPublishOutputUnsupportedTranscriptSourceRejected(t *testing.T) {
source := "narratio.transcript." + "merged"
pipelineYAML := testPipelineBaseYAML + `
publish:
outputs:
- source: ` + source + `
dest: transcripts/final.trimmed.json
`
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
cfg, err := Load(pipelinePath, sessionPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
err = Validate(cfg)
if err == nil || !strings.Contains(err.Error(), "pipeline.publish.outputs[0].source") || !strings.Contains(err.Error(), "unsupported") {
t.Fatalf("Validate() error = %v, want source field and unsupported category", err)
}
}
func TestPublishOutputDerivesDestinationWhenOmitted(t *testing.T) {
tests := []struct {
name string
pipelineYML string
wantDest string
}{
{
name: "built in source derives canonical destination",
pipelineYML: testPipelineBaseYAML + `
publish:
outputs:
- source: narratio.transcript.final
`,
wantDest: "transcripts/final.json",
},
{
name: "configured source derives configured output path",
pipelineYML: testPipelineBaseYAML + `
scriptorium:
artifacts:
session_recap:
enabled: true
prompt_id: dnd.session_recap
output_path: artifacts/session_recap.md
publish:
outputs:
- source: narratio.artifact.session_recap
`,
wantDest: "artifacts/session_recap.md",
},
{
name: "markdown built in derives canonical destination",
pipelineYML: testPipelineBaseYAML + `
publish:
outputs:
- source: narratio.transcript.final_markdown
`,
wantDest: "transcripts/final.md",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pipelinePath, sessionPath := writeConfigFiles(t, tt.pipelineYML, testSessionBaseYAML)
cfg, err := Load(pipelinePath, sessionPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if err := Validate(cfg); err != nil {
t.Fatalf("Validate() error = %v", err)
}
if len(cfg.Pipeline.Publish.Outputs) != 1 {
t.Fatalf("publish.outputs len = %d, want 1", len(cfg.Pipeline.Publish.Outputs))
}
if cfg.Pipeline.Publish.Outputs[0].Dest != tt.wantDest {
t.Fatalf("publish.outputs[0].dest = %q, want %q", cfg.Pipeline.Publish.Outputs[0].Dest, tt.wantDest)
}
})
}
}
func TestPublishDerivedDestinationsPreserveExplicitRequiredness(t *testing.T) {
pipelinePath, sessionPath := writeConfigFiles(t, testPipelineBaseYAML+`
scriptorium:
artifacts:
session_recap:
enabled: true
prompt_id: dnd.session_recap
output_path: artifacts/session_recap.md
publish:
outputs:
- {source: narratio.transcript.final, required: true}
- {source: narratio.transcript.final_markdown, required: false}
- {source: narratio.artifact.session_recap, required: false}
- {source: narratio.transcript.final_trimmed, dest: custom/trimmed.json, required: true}
`, testSessionBaseYAML)
cfg, err := Load(pipelinePath, sessionPath)
if err != nil {
t.Fatal(err)
}
if err := Validate(cfg); err != nil {
t.Fatal(err)
}
effective, err := MarshalEffectivePipeline(cfg.Pipeline)
if err != nil {
t.Fatal(err)
}
var projected struct {
Publish PublishConfig `yaml:"publish"`
}
if err := yaml.Unmarshal(effective, &projected); err != nil {
t.Fatal(err)
}
wantDest := []string{"transcripts/final.json", "transcripts/final.md", "artifacts/session_recap.md", "custom/trimmed.json"}
wantRequired := []bool{true, false, false, true}
for label, publish := range map[string]*PublishConfig{"loaded": cfg.Pipeline.Publish, "effective": &projected.Publish} {
if len(publish.Outputs) != len(wantDest) {
t.Fatalf("%s output count = %d, want %d", label, len(publish.Outputs), len(wantDest))
}
for i, output := range publish.Outputs {
if output.Dest != wantDest[i] || output.Required == nil || *output.Required != wantRequired[i] {
t.Errorf("%s output %d = %#v, want destination %q and required %t", label, i, output, wantDest[i], wantRequired[i])
}
}
}
}
func TestScriptoriumOutputPathsIgnoreInvalidKeys(t *testing.T) {
scriptorium := &ScriptoriumConfig{Artifacts: map[string]ScriptoriumArtifactConfig{
" artifact": {OutputPath: "artifacts/invalid.md"},
"artifact": {OutputPath: "artifacts/canonical.md"},
}}
paths := scriptoriumOutputPathMap(scriptorium)
if got := paths["artifact"]; got != "artifacts/canonical.md" {
t.Fatalf("output path = %q, want canonical artifact declaration", got)
}
}
func TestPublishLocksFieldFailsStrictDecode(t *testing.T) {
pipelineYAML := testPipelineBaseYAML + `
publish:
locks:
- source: narratio.transcript.final_trimmed
`
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
_, err := Load(pipelinePath, sessionPath)
if err == nil || !strings.Contains(err.Error(), "decode.unknown_field") {
t.Fatalf("Load() error = %v, want retired locks field rejected", err)
}
}
func TestPublishLegacyFromToFailsStrictDecode(t *testing.T) {
pipelineYAML := testPipelineBaseYAML + `
publish:
outputs:
- from: transcripts/final.trimmed.json
to: transcripts/final.trimmed.json
`
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
_, err := Load(pipelinePath, sessionPath)
if err == nil || !strings.Contains(err.Error(), "strict decode failed") {
t.Fatalf("Load() error = %v, want strict decode failed", err)
}
}
func TestSessionAudioS3Validation(t *testing.T) {
tests := []struct {
name string
sessionYAML string
wantErr string
}{
{
name: "valid audio_s3 prefix",
sessionYAML: `session_id: 2026-05-03
campaign: forsaken
inputs:
audio_s3:
prefix: audio/
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
`,
},
{
name: "invalid audio_s3 absolute prefix",
sessionYAML: `session_id: 2026-05-03
campaign: forsaken
inputs:
audio_s3:
prefix: /audio/
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
`,
wantErr: "session.inputs.audio_s3.prefix must be a relative path",
},
{
name: "invalid audio_s3 traversal prefix",
sessionYAML: `session_id: 2026-05-03
campaign: forsaken
inputs:
audio_s3:
prefix: ../audio/
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
`,
wantErr: "session.inputs.audio_s3.prefix must not contain path traversal",
},
{
name: "local and s3 audio conflict",
sessionYAML: `session_id: 2026-05-03
campaign: forsaken
inputs:
audio_dir: ./audio
audio_s3:
prefix: audio/
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
`,
wantErr: "mutually exclusive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pipelineYAML := testPipelineBaseYAML + `
storage:
backend: s3
s3:
bucket: my-dnd-archive
`
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, tt.sessionYAML)
cfg, err := Load(pipelinePath, sessionPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
err = Validate(cfg)
if tt.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("Validate() error = %v, want to contain %q", err, tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("Validate() error = %v", err)
}
})
}
}
func TestStorageS3BucketRequiredWhenS3DependentFeatureEnabled(t *testing.T) {
pipelineYAML := testPipelineBaseYAML + `
storage:
backend: s3
s3: {}
`
sessionYAML := `session_id: 2026-05-03
campaign: forsaken
inputs:
audio_s3:
prefix: audio/
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
`
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, sessionYAML)
cfg, err := Load(pipelinePath, sessionPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
err = Validate(cfg)
if err == nil || !strings.Contains(err.Error(), "pipeline.storage.s3.bucket is required") {
t.Fatalf("Validate() error = %v, want bucket requirement", err)
}
}
func TestLocalAudioConfigStillValid(t *testing.T) {
pipelinePath, sessionPath := writeConfigFiles(t, testPipelineBaseYAML, testSessionBaseYAML)
cfg, err := Load(pipelinePath, sessionPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if err := Validate(cfg); err != nil {
t.Fatalf("Validate() error = %v", err)
}
}