Add Notarius configuration and extraction source policy
This commit is contained in:
283
internal/config/notarius_test.go
Normal file
283
internal/config/notarius_test.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNotariusOmittedAndDisabledBehavior(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
omittedPath := filepath.Join(dir, "omitted.yml")
|
||||
if err := os.WriteFile(omittedPath, []byte(testPipelineBaseYAML), 0o644); err != nil {
|
||||
t.Fatalf("write omitted pipeline: %v", err)
|
||||
}
|
||||
omitted, err := LoadPipeline(omittedPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPipeline(omitted) error = %v", err)
|
||||
}
|
||||
if omitted.Notarius != nil {
|
||||
t.Fatalf("Notarius = %#v, want nil when omitted", omitted.Notarius)
|
||||
}
|
||||
|
||||
disabledPath := filepath.Join(dir, "disabled.yml")
|
||||
disabledYAML := testPipelineBaseYAML + `
|
||||
notarius:
|
||||
enabled: false
|
||||
config_path: relative/notarius.yml
|
||||
`
|
||||
if err := os.WriteFile(disabledPath, []byte(disabledYAML), 0o644); err != nil {
|
||||
t.Fatalf("write disabled pipeline: %v", err)
|
||||
}
|
||||
disabled, err := LoadPipeline(disabledPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPipeline(disabled) error = %v", err)
|
||||
}
|
||||
if disabled.Notarius == nil {
|
||||
t.Fatal("Notarius = nil, want configured disabled section")
|
||||
}
|
||||
if disabled.Notarius.Enabled {
|
||||
t.Fatal("Notarius.Enabled = true, want false")
|
||||
}
|
||||
if disabled.Notarius.Binary != DefaultNotariusBinary || disabled.Notarius.Timeout != DefaultNotariusTimeout {
|
||||
t.Fatalf("disabled defaults = %#v", disabled.Notarius)
|
||||
}
|
||||
if disabled.Notarius.ConfigPath != "relative/notarius.yml" || disabled.Notarius.WorkingDirectory != "" {
|
||||
t.Fatalf("disabled paths were resolved unexpectedly: %#v", disabled.Notarius)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotariusEnabledDefaultsAndPathResolution(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
pipelinePath := filepath.Join(dir, "deployment", "pipeline.yml")
|
||||
if err := os.MkdirAll(filepath.Dir(pipelinePath), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll() error = %v", err)
|
||||
}
|
||||
pipelineYAML := testPipelineBaseYAML + `
|
||||
notarius:
|
||||
enabled: true
|
||||
config_path: notarius/config.yml
|
||||
pipeline_id: dnd-session
|
||||
outputs:
|
||||
npc_registry:
|
||||
lane_id: npc-registry
|
||||
media_type: application/json
|
||||
schema_id: notarius.dnd.npc_registry
|
||||
schema_version: v1
|
||||
`
|
||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||
t.Fatalf("write pipeline: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadPipeline(pipelinePath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPipeline() error = %v", err)
|
||||
}
|
||||
wantConfigPath := filepath.Join(filepath.Dir(pipelinePath), "notarius", "config.yml")
|
||||
if cfg.Notarius.Binary != "notarius" || cfg.Notarius.Timeout != "3h" {
|
||||
t.Fatalf("defaults = %#v", cfg.Notarius)
|
||||
}
|
||||
if cfg.Notarius.ConfigPath != wantConfigPath {
|
||||
t.Fatalf("config_path = %q, want %q", cfg.Notarius.ConfigPath, wantConfigPath)
|
||||
}
|
||||
if cfg.Notarius.WorkingDirectory != filepath.Dir(wantConfigPath) {
|
||||
t.Fatalf("working_directory = %q, want %q", cfg.Notarius.WorkingDirectory, filepath.Dir(wantConfigPath))
|
||||
}
|
||||
|
||||
explicitPath := filepath.Join(dir, "explicit.yml")
|
||||
explicitYAML := strings.Replace(pipelineYAML, " pipeline_id: dnd-session\n", " pipeline_id: dnd-session\n working_directory: runtime\n", 1)
|
||||
if err := os.WriteFile(explicitPath, []byte(explicitYAML), 0o644); err != nil {
|
||||
t.Fatalf("write explicit pipeline: %v", err)
|
||||
}
|
||||
explicit, err := LoadPipeline(explicitPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPipeline(explicit working directory) error = %v", err)
|
||||
}
|
||||
if explicit.Notarius.WorkingDirectory != filepath.Join(dir, "runtime") {
|
||||
t.Fatalf("explicit working_directory = %q, want %q", explicit.Notarius.WorkingDirectory, filepath.Join(dir, "runtime"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotariusStrictYAML(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
yaml string
|
||||
}{
|
||||
{name: "unknown section field", yaml: "notarius:\n unknown: true\n"},
|
||||
{name: "unknown output field", yaml: "notarius:\n outputs:\n npc_registry:\n lane_id: npc-registry\n unknown: true\n"},
|
||||
{name: "unsupported session id", yaml: "notarius:\n session_id: forbidden\n"},
|
||||
{name: "unsupported model", yaml: "notarius:\n model: forbidden\n"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "pipeline.yml")
|
||||
if err := os.WriteFile(path, []byte(testPipelineBaseYAML+"\n"+tt.yaml), 0o644); err != nil {
|
||||
t.Fatalf("write pipeline: %v", err)
|
||||
}
|
||||
if _, err := LoadPipeline(path); err == nil || !strings.Contains(err.Error(), "strict decode failed") {
|
||||
t.Fatalf("LoadPipeline() error = %v, want strict decode failure", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotariusEnabledValidation(t *testing.T) {
|
||||
valid := `notarius:
|
||||
enabled: true
|
||||
config_path: ./notarius.yml
|
||||
pipeline_id: dnd-session
|
||||
timeout: 45m
|
||||
outputs:
|
||||
npc_registry:
|
||||
lane_id: npc-registry
|
||||
media_type: application/json
|
||||
schema_id: notarius.dnd.npc_registry
|
||||
schema_version: v1
|
||||
module_key: dnd/npc-registry
|
||||
`
|
||||
tests := []struct {
|
||||
name string
|
||||
section 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: "normalized key collision",
|
||||
section: strings.Replace(valid, " module_key: dnd/npc-registry\n", ` module_key: dnd/npc-registry
|
||||
" npc_registry ":
|
||||
lane_id: npc-registry-two
|
||||
media_type: application/json
|
||||
schema_id: two
|
||||
schema_version: v1
|
||||
`, 1),
|
||||
wantErr: "normalize to duplicate source",
|
||||
},
|
||||
{
|
||||
name: "duplicate normalized lane",
|
||||
section: strings.Replace(valid, " module_key: dnd/npc-registry\n", ` module_key: dnd/npc-registry
|
||||
spells:
|
||||
lane_id: " npc-registry "
|
||||
media_type: application/json
|
||||
schema_id: two
|
||||
schema_version: v1
|
||||
`, 1),
|
||||
wantErr: "duplicates pipeline.notarius.outputs.npc_registry.lane_id",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pipelinePath, sessionPath := writeConfigFiles(t, testPipelineBaseYAML+"\n"+tt.section, testSessionBaseYAML)
|
||||
cfg, err := Load(pipelinePath, sessionPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
err = Validate(cfg)
|
||||
if tt.wantErr == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
output := cfg.Pipeline.Notarius.Outputs["npc_registry"]
|
||||
if output.LaneID != "npc-registry" || output.ModuleKey != "dnd/npc-registry" {
|
||||
t.Fatalf("normalized output = %#v", output)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("Validate() error = %v, want containing %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractionReferencesRequireDeclaredOutput(t *testing.T) {
|
||||
declared := `notarius:
|
||||
enabled: false
|
||||
outputs:
|
||||
npc_registry: {}
|
||||
`
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "scriptorium declared extraction",
|
||||
body: declared + `scriptorium:
|
||||
artifacts:
|
||||
recap:
|
||||
inputs:
|
||||
npcs:
|
||||
source: narratio.extraction.npc_registry
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "scriptorium unknown extraction",
|
||||
body: declared + `scriptorium:
|
||||
artifacts:
|
||||
recap:
|
||||
inputs:
|
||||
npcs:
|
||||
source: narratio.extraction.unknown
|
||||
`,
|
||||
wantErr: `references unknown extraction output "unknown"`,
|
||||
},
|
||||
{
|
||||
name: "publish declared extraction",
|
||||
body: declared + `publish:
|
||||
outputs:
|
||||
- source: narratio.extraction.npc_registry
|
||||
dest: artifacts/npc-registry.json
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "publish unknown extraction",
|
||||
body: declared + `publish:
|
||||
outputs:
|
||||
- source: narratio.extraction.unknown
|
||||
dest: artifacts/unknown.json
|
||||
`,
|
||||
wantErr: `extraction output "unknown" is not defined`,
|
||||
},
|
||||
{
|
||||
name: "publish lock declared extraction",
|
||||
body: declared + `publish:
|
||||
locks:
|
||||
- source: narratio.extraction.npc_registry
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pipelinePath, sessionPath := writeConfigFiles(t, testPipelineBaseYAML+"\n"+tt.body, testSessionBaseYAML)
|
||||
cfg, err := Load(pipelinePath, sessionPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
err = Validate(cfg)
|
||||
if tt.wantErr == "" {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user