Complete configuration diagnostics workstream
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user