package spells import ( "reflect" "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" ) func TestNewRequiresLLMClientAndReturnsExtractor(t *testing.T) { if _, err := New(nil, Options{}); err == nil || !strings.Contains(err.Error(), "LLM client") { t.Fatalf("New(nil) error = %v, want LLM client error", err) } extractor := newExtractor(t, &fakeSpellsLLMClient{}) if extractor.Key() != Key { t.Fatalf("extractor.Key() = %q, want %q", extractor.Key(), Key) } } func TestModuleSpec(t *testing.T) { got := ModuleSpec() want := pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageExtract, Requires: []string{ "chunks", "source.transcript", }, Provides: []string{ "dnd.spell_casts", }, ArtifactKind: dnd.SpellListKind, ReferenceSlots: []contracts.ReferenceSlot{ { Name: "glossary", Description: "Optional campaign glossary reference material used only for disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}, }, { Name: "party", Description: "Optional party roster reference material used only for disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}, }, { Name: "players", Description: "Optional player list reference material used only for disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}, }, { Name: "roster", Description: "Deprecated alias for party roster reference material used only for disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}, }, }, } if !reflect.DeepEqual(got, want) { t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) } got.Requires[0] = "changed" got.Provides[0] = "changed" got.ReferenceSlots[0].AcceptedMediaTypes[0] = "changed" again := ModuleSpec() if !reflect.DeepEqual(again, want) { t.Fatalf("ModuleSpec() after caller mutation = %#v, want %#v", again, want) } } func TestRegisterMakesExtractorBuildable(t *testing.T) { registry := pipeline.NewExtractorRegistry() if err := Register(registry); err != nil { t.Fatalf("Register() error = %v, want nil", err) } if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil || !strings.Contains(err.Error(), "unknown option") { t.Fatalf("DecodeOptions() error = %v, want unknown option error", err) } } func TestRegisterStoresModuleSpec(t *testing.T) { registry := pipeline.NewExtractorRegistry() if err := Register(registry); err != nil { t.Fatalf("Register() error = %v, want nil", err) } got, ok := registry.Spec(Key) if !ok { t.Fatal("Spec() ok = false, want true") } want := ModuleSpec() if !reflect.DeepEqual(got, want) { t.Fatalf("Spec() = %#v, want %#v", got, want) } } func TestRuntimeReferenceSlotsMatchModuleSpec(t *testing.T) { extractor := newExtractor(t, &fakeSpellsLLMClient{}) spec := ModuleSpec() if !reflect.DeepEqual(extractor.ReferenceSlots(), spec.ReferenceSlots) { t.Fatalf("ReferenceSlots() = %#v, want spec slots %#v", extractor.ReferenceSlots(), spec.ReferenceSlots) } } func TestRegisterNilRegistryReturnsError(t *testing.T) { err := Register(nil) if err == nil { t.Fatal("Register(nil) error = nil, want error") } if !strings.Contains(err.Error(), "extractor registry") { t.Fatalf("Register(nil) error = %q, want registry context", err.Error()) } }