255 lines
11 KiB
Go
255 lines
11 KiB
Go
package register
|
|
|
|
import (
|
|
"io/fs"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
|
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
|
|
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs"
|
|
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
|
|
)
|
|
|
|
func TestRegisterAddsDNDFamily(t *testing.T) {
|
|
registries := completeRegistries()
|
|
assets := llm.NewAssetRegistry()
|
|
if err := Register(registries, assets); err != nil {
|
|
t.Fatalf("Register() error = %v, want nil", err)
|
|
}
|
|
assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"})
|
|
assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key})
|
|
assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, pipeline.DefaultNormalizeModule})
|
|
assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind})
|
|
assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind})
|
|
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind})
|
|
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(npcnormalize.Key), []contracts.ArtifactKind{dnd.NPCListKind})
|
|
assertContainsKeys(t, "validators", registries.Validators.RegisteredKeys(), []string{
|
|
"extract/dnd/npcs/shape",
|
|
"extract/dnd/npcs/source_refs",
|
|
"extract/dnd/npcs/source_relatedness",
|
|
"normalize/dnd/npcs/identity",
|
|
"extract/dnd/spells/catalog",
|
|
"extract/dnd/spells/shape",
|
|
"extract/dnd/spells/source_refs",
|
|
"extract/dnd/spells/source_relatedness",
|
|
"generic/always_accept",
|
|
"generic/always_reject",
|
|
})
|
|
wantChain := []pipeline.ModuleBinding{
|
|
pipeline.Binding("generic/valid_json"),
|
|
pipeline.Binding("generic/valid_json_schema"),
|
|
pipeline.Binding("extract/dnd/spells/shape"),
|
|
pipeline.Binding("extract/dnd/spells/catalog"),
|
|
pipeline.Binding("extract/dnd/spells/source_refs"),
|
|
pipeline.Binding("extract/dnd/spells/source_relatedness"),
|
|
}
|
|
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, spells.Key); !reflect.DeepEqual(got, wantChain) {
|
|
t.Fatalf("spell validator chain = %#v, want %#v", got, wantChain)
|
|
}
|
|
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, spellnormalize.Key); !reflect.DeepEqual(got, wantChain) {
|
|
t.Fatalf("spell normalize validator chain = %#v, want %#v", got, wantChain)
|
|
}
|
|
npcExtractChain := []pipeline.ModuleBinding{
|
|
pipeline.Binding("generic/valid_json"),
|
|
pipeline.Binding("generic/valid_json_schema"),
|
|
pipeline.Binding("extract/dnd/npcs/shape"),
|
|
pipeline.Binding("extract/dnd/npcs/source_refs"),
|
|
pipeline.Binding("extract/dnd/npcs/source_relatedness"),
|
|
}
|
|
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, npcextract.Key); !reflect.DeepEqual(got, npcExtractChain) {
|
|
t.Fatalf("NPC extract validator chain = %#v, want %#v", got, npcExtractChain)
|
|
}
|
|
npcNormalizeChain := []pipeline.ModuleBinding{
|
|
pipeline.Binding("generic/valid_json"),
|
|
pipeline.Binding("generic/valid_json_schema"),
|
|
pipeline.Binding("extract/dnd/npcs/shape"),
|
|
pipeline.Binding("normalize/dnd/npcs/identity"),
|
|
pipeline.Binding("extract/dnd/npcs/source_refs"),
|
|
pipeline.Binding("extract/dnd/npcs/source_relatedness"),
|
|
}
|
|
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, npcnormalize.Key); !reflect.DeepEqual(got, npcNormalizeChain) {
|
|
t.Fatalf("NPC normalize validator chain = %#v, want %#v", got, npcNormalizeChain)
|
|
}
|
|
if got := registries.ValidatorChains.Validators(pipeline.StageMerge, npcextract.Key); got != nil {
|
|
t.Fatalf("NPC merge validator chain = %#v, want absent", got)
|
|
}
|
|
assertAssetNamesContain(t, assets.PromptFS, []string{
|
|
"dnd.scenes/dnd.scenes.yaml",
|
|
"dnd.scenes/instructions.md",
|
|
"dnd.scenes/sharedassets/common-dnd-references.md",
|
|
"dnd.scenes/sharedassets/common-dnd-system.md",
|
|
"dnd.scenes/sharedassets/common-dnd-transcript.md",
|
|
"dnd.scenes/task.md",
|
|
"dnd.spells/dnd.spells.yaml",
|
|
"dnd.spells/instructions.md",
|
|
"dnd.spells/sharedassets/common-dnd-references.md",
|
|
"dnd.spells/sharedassets/common-dnd-system.md",
|
|
"dnd.spells/sharedassets/common-dnd-transcript.md",
|
|
"dnd.spells/task.md",
|
|
"dnd.npcs/dnd.npcs.yaml",
|
|
"dnd.npcs/instructions.md",
|
|
"dnd.npcs/sharedassets/common-dnd-references.md",
|
|
"dnd.npcs/sharedassets/common-dnd-system.md",
|
|
"dnd.npcs/sharedassets/common-dnd-transcript.md",
|
|
"dnd.npcs/task.md",
|
|
})
|
|
assertAssetNamesContain(t, assets.SchemaFS, []string{
|
|
"dnd_scenes.v1.json",
|
|
"dnd_spells_llm.v1.json",
|
|
"dnd_npcs_llm.v1.json",
|
|
})
|
|
if spec, ok := registries.Chunkers.Spec("dnd/scenes"); !ok || spec.Key != "dnd/scenes" {
|
|
t.Fatalf("scene chunker spec = %#v, present = %t; want family-owned spec", spec, ok)
|
|
}
|
|
if spec, ok := registries.Extractors.Spec(spells.Key); !ok || spec.ArtifactKind != dnd.SpellListKind {
|
|
t.Fatalf("spell extractor spec = %#v, present = %t; want dnd spell-list artifact", spec, ok)
|
|
}
|
|
if spec, ok := registries.Normalizers.Spec(spellnormalize.Key); !ok || spec.ArtifactKind != dnd.SpellListKind || spec.Stage != pipeline.StageNormalize {
|
|
t.Fatalf("spell normalizer spec = %#v, present = %t; want dnd spell-list artifact", spec, ok)
|
|
}
|
|
if spec, ok := registries.Extractors.Spec(npcextract.Key); !ok || spec.ArtifactKind != dnd.NPCListKind {
|
|
t.Fatalf("NPC extractor spec = %#v, present = %t; want dnd NPC-list artifact", spec, ok)
|
|
}
|
|
if spec, ok := registries.Normalizers.Spec(npcnormalize.Key); !ok || spec.ArtifactKind != dnd.NPCListKind || spec.Stage != pipeline.StageNormalize {
|
|
t.Fatalf("NPC normalizer spec = %#v, present = %t; want dnd NPC-list artifact", spec, ok)
|
|
}
|
|
}
|
|
|
|
func TestAppendNPCListsPreservesOrderAndArrayPresence(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in []dnd.NPCList
|
|
want dnd.NPCList
|
|
}{
|
|
{name: "no values", in: nil, want: dnd.NPCList{}},
|
|
{name: "nil values", in: []dnd.NPCList{{}, {}}, want: dnd.NPCList{}},
|
|
{name: "present empty", in: []dnd.NPCList{{NPCs: []dnd.NPC{}}}, want: dnd.NPCList{NPCs: []dnd.NPC{}}},
|
|
{name: "ordered values", in: []dnd.NPCList{{NPCs: []dnd.NPC{{Name: "first"}}}, {NPCs: []dnd.NPC{{Name: "second"}}}}, want: dnd.NPCList{NPCs: []dnd.NPC{{Name: "first"}, {Name: "second"}}}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := appendNPCLists(tt.in)
|
|
if err != nil || !reflect.DeepEqual(got, tt.want) {
|
|
t.Fatalf("appendNPCLists() = %#v, error = %v, want %#v", got, err, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegisterRejectsMissingDNDDependenciesBeforeMutation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
remove func(*pipeline.Registries, **llm.AssetRegistry)
|
|
wantErr string
|
|
}{
|
|
{name: "chunkers", remove: func(r *pipeline.Registries, _ **llm.AssetRegistry) { r.Chunkers = nil }, wantErr: "chunker registry"},
|
|
{name: "artifact codecs", remove: func(r *pipeline.Registries, _ **llm.AssetRegistry) { r.ArtifactCodecs = nil }, wantErr: "artifact codec registry"},
|
|
{name: "extractors", remove: func(r *pipeline.Registries, _ **llm.AssetRegistry) { r.Extractors = nil }, wantErr: "extractor registry"},
|
|
{name: "mergers", remove: func(r *pipeline.Registries, _ **llm.AssetRegistry) { r.Mergers = nil }, wantErr: "merger registry"},
|
|
{name: "normalizers", remove: func(r *pipeline.Registries, _ **llm.AssetRegistry) { r.Normalizers = nil }, wantErr: "normalizer registry"},
|
|
{name: "validators", remove: func(r *pipeline.Registries, _ **llm.AssetRegistry) { r.Validators = nil }, wantErr: "validator registry"},
|
|
{name: "validator chains", remove: func(r *pipeline.Registries, _ **llm.AssetRegistry) { r.ValidatorChains = nil }, wantErr: "validator chain registry"},
|
|
{name: "assets", remove: func(_ *pipeline.Registries, assets **llm.AssetRegistry) { *assets = nil }, wantErr: "asset registry"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
registries := completeRegistries()
|
|
assets := llm.NewAssetRegistry()
|
|
test.remove(®istries, &assets)
|
|
err := Register(registries, assets)
|
|
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
|
|
t.Fatalf("Register() error = %v, want %q", err, test.wantErr)
|
|
}
|
|
if got := registries.Chunkers; got != nil && len(got.RegisteredKeys()) != 0 {
|
|
t.Fatalf("chunker keys = %#v, want validation before mutation", got.RegisteredKeys())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegisterReportsDuplicateDNDRegistration(t *testing.T) {
|
|
registries := completeRegistries()
|
|
assets := llm.NewAssetRegistry()
|
|
if err := Register(registries, assets); err != nil {
|
|
t.Fatalf("first Register() error = %v, want nil", err)
|
|
}
|
|
err := Register(registries, assets)
|
|
if err == nil || !strings.Contains(err.Error(), "register dnd spells codec") || !strings.Contains(err.Error(), "already registered") {
|
|
t.Fatalf("second Register() error = %v, want contextual duplicate error", err)
|
|
}
|
|
}
|
|
|
|
func completeRegistries() pipeline.Registries {
|
|
return pipeline.Registries{
|
|
Inputs: pipeline.NewInputAdapterRegistry(),
|
|
Chunkers: pipeline.NewChunkerRegistry(),
|
|
ArtifactCodecs: pipeline.NewArtifactCodecRegistry(),
|
|
Extractors: pipeline.NewExtractorRegistry(),
|
|
Mergers: pipeline.NewMergerRegistry(),
|
|
Normalizers: pipeline.NewNormalizerRegistry(),
|
|
Validators: pipeline.NewValidatorRegistry(),
|
|
ValidatorChains: pipeline.NewValidatorChainRegistry(),
|
|
Outputs: pipeline.NewOutputEncoderRegistry(),
|
|
}
|
|
}
|
|
|
|
func assertContainsKeys(t *testing.T, name string, got, want []string) {
|
|
t.Helper()
|
|
seen := make(map[string]struct{}, len(got))
|
|
for _, key := range got {
|
|
seen[key] = struct{}{}
|
|
}
|
|
for _, key := range want {
|
|
if _, ok := seen[key]; !ok {
|
|
t.Fatalf("%s keys = %#v, want required key %q", name, got, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertContainsArtifactKinds(t *testing.T, got, want []contracts.ArtifactKind) {
|
|
t.Helper()
|
|
seen := make(map[contracts.ArtifactKind]struct{}, len(got))
|
|
for _, kind := range got {
|
|
seen[kind] = struct{}{}
|
|
}
|
|
for _, kind := range want {
|
|
if _, ok := seen[kind]; !ok {
|
|
t.Fatalf("artifact codec kinds = %#v, want required kind %q", got, kind)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertAssetNamesContain(t *testing.T, getFS func() (fs.FS, error), want []string) {
|
|
t.Helper()
|
|
fSys, err := getFS()
|
|
if err != nil {
|
|
t.Fatalf("asset filesystem error = %v, want nil", err)
|
|
}
|
|
var got []string
|
|
if err := fs.WalkDir(fSys, ".", func(path string, entry fs.DirEntry, err error) error {
|
|
if err == nil && !entry.IsDir() {
|
|
got = append(got, path)
|
|
}
|
|
return err
|
|
}); err != nil {
|
|
t.Fatalf("walk assets: %v", err)
|
|
}
|
|
sort.Strings(got)
|
|
seen := make(map[string]struct{}, len(got))
|
|
for _, name := range got {
|
|
seen[name] = struct{}{}
|
|
}
|
|
for _, name := range want {
|
|
if _, ok := seen[name]; !ok {
|
|
t.Fatalf("asset names = %#v, want required asset %q", got, name)
|
|
}
|
|
}
|
|
}
|