Complete configuration diagnostics workstream

This commit is contained in:
2026-09-05 00:42:47 +00:00
parent 979d8049b1
commit 0d4d71b9dc
6 changed files with 133 additions and 127 deletions

View File

@@ -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 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 source names, full field paths, node kinds, declaration order, and explicit
zero, false, empty-map, and empty-list values. zero, false, empty-map, and empty-list values.
2. Remove the root-only `composition` envelope and validate its explicit 2. Decode the root-only `composition` envelope, validate its declaration shapes
`imports`, `default_profile`, and named `profiles` declarations. A load and cross-field profile semantics, then clone the root without that envelope
option retains the difference between omitted and explicitly empty profile as the ordinary pipeline body. The parsed source document is not mutated. A
selection. load option retains the difference between omitted and explicitly empty
profile selection.
3. Open each import relative to the root pipeline directory through the 3. Open each import relative to the root pipeline directory through the
confined regular-file boundary. Imports must use a `.yml` or `.yaml` confined regular-file boundary. Imports must use a `.yml` or `.yaml`
extension and cannot traverse, use symlinks, repeat a file, import the root, 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 An unversioned source is rejected before resolution, family expansion, or
prepare-stage materialization. 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 ## Diagnostics And Runtime Metadata
Syntax, duplicate-key, composition, conflict, and schema failures include the Syntax, duplicate-key, composition, conflict, and schema failures include the

View File

@@ -2,10 +2,10 @@
## Status ## Status
Implementation plan for Workstream 7, **Configuration structure and semantic Completed implementation record for Workstream 7, **Configuration structure
diagnostics**, in [`refactoring.md`](refactoring.md). Workstreams 1 through 6 and semantic diagnostics**, in [`refactoring.md`](refactoring.md). Workstreams
are complete and are prerequisites for this plan. All stages below are pending 1 through 6 were prerequisites, and all stages below were completed in
and must be implemented in numerical order. numerical order.
The purpose of this workstream is to preserve Narratio's strict, deterministic, The purpose of this workstream is to preserve Narratio's strict, deterministic,
operator-oriented configuration behavior while making the implementation 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 ## Stage 6 — Align Diagnostic Tests, Documentation, And Close Workstream 7
**Status:** Pending **Status:** Completed
### Goal ### Goal

View File

@@ -314,7 +314,7 @@ needs attention.
### Workstream 7 — Configuration structure and semantic diagnostics ### 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. **Audit sources:** H04, H14, H15, H17, H38, H39, H40, H41, H42, C01, and C06.

View File

