diff --git a/internal/config/notarius_test.go b/internal/config/notarius_test.go index 02628f3..a5cbf12 100644 --- a/internal/config/notarius_test.go +++ b/internal/config/notarius_test.go @@ -351,11 +351,14 @@ func cloneNotariusConfigForTest(cfg *NotariusConfig) *NotariusConfig { func TestNotariusSpellCatalogReferenceRequiresEffectiveInput(t *testing.T) { cfg := loadedValidConfig(t) cfg.Pipeline.Notarius = validNotariusConfig() - cfg.Pipeline.Notarius.References = map[string]string{"spells": "narratio.input.spell_catalog"} + cfg.Pipeline.Notarius.References = map[string]string{ + "zeta": "narratio.input.spell_catalog", + "alpha": "narratio.input.spell_catalog", + } err := Validate(cfg) - if err == nil || !strings.Contains(err.Error(), "requires campaign.inputs.spell_catalog_file or session.inputs.spell_catalog_file") { - t.Fatalf("Validate() error = %v, want missing spell catalog input", err) + if err == nil || !strings.Contains(err.Error(), "pipeline.notarius.references.alpha") || !strings.Contains(err.Error(), "requires campaign.inputs.spell_catalog_file or session.inputs.spell_catalog_file") { + t.Fatalf("Validate() error = %v, want sorted selector and missing spell catalog input", err) } cfg.StableInputs.SpellCatalogFile = ResolvedInputFile{Path: "./spells.json", Source: "campaign_config"} diff --git a/internal/config/storage_publish_test.go b/internal/config/storage_publish_test.go index 809738f..1a90493 100644 --- a/internal/config/storage_publish_test.go +++ b/internal/config/storage_publish_test.go @@ -480,6 +480,18 @@ publish: } } +func TestScriptoriumOutputPathsIgnoreInvalidKeys(t *testing.T) { + scriptorium := &ScriptoriumConfig{Artifacts: map[string]ScriptoriumArtifactConfig{ + " artifact": {OutputPath: "artifacts/invalid.md"}, + "artifact": {OutputPath: "artifacts/canonical.md"}, + }} + + paths := scriptoriumOutputPathMap(scriptorium) + if got := paths["artifact"]; got != "artifacts/canonical.md" { + t.Fatalf("output path = %q, want canonical artifact declaration", got) + } +} + func TestPublishLockValidation(t *testing.T) { tests := []struct { name string diff --git a/internal/config/validate_core.go b/internal/config/validate_core.go index 16f3e94..4355a07 100644 --- a/internal/config/validate_core.go +++ b/internal/config/validate_core.go @@ -139,7 +139,8 @@ func validateCrossConfig(pipeline *PipelineConfig, session *SessionConfig, stabl return nil } if pipeline.Notarius != nil && pipeline.Notarius.Enabled { - for selector, source := range pipeline.Notarius.References { + for _, selector := range sortedStringKeys(pipeline.Notarius.References) { + source := pipeline.Notarius.References[selector] if source == artifactpolicy.SourceInputSpellCatalog && strings.TrimSpace(stableInputs.SpellCatalogFile.Path) == "" { return fmt.Errorf("pipeline.notarius.references.%s requires campaign.inputs.spell_catalog_file or session.inputs.spell_catalog_file", selector) } diff --git a/internal/config/validate_helpers.go b/internal/config/validate_helpers.go index b37b6d9..a9b5957 100644 --- a/internal/config/validate_helpers.go +++ b/internal/config/validate_helpers.go @@ -4,6 +4,7 @@ import ( "fmt" "path/filepath" "regexp" + "sort" "strings" "time" ) @@ -11,6 +12,15 @@ import ( var windowsAbsPathRE = regexp.MustCompile(`^[A-Za-z]:[\\/].*`) var envVarNameRE = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) +func sortedStringKeys[V any](values map[string]V) []string { + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + func validateEnvVarNameField(fieldName, value string) error { trimmed := strings.TrimSpace(value) if trimmed == "" { diff --git a/internal/config/validate_publish.go b/internal/config/validate_publish.go index 9bb3629..a091caa 100644 --- a/internal/config/validate_publish.go +++ b/internal/config/validate_publish.go @@ -153,7 +153,11 @@ func scriptoriumOutputPathMap(scriptorium *ScriptoriumConfig) map[string]string if scriptorium == nil || len(scriptorium.Artifacts) == 0 { return out } - for key, artifactCfg := range scriptorium.Artifacts { + for _, key := range sortedStringKeys(scriptorium.Artifacts) { + artifactCfg := scriptorium.Artifacts[key] + if !artifactpolicy.IsConfiguredKey(key) { + continue + } out[strings.TrimSpace(key)] = strings.TrimSpace(artifactCfg.OutputPath) } return out