Make cross-configuration validation deterministic

This commit is contained in:
2026-09-05 03:37:00 +00:00
parent 0d4d71b9dc
commit 5b62a8d60d
5 changed files with 35 additions and 5 deletions

View File

@@ -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"}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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 == "" {

View File

@@ -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