diff --git a/docs/internal/configuration.md b/docs/internal/configuration.md index bb6be25..0e1a423 100644 --- a/docs/internal/configuration.md +++ b/docs/internal/configuration.md @@ -11,10 +11,11 @@ pipeline-loading boundary implemented by `internal/config`. 1. Parse the root YAML into a presence-aware composition tree. The tree retains source names, full field paths, node kinds, declaration order, and explicit zero, false, empty-map, and empty-list values. -2. Remove the root-only `composition` envelope and validate its explicit - `imports`, `default_profile`, and named `profiles` declarations. A load - option retains the difference between omitted and explicitly empty profile - selection. +2. Decode the root-only `composition` envelope, validate its declaration shapes + and cross-field profile semantics, then clone the root without that envelope + as the ordinary pipeline body. The parsed source document is not mutated. A + load option retains the difference between omitted and explicitly empty + profile selection. 3. Open each import relative to the root pipeline directory through the confined regular-file boundary. Imports must use a `.yml` or `.yaml` extension and cannot traverse, use symlinks, repeat a file, import the root, @@ -108,6 +109,27 @@ projection. An unversioned source is rejected before resolution, family expansion, or prepare-stage materialization. +## Validation Ownership + +Configuration validation remains within `internal/config`, with the public +entry points and pipeline-domain sequencing separated from the private owners +for core pipeline/session rules, external adapters, publish/storage, Notarius, +Scriptorium, and shared scalar/path checks. These are package-local ownership +boundaries rather than extension points or a general validation framework. + +Validation fails fast and does not define which of several independent invalid +fields is reported first. Map-backed declarations are nevertheless traversed +deterministically. Notarius validates scalar policy, builds normalized reference +bindings and output lanes independently, and installs all candidate values only +after every check succeeds. Scriptorium validates artifact declarations and +outputs, dependencies and inputs, variables, referenced outputs, and the final +dependency graph in deterministic artifact order. + +Diagnostics identify the relevant semantic field. Composition conflicts also +retain every claiming source needed to correct an additive ownership conflict. +Complete prose, punctuation, formatting, and private validation choreography are +not compatibility contracts. + ## Diagnostics And Runtime Metadata Syntax, duplicate-key, composition, conflict, and schema failures include the diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 57e5b99..d67bf64 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -2,10 +2,10 @@ ## Status -Implementation plan for Workstream 7, **Configuration structure and semantic -diagnostics**, in [`refactoring.md`](refactoring.md). Workstreams 1 through 6 -are complete and are prerequisites for this plan. All stages below are pending -and must be implemented in numerical order. +Completed implementation record for Workstream 7, **Configuration structure +and semantic diagnostics**, in [`refactoring.md`](refactoring.md). Workstreams +1 through 6 were prerequisites, and all stages below were completed in +numerical order. The purpose of this workstream is to preserve Narratio's strict, deterministic, operator-oriented configuration behavior while making the implementation @@ -451,7 +451,7 @@ that imports, profiles, diagnostics, and inspection commands require. ## Stage 6 — Align Diagnostic Tests, Documentation, And Close Workstream 7 -**Status:** Pending +**Status:** Completed ### Goal diff --git a/docs/roadmap/refactoring.md b/docs/roadmap/refactoring.md index 8d60ad7..25f068c 100644 --- a/docs/roadmap/refactoring.md +++ b/docs/roadmap/refactoring.md @@ -314,7 +314,7 @@ needs attention. ### Workstream 7 — Configuration structure and semantic diagnostics -**Status:** Pending +**Status:** Complete **Audit sources:** H04, H14, H15, H17, H38, H39, H40, H41, H42, C01, and C06. diff --git a/internal/config/notarius_test.go b/internal/config/notarius_test.go index 80a19c4..02628f3 100644 --- a/internal/config/notarius_test.go +++ b/internal/config/notarius_test.go @@ -192,20 +192,22 @@ func TestNotariusReferenceValidationRejectsInvalidBindings(t *testing.T) { tests := []struct { name string references map[string]string - wantErr string + wantField string + wantReason string }{ - {name: "empty selector", references: map[string]string{" ": "narratio.input.party"}, wantErr: "selector is required"}, - {name: "equals in selector", references: map[string]string{"party=x": "narratio.input.party"}, wantErr: "must not contain"}, - {name: "invalid stage", references: map[string]string{"lane.prepare.party": "narratio.input.party"}, wantErr: "middle component"}, - {name: "empty source", references: map[string]string{"party": " "}, wantErr: "source is required"}, - {name: "unsupported source", references: map[string]string{"party": "narratio.input.unknown"}, wantErr: "not a supported prepared input source"}, + {name: "empty selector", references: map[string]string{" ": "narratio.input.party"}, wantField: "pipeline.notarius.references", wantReason: "selector is required"}, + {name: "equals in selector", references: map[string]string{"party=x": "narratio.input.party"}, wantField: "pipeline.notarius.references", wantReason: "must not contain"}, + {name: "invalid stage", references: map[string]string{"lane.prepare.party": "narratio.input.party"}, wantField: "pipeline.notarius.references", wantReason: "middle component"}, + {name: "empty source", references: map[string]string{"party": " "}, wantField: "pipeline.notarius.references.party", wantReason: "source is required"}, + {name: "unsupported source", references: map[string]string{"party": "narratio.input.unknown"}, wantField: "pipeline.notarius.references.party", wantReason: "not a supported prepared input source"}, { name: "normalized collision", references: map[string]string{ "chunk.party": "narratio.input.party", " chunk . party ": "narratio.input.players", }, - wantErr: "normalize to", + wantField: "pipeline.notarius.references", + wantReason: "normalize to", }, } @@ -214,8 +216,8 @@ func TestNotariusReferenceValidationRejectsInvalidBindings(t *testing.T) { cfg := validNotariusConfig() cfg.References = tt.references err := validateNotarius(cfg, nil) - if err == nil || !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("validateNotarius() error = %v, want containing %q", err, tt.wantErr) + if err == nil || !strings.Contains(err.Error(), tt.wantField) || !strings.Contains(err.Error(), tt.wantReason) { + t.Fatalf("validateNotarius() error = %v, want field %q and reason %q", err, tt.wantField, tt.wantReason) } }) } @@ -398,21 +400,21 @@ func TestNotariusEnabledValidation(t *testing.T) { tests := []struct { name string section string - wantErr string + wantErr []string }{ {name: "valid", section: valid}, - {name: "blank binary", section: strings.Replace(valid, " enabled: true\n", " enabled: true\n binary: \" \"\n", 1), wantErr: "pipeline.notarius.binary is required"}, - {name: "missing config path", section: strings.Replace(valid, " config_path: ./notarius.yml\n", "", 1), wantErr: "pipeline.notarius.config_path is required"}, - {name: "missing pipeline id", section: strings.Replace(valid, " pipeline_id: dnd-session\n", "", 1), wantErr: "pipeline.notarius.pipeline_id is required"}, - {name: "missing outputs", section: strings.Split(valid, " outputs:\n")[0], wantErr: "pipeline.notarius.outputs must contain at least one output"}, - {name: "zero timeout", section: strings.Replace(valid, " timeout: 45m", " timeout: 0s", 1), wantErr: "pipeline.notarius.timeout must be positive"}, - {name: "negative timeout", section: strings.Replace(valid, " timeout: 45m", " timeout: -1s", 1), wantErr: "pipeline.notarius.timeout must be positive"}, - {name: "invalid timeout", section: strings.Replace(valid, " timeout: 45m", " timeout: later", 1), wantErr: "pipeline.notarius.timeout must be a valid duration"}, - {name: "invalid output key", section: strings.Replace(valid, " npc_registry:", " npc-registry:", 1), wantErr: "outputs keys must match"}, - {name: "missing lane", section: strings.Replace(valid, " lane_id: npc-registry\n", "", 1), wantErr: "lane_id is required"}, - {name: "missing media type", section: strings.Replace(valid, " media_type: application/json\n", "", 1), wantErr: "media_type is required"}, - {name: "missing schema id", section: strings.Replace(valid, " schema_id: notarius.dnd.npc_registry\n", "", 1), wantErr: "schema_id is required"}, - {name: "missing schema version", section: strings.Replace(valid, " schema_version: v1\n", "", 1), wantErr: "schema_version is required"}, + {name: "blank binary", section: strings.Replace(valid, " enabled: true\n", " enabled: true\n binary: \" \"\n", 1), wantErr: []string{"pipeline.notarius.binary", "required"}}, + {name: "missing config path", section: strings.Replace(valid, " config_path: ./notarius.yml\n", "", 1), wantErr: []string{"pipeline.notarius.config_path", "required"}}, + {name: "missing pipeline id", section: strings.Replace(valid, " pipeline_id: dnd-session\n", "", 1), wantErr: []string{"pipeline.notarius.pipeline_id", "required"}}, + {name: "missing outputs", section: strings.Split(valid, " outputs:\n")[0], wantErr: []string{"pipeline.notarius.outputs", "at least one"}}, + {name: "zero timeout", section: strings.Replace(valid, " timeout: 45m", " timeout: 0s", 1), wantErr: []string{"pipeline.notarius.timeout", "positive"}}, + {name: "negative timeout", section: strings.Replace(valid, " timeout: 45m", " timeout: -1s", 1), wantErr: []string{"pipeline.notarius.timeout", "positive"}}, + {name: "invalid timeout", section: strings.Replace(valid, " timeout: 45m", " timeout: later", 1), wantErr: []string{"pipeline.notarius.timeout", "valid duration"}}, + {name: "invalid output key", section: strings.Replace(valid, " npc_registry:", " npc-registry:", 1), wantErr: []string{"pipeline.notarius.outputs keys", "must match"}}, + {name: "missing lane", section: strings.Replace(valid, " lane_id: npc-registry\n", "", 1), wantErr: []string{"pipeline.notarius.outputs.npc_registry.lane_id", "required"}}, + {name: "missing media type", section: strings.Replace(valid, " media_type: application/json\n", "", 1), wantErr: []string{"pipeline.notarius.outputs.npc_registry.media_type", "required"}}, + {name: "missing schema id", section: strings.Replace(valid, " schema_id: notarius.dnd.npc_registry\n", "", 1), wantErr: []string{"pipeline.notarius.outputs.npc_registry.schema_id", "required"}}, + {name: "missing schema version", section: strings.Replace(valid, " schema_version: v1\n", "", 1), wantErr: []string{"pipeline.notarius.outputs.npc_registry.schema_version", "required"}}, { name: "normalized key collision", section: strings.Replace(valid, " module_key: dnd/npc-registry\n", ` module_key: dnd/npc-registry @@ -422,7 +424,7 @@ func TestNotariusEnabledValidation(t *testing.T) { schema_id: two schema_version: v1 `, 1), - wantErr: "normalize to duplicate source", + wantErr: []string{"pipeline.notarius.outputs keys", "duplicate source"}, }, { name: "duplicate normalized lane", @@ -433,7 +435,7 @@ func TestNotariusEnabledValidation(t *testing.T) { schema_id: two schema_version: v1 `, 1), - wantErr: "duplicates pipeline.notarius.outputs.npc_registry.lane_id", + wantErr: []string{"pipeline.notarius.outputs.spells.lane_id", "duplicates"}, }, } @@ -445,7 +447,7 @@ func TestNotariusEnabledValidation(t *testing.T) { t.Fatalf("Load() error = %v", err) } err = Validate(cfg) - if tt.wantErr == "" { + if len(tt.wantErr) == 0 { if err != nil { t.Fatalf("Validate() error = %v", err) } @@ -455,8 +457,13 @@ func TestNotariusEnabledValidation(t *testing.T) { } return } - if err == nil || !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("Validate() error = %v, want containing %q", err, tt.wantErr) + if err == nil { + t.Fatalf("Validate() error = nil, want containing %q", tt.wantErr) + } + for _, fragment := range tt.wantErr { + if !strings.Contains(err.Error(), fragment) { + t.Fatalf("Validate() error = %v, want containing %q", err, fragment) + } } }) } @@ -471,7 +478,7 @@ func TestExtractionReferencesRequireDeclaredOutput(t *testing.T) { tests := []struct { name string body string - wantErr string + wantErr []string }{ { name: "scriptorium declared extraction", @@ -492,7 +499,7 @@ func TestExtractionReferencesRequireDeclaredOutput(t *testing.T) { npcs: source: narratio.extraction.unknown `, - wantErr: `references unknown extraction output "unknown"`, + wantErr: []string{"pipeline.scriptorium.artifacts.recap.inputs.npcs.source", "unknown extraction output"}, }, { name: "publish declared extraction", @@ -509,7 +516,7 @@ func TestExtractionReferencesRequireDeclaredOutput(t *testing.T) { - source: narratio.extraction.unknown dest: artifacts/unknown.json `, - wantErr: `extraction output "unknown" is not defined`, + wantErr: []string{"pipeline.publish.outputs[0].source", "not defined"}, }, { name: "publish lock declared extraction", @@ -528,14 +535,19 @@ func TestExtractionReferencesRequireDeclaredOutput(t *testing.T) { t.Fatalf("Load() error = %v", err) } err = Validate(cfg) - if tt.wantErr == "" { + if len(tt.wantErr) == 0 { if err != nil { t.Fatalf("Validate() error = %v", err) } return } - if err == nil || !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("Validate() error = %v, want containing %q", err, tt.wantErr) + if err == nil { + t.Fatalf("Validate() error = nil, want containing %q", tt.wantErr) + } + for _, fragment := range tt.wantErr { + if !strings.Contains(err.Error(), fragment) { + t.Fatalf("Validate() error = %v, want containing %q", err, fragment) + } } }) } diff --git a/internal/config/scriptorium_test.go b/internal/config/scriptorium_test.go index 60e1a92..a9cb8ff 100644 --- a/internal/config/scriptorium_test.go +++ b/internal/config/scriptorium_test.go @@ -11,7 +11,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { name string scriptoriumYAML string wantLoadErr string - wantValidateErr string + wantValidateErr []string assert func(t *testing.T, cfg *Config) }{ { @@ -73,7 +73,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { enabled: true output_path: artifacts/session_recap.md `, - wantValidateErr: "pipeline.scriptorium.artifacts.session_recap.prompt_id is required when enabled", + wantValidateErr: []string{"pipeline.scriptorium.artifacts.session_recap.prompt_id", "required"}, }, { name: "enabled artifact missing output path fails", @@ -84,7 +84,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { enabled: true prompt_id: dnd.session_recap `, - wantValidateErr: "pipeline.scriptorium.artifacts.session_recap.output_path is required when enabled", + wantValidateErr: []string{"pipeline.scriptorium.artifacts.session_recap.output_path", "required"}, }, { name: "invalid timeout fails", @@ -92,7 +92,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { binary: scriptorium timeout: definitely-not-a-duration `, - wantValidateErr: "pipeline.scriptorium.timeout must be a valid duration", + wantValidateErr: []string{"pipeline.scriptorium.timeout", "valid duration"}, }, { name: "legacy previous session source fails validation", @@ -114,7 +114,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { session_id: true output_kind: session_recap `, - wantValidateErr: `pipeline.scriptorium.artifacts.session_recap.inputs.previous_recap.source "` + legacyPreviousSource + `" is unsupported`, + wantValidateErr: []string{"pipeline.scriptorium.artifacts.session_recap.inputs.previous_recap.source", "unsupported"}, }, { name: "obsolete input passthrough fields fail strict decoding", @@ -166,7 +166,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { 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_]*$`, + wantValidateErr: []string{"pipeline.scriptorium.artifacts.session_recap.inputs.previous_recap.source", "configured artifact key"}, }, { name: "canonical previous-session source invalid artifact key fails validation", @@ -182,7 +182,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { 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_]*$`, + wantValidateErr: []string{"pipeline.scriptorium.artifacts.session_recap.inputs.previous_recap.source", "configured artifact key"}, }, { name: "canonical previous-session source unknown artifact fails validation", @@ -198,7 +198,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { 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"`, + wantValidateErr: []string{"pipeline.scriptorium.artifacts.session_recap.inputs.previous_recap.source", "unknown artifact"}, }, { name: "canonical artifact source is accepted", @@ -265,7 +265,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { source: narratio.unknown required: true `, - wantValidateErr: `pipeline.scriptorium.artifacts.session_recap.inputs.transcript.source "narratio.unknown" is unsupported`, + wantValidateErr: []string{"pipeline.scriptorium.artifacts.session_recap.inputs.transcript.source", "unsupported"}, }, { name: "artifact render_debug override is accepted", @@ -380,7 +380,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { prompt_id: dnd.session_recap output_path: artifacts/session_recap.md `, - wantValidateErr: "pipeline.scriptorium.artifacts keys must match ^[a-z][a-z0-9_]*$", + wantValidateErr: []string{"pipeline.scriptorium.artifacts keys", "must match"}, }, { name: "artifact output path outside artifacts root fails validation", @@ -392,7 +392,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { prompt_id: dnd.session_recap output_path: transcripts/session_recap.md `, - wantValidateErr: "pipeline.scriptorium.artifacts.session_recap.output_path must be under artifacts/", + wantValidateErr: []string{"pipeline.scriptorium.artifacts.session_recap.output_path", "under artifacts/"}, }, { name: "missing depends_on for artifact source fails validation", @@ -416,7 +416,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { 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"`, + wantValidateErr: []string{"pipeline.scriptorium.artifacts.player_handout.inputs.recap.source", "requires depends_on"}, }, { name: "dependency on unknown artifact fails validation", @@ -434,7 +434,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { source: narratio.artifact.session_recap required: true `, - wantValidateErr: `pipeline.scriptorium.artifacts.player_handout.depends_on[0] "session_recap" is not a configured artifact key`, + wantValidateErr: []string{"pipeline.scriptorium.artifacts.player_handout.depends_on[0]", "not a configured artifact"}, }, { name: "self dependency fails validation", @@ -448,7 +448,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { prompt_id: dnd.session_recap output_path: artifacts/session_recap.md `, - wantValidateErr: "pipeline.scriptorium.artifacts.session_recap.depends_on must not include itself", + wantValidateErr: []string{"pipeline.scriptorium.artifacts.session_recap.depends_on", "itself"}, }, { name: "configured dependency cycle fails validation", @@ -476,7 +476,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { source: narratio.artifact.artifact_a required: true `, - wantValidateErr: "pipeline.scriptorium.artifacts dependencies must not contain cycles", + wantValidateErr: []string{"pipeline.scriptorium.artifacts dependencies", "cycles"}, }, { name: "disabled dependency cycle fails validation", @@ -494,7 +494,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { - artifact_a output_path: artifacts/b.md `, - wantValidateErr: "pipeline.scriptorium.artifacts dependencies must not contain cycles", + wantValidateErr: []string{"pipeline.scriptorium.artifacts dependencies", "cycles"}, }, { name: "artifact source typo fails validation", @@ -510,7 +510,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { source: narratio.artifact.session-recap required: true `, - wantValidateErr: `pipeline.scriptorium.artifacts.player_handout.inputs.recap.source "narratio.artifact.session-recap" is unsupported`, + wantValidateErr: []string{"pipeline.scriptorium.artifacts.player_handout.inputs.recap.source", "unsupported"}, }, { name: "referenced disabled artifact missing output path fails validation", @@ -530,7 +530,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { source: narratio.artifact.session_recap required: true `, - wantValidateErr: "pipeline.scriptorium.artifacts.session_recap.output_path is required when artifact is referenced", + wantValidateErr: []string{"pipeline.scriptorium.artifacts.session_recap.output_path", "referenced"}, }, } @@ -558,12 +558,14 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { } err = Validate(cfg) - if tt.wantValidateErr != "" { + if len(tt.wantValidateErr) > 0 { 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) + for _, fragment := range tt.wantValidateErr { + if !strings.Contains(err.Error(), fragment) { + t.Fatalf("validation error = %q, want to contain %q", err.Error(), fragment) + } } return } @@ -596,16 +598,9 @@ func TestScriptoriumDiagnosticsAreIndependentOfMapInsertionOrder(t *testing.T) { } } -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 + ` +func TestScriptoriumUnsupportedTranscriptSourceRejected(t *testing.T) { + source := "narratio.transcript." + "merged" + pipelineYAML := testPipelineBaseYAML + ` scriptorium: binary: scriptorium artifacts: @@ -618,17 +613,14 @@ scriptorium: 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) - } - }) + 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.scriptorium.artifacts.session_recap.inputs.transcript.source") || !strings.Contains(err.Error(), "unsupported") { + t.Fatalf("Validate() error = %v, want source field and unsupported category", err) } } diff --git a/internal/config/storage_publish_test.go b/internal/config/storage_publish_test.go index 5f3a873..809738f 100644 --- a/internal/config/storage_publish_test.go +++ b/internal/config/storage_publish_test.go @@ -400,32 +400,22 @@ publish: } } -func TestPublishOutputLegacyTranscriptSourcesRejected(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 + ` +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) - wantErr := `source "` + source + `" is unsupported` - if err == nil || !strings.Contains(err.Error(), wantErr) { - t.Fatalf("Validate() error = %v, want to contain %q", err, wantErr) - } - }) + 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) } } @@ -579,31 +569,21 @@ publish: } } -func TestPublishLockLegacyTranscriptSourcesRejected(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 + ` +func TestPublishLockUnsupportedTranscriptSourceRejected(t *testing.T) { + source := "narratio.transcript." + "merged" + pipelineYAML := testPipelineBaseYAML + ` publish: locks: - source: ` + source + ` ` - pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML) - cfg, err := Load(pipelinePath, sessionPath) - if err != nil { - t.Fatalf("Load() error = %v", err) - } - err = Validate(cfg) - wantErr := `source "` + source + `" is unsupported` - if err == nil || !strings.Contains(err.Error(), wantErr) { - t.Fatalf("Validate() error = %v, want to contain %q", err, wantErr) - } - }) + 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.locks[0].source") || !strings.Contains(err.Error(), "unsupported") { + t.Fatalf("Validate() error = %v, want source field and unsupported category", err) } }