@@ -192,20 +192,22 @@ func TestNotariusReferenceValidationRejectsInvalidBindings(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
references map[string]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: "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"}, wantErr: "must not contain"}, {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"}, wantErr: "middle component"}, {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": " "}, wantErr: "source is required"}, {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"}, wantErr: "not a supported prepared input source"}, {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", name: "normalized collision",
references: map[string]string{ references: map[string]string{
"chunk.party": "narratio.input.party", "chunk.party": "narratio.input.party",
" chunk . party ": "narratio.input.players", " 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 := validNotariusConfig()
cfg.References = tt.references cfg.References = tt.references
err := validateNotarius(cfg, nil) err := validateNotarius(cfg, nil)
if err == nil || !strings.Contains(err.Error(), tt.wantErr) { if err == nil || !strings.Contains(err.Error(), tt.wantField) || !strings.Contains(err.Error(), tt.wantReason) {
t.Fatalf("validateNotarius() error = %v, want containing %q", err, tt.wantErr) 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 { tests := []struct {
name string name string
section string section string
wantErr string wantErr []string
}{ }{
{name: "valid", section: valid}, {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: "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: "pipeline.notarius.config_path is 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: "pipeline.notarius.pipeline_id is 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: "pipeline.notarius.outputs must contain at least one output"}, {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: "pipeline.notarius.timeout must be positive"}, {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: "pipeline.notarius.timeout must be 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: "pipeline.notarius.timeout must be a valid duration"}, {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: "outputs keys must match"}, {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: "lane_id is required"}, {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: "media_type is 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: "schema_id is 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: "schema_version is 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", name: "normalized key collision",
section: strings.Replace(valid, " module_key: dnd/npc-registry\n", ` module_key: dnd/npc-registry 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_id: two
schema_version: v1 schema_version: v1
`, 1), `, 1),
wantErr: "normalize to duplicate source", wantErr: []string{"pipeline.notarius.outputs keys", "duplicate source"},
}, },
{ {
name: "duplicate normalized lane", name: "duplicate normalized lane",
@@ -433,7 +435,7 @@ func TestNotariusEnabledValidation(t *testing.T) {
schema_id: two schema_id: two
schema_version: v1 schema_version: v1
`, 1), `, 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) t.Fatalf("Load() error = %v", err)
} }
err = Validate(cfg) err = Validate(cfg)
if tt.wantErr == "" { if len(tt.wantErr) == 0 {
if err != nil { if err != nil {
t.Fatalf("Validate() error = %v", err) t.Fatalf("Validate() error = %v", err)
} }
@@ -455,8 +457,13 @@ func TestNotariusEnabledValidation(t *testing.T) {
} }
return return
} }
if err == nil || !strings.Contains(err.Error(), tt.wantErr) { if err == nil {
t.Fatalf("Validate() error = %v, want containing %q", err, tt.wantErr) 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 { tests := []struct {
name string name string
body string body string
wantErr string wantErr []string
}{ }{
{ {
name: "scriptorium declared extraction", name: "scriptorium declared extraction",
@@ -492,7 +499,7 @@ func TestExtractionReferencesRequireDeclaredOutput(t *testing.T) {
npcs: npcs:
source: narratio.extraction.unknown 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", name: "publish declared extraction",
@@ -509,7 +516,7 @@ func TestExtractionReferencesRequireDeclaredOutput(t *testing.T) {
- source: narratio.extraction.unknown - source: narratio.extraction.unknown
dest: artifacts/unknown.json 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", name: "publish lock declared extraction",
@@ -528,14 +535,19 @@ func TestExtractionReferencesRequireDeclaredOutput(t *testing.T) {
t.Fatalf("Load() error = %v", err) t.Fatalf("Load() error = %v", err)
} }
err = Validate(cfg) err = Validate(cfg)
if tt.wantErr == "" { if len(tt.wantErr) == 0 {
if err != nil { if err != nil {
t.Fatalf("Validate() error = %v", err) t.Fatalf("Validate() error = %v", err)
} }
return return
} }
if err == nil || !strings.Contains(err.Error(), tt.wantErr) { if err == nil {
t.Fatalf("Validate() error = %v, want containing %q", err, tt.wantErr) 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)
}
} }
}) })
} }

View File

@@ -11,7 +11,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
name string name string
scriptoriumYAML string scriptoriumYAML string
wantLoadErr string wantLoadErr string
wantValidateErr string wantValidateErr []string
assert func(t *testing.T, cfg *Config) assert func(t *testing.T, cfg *Config)
}{ }{
{ {
@@ -73,7 +73,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
enabled: true enabled: true
output_path: artifacts/session_recap.md 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", name: "enabled artifact missing output path fails",
@@ -84,7 +84,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
enabled: true enabled: true
prompt_id: dnd.session_recap 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", name: "invalid timeout fails",
@@ -92,7 +92,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
binary: scriptorium binary: scriptorium
timeout: definitely-not-a-duration 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", name: "legacy previous session source fails validation",
@@ -114,7 +114,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
session_id: true session_id: true
output_kind: session_recap 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", name: "obsolete input passthrough fields fail strict decoding",
@@ -166,7 +166,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
source: narratio.previous_session.artifact. source: narratio.previous_session.artifact.
required: false 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", 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 source: narratio.previous_session.artifact.session-recap
required: false 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", 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 source: narratio.previous_session.artifact.quest_log
required: false 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", name: "canonical artifact source is accepted",
@@ -265,7 +265,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
source: narratio.unknown source: narratio.unknown
required: true 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", name: "artifact render_debug override is accepted",
@@ -380,7 +380,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
prompt_id: dnd.session_recap prompt_id: dnd.session_recap
output_path: artifacts/session_recap.md 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", name: "artifact output path outside artifacts root fails validation",
@@ -392,7 +392,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
prompt_id: dnd.session_recap prompt_id: dnd.session_recap
output_path: transcripts/session_recap.md 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", name: "missing depends_on for artifact source fails validation",
@@ -416,7 +416,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
source: narratio.artifact.session_recap source: narratio.artifact.session_recap
required: true 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", name: "dependency on unknown artifact fails validation",
@@ -434,7 +434,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
source: narratio.artifact.session_recap source: narratio.artifact.session_recap
required: true 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", name: "self dependency fails validation",
@@ -448,7 +448,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
prompt_id: dnd.session_recap prompt_id: dnd.session_recap
output_path: artifacts/session_recap.md 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", name: "configured dependency cycle fails validation",
@@ -476,7 +476,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
source: narratio.artifact.artifact_a source: narratio.artifact.artifact_a
required: true required: true
`, `,
wantValidateErr: "pipeline.scriptorium.artifacts dependencies must not contain cycles", wantValidateErr: []string{"pipeline.scriptorium.artifacts dependencies", "cycles"},
}, },
{ {
name: "disabled dependency cycle fails validation", name: "disabled dependency cycle fails validation",
@@ -494,7 +494,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
- artifact_a - artifact_a
output_path: artifacts/b.md 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", name: "artifact source typo fails validation",
@@ -510,7 +510,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
source: narratio.artifact.session-recap source: narratio.artifact.session-recap
required: true 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", name: "referenced disabled artifact missing output path fails validation",
@@ -530,7 +530,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
source: narratio.artifact.session_recap source: narratio.artifact.session_recap
required: true 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) err = Validate(cfg)
if tt.wantValidateErr != "" { if len(tt.wantValidateErr) > 0 {
if err == nil { if err == nil {
t.Fatalf("expected validation error containing %q, got nil", tt.wantValidateErr) t.Fatalf("expected validation error containing %q, got nil", tt.wantValidateErr)
} }
if !strings.Contains(err.Error(), tt.wantValidateErr) { for _, fragment := range tt.wantValidateErr {
t.Fatalf("validation error = %q, want to contain %q", err.Error(), tt.wantValidateErr) if !strings.Contains(err.Error(), fragment) {
t.Fatalf("validation error = %q, want to contain %q", err.Error(), fragment)
}
} }
return return
} }
@@ -596,16 +598,9 @@ func TestScriptoriumDiagnosticsAreIndependentOfMapInsertionOrder(t *testing.T) {
} }
} }
func TestScriptoriumLegacyTranscriptSourcesRejected(t *testing.T) { func TestScriptoriumUnsupportedTranscriptSourceRejected(t *testing.T) {
legacyTranscriptSources := []string{ source := "narratio.transcript." + "merged"
"narratio.transcript." + "merged", pipelineYAML := testPipelineBaseYAML + `
"narratio.transcript." + "full",
"narratio.transcript." + "trimmed",
}
for _, source := range legacyTranscriptSources {
t.Run(source, func(t *testing.T) {
pipelineYAML := testPipelineBaseYAML + `
scriptorium: scriptorium:
binary: scriptorium binary: scriptorium
artifacts: artifacts:
@@ -618,17 +613,14 @@ scriptorium:
source: ` + source + ` source: ` + source + `
required: true required: true
` `
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML) pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
cfg, err := Load(pipelinePath, sessionPath) cfg, err := Load(pipelinePath, sessionPath)
if err != nil { if err != nil {
t.Fatalf("Load() error = %v", err) t.Fatalf("Load() error = %v", err)
} }
err = Validate(cfg) err = Validate(cfg)
wantErr := `pipeline.scriptorium.artifacts.session_recap.inputs.transcript.source "` + source + `" is unsupported` if err == nil || !strings.Contains(err.Error(), "pipeline.scriptorium.artifacts.session_recap.inputs.transcript.source") || !strings.Contains(err.Error(), "unsupported") {
if err == nil || !strings.Contains(err.Error(), wantErr) { t.Fatalf("Validate() error = %v, want source field and unsupported category", err)
t.Fatalf("Validate() error = %v, want to contain %q", err, wantErr)
}
})
} }
} }

View File

@@ -400,32 +400,22 @@ publish:
} }
} }
func TestPublishOutputLegacyTranscriptSourcesRejected(t *testing.T) { func TestPublishOutputUnsupportedTranscriptSourceRejected(t *testing.T) {
legacyTranscriptSources := []string{ source := "narratio.transcript." + "merged"
"narratio.transcript." + "merged", pipelineYAML := testPipelineBaseYAML + `
"narratio.transcript." + "full",
"narratio.transcript." + "trimmed",
}
for _, source := range legacyTranscriptSources {
t.Run(source, func(t *testing.T) {
pipelineYAML := testPipelineBaseYAML + `
publish: publish:
outputs: outputs:
- source: ` + source + ` - source: ` + source + `
dest: transcripts/final.trimmed.json dest: transcripts/final.trimmed.json
` `
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML) pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
cfg, err := Load(pipelinePath, sessionPath) cfg, err := Load(pipelinePath, sessionPath)
if err != nil { if err != nil {
t.Fatalf("Load() error = %v", err) t.Fatalf("Load() error = %v", err)
} }
err = Validate(cfg) err = Validate(cfg)
wantErr := `source "` + source + `" is unsupported` if err == nil || !strings.Contains(err.Error(), "pipeline.publish.outputs[0].source") || !strings.Contains(err.Error(), "unsupported") {
if err == nil || !strings.Contains(err.Error(), wantErr) { t.Fatalf("Validate() error = %v, want source field and unsupported category", err)
t.Fatalf("Validate() error = %v, want to contain %q", err, wantErr)
}
})
} }
} }
@@ -579,31 +569,21 @@ publish:
} }
} }
func TestPublishLockLegacyTranscriptSourcesRejected(t *testing.T) { func TestPublishLockUnsupportedTranscriptSourceRejected(t *testing.T) {
legacyTranscriptSources := []string{ source := "narratio.transcript." + "merged"
"narratio.transcript." + "merged", pipelineYAML := testPipelineBaseYAML + `
"narratio.transcript." + "full",
"narratio.transcript." + "trimmed",
}
for _, source := range legacyTranscriptSources {
t.Run(source, func(t *testing.T) {
pipelineYAML := testPipelineBaseYAML + `
publish: publish:
locks: locks:
- source: ` + source + ` - source: ` + source + `
` `
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML) pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
cfg, err := Load(pipelinePath, sessionPath) cfg, err := Load(pipelinePath, sessionPath)
if err != nil { if err != nil {
t.Fatalf("Load() error = %v", err) t.Fatalf("Load() error = %v", err)
} }
err = Validate(cfg) err = Validate(cfg)
wantErr := `source "` + source + `" is unsupported` if err == nil || !strings.Contains(err.Error(), "pipeline.publish.locks[0].source") || !strings.Contains(err.Error(), "unsupported") {
if err == nil || !strings.Contains(err.Error(), wantErr) { t.Fatalf("Validate() error = %v, want source field and unsupported category", err)
t.Fatalf("Validate() error = %v, want to contain %q", err, wantErr)
}
})
} }
} }