Add Notarius reference configuration vocabulary

This commit is contained in:
2026-08-29 14:55:24 +00:00
parent 42ed81cbe1
commit 5831c0c9e6
12 changed files with 476 additions and 42 deletions

View File

@@ -1,6 +1,7 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
@@ -108,6 +109,7 @@ func TestNotariusStrictYAML(t *testing.T) {
{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"},
{name: "duplicate reference selector", yaml: "notarius:\n references:\n party: narratio.input.party\n party: narratio.input.players\n"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -122,6 +124,134 @@ func TestNotariusStrictYAML(t *testing.T) {
}
}
func TestNotariusReferenceValidationAndNormalization(t *testing.T) {
cfg := validNotariusConfig()
cfg.References = map[string]string{
" party ": " narratio.input.party ",
" chunk . players ": "narratio.input.players",
" npc-registry . extract . glossary ": "narratio.input.glossary",
"spells": "narratio.input.spell_catalog",
}
if err := validateNotarius(cfg, nil); err != nil {
t.Fatalf("validateNotarius() error = %v", err)
}
want := map[string]string{
"party": "narratio.input.party",
"chunk.players": "narratio.input.players",
"npc-registry.extract.glossary": "narratio.input.glossary",
"spells": "narratio.input.spell_catalog",
}
if len(cfg.References) != len(want) {
t.Fatalf("normalized references = %#v, want %#v", cfg.References, want)
}
for selector, source := range want {
if cfg.References[selector] != source {
t.Fatalf("references[%q] = %q, want %q", selector, cfg.References[selector], source)
}
}
}
func TestNotariusReferenceValidationRejectsInvalidBindings(t *testing.T) {
tests := []struct {
name string
references map[string]string
wantErr string
}{
{name: "empty selector", references: map[string]string{" ": "narratio.input.party"}, wantErr: "selector is required"},
{name: "equals in selector", references: map[string]string{"party=x": "narratio.input.party"}, wantErr: "must not contain"},
{name: "invalid stage", references: map[string]string{"lane.prepare.party": "narratio.input.party"}, wantErr: "middle component"},
{name: "empty source", references: map[string]string{"party": " "}, wantErr: "source is required"},
{name: "unsupported source", references: map[string]string{"party": "narratio.input.unknown"}, wantErr: "not a supported prepared input source"},
{
name: "normalized collision",
references: map[string]string{
"chunk.party": "narratio.input.party",
" chunk . party ": "narratio.input.players",
},
wantErr: "normalize to",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := validNotariusConfig()
cfg.References = tt.references
err := validateNotarius(cfg, nil)
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("validateNotarius() error = %v, want containing %q", err, tt.wantErr)
}
})
}
}
func TestNotariusReferenceLimit(t *testing.T) {
for _, count := range []int{MaxNotariusReferenceBindings, MaxNotariusReferenceBindings + 1} {
t.Run(fmt.Sprintf("count_%d", count), func(t *testing.T) {
cfg := validNotariusConfig()
cfg.References = make(map[string]string, count)
for i := 0; i < count; i++ {
cfg.References[fmt.Sprintf("lane-%03d.party", i)] = "narratio.input.party"
}
err := validateNotarius(cfg, nil)
if count == MaxNotariusReferenceBindings {
if err != nil {
t.Fatalf("validateNotarius() at limit error = %v", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), "at most 256 bindings") {
t.Fatalf("validateNotarius() above limit error = %v", err)
}
})
}
}
func TestNotariusNilAndEmptyReferencesAreValid(t *testing.T) {
for _, references := range []map[string]string{nil, {}} {
cfg := validNotariusConfig()
cfg.References = references
if err := validateNotarius(cfg, nil); err != nil {
t.Fatalf("validateNotarius(%#v) error = %v", references, err)
}
}
}
func TestNotariusSpellCatalogReferenceRequiresEffectiveInput(t *testing.T) {
cfg := loadedValidConfig(t)
cfg.Pipeline.Notarius = validNotariusConfig()
cfg.Pipeline.Notarius.References = map[string]string{"spells": "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)
}
cfg.StableInputs.SpellCatalogFile = ResolvedInputFile{Path: "./spells.json", Source: "campaign_config"}
if err := Validate(cfg); err != nil {
t.Fatalf("Validate() with spell catalog error = %v", err)
}
}
func validNotariusConfig() *NotariusConfig {
return &NotariusConfig{
Enabled: true,
Binary: "notarius",
ConfigPath: "./notarius.yml",
PipelineID: "dnd-session",
Timeout: "45m",
WorkingDirectory: ".",
Outputs: map[string]NotariusOutputConfig{
"npc_registry": {
LaneID: "npc-registry",
MediaType: "application/json",
SchemaID: "notarius.dnd.npc_registry",
SchemaVersion: "v1",
},
},
}
}
func TestNotariusEnabledValidation(t *testing.T) {
valid := `notarius:
enabled: true