Files
notarius/internal/modules/dnd/normalize/spells/normalizer_test.go

481 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package spells
import (
"context"
"fmt"
"reflect"
"strings"
"testing"
"unicode/utf8"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
spellcatalog "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/spells/catalog"
)
func TestModuleContractAndStrictOptions(t *testing.T) {
if _, err := DecodeOptions(nil); err != nil {
t.Fatalf("DecodeOptions(nil) 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)
}
want := pipeline.ModuleSpec{
Key: Key,
Stage: pipeline.StageNormalize,
Requires: []string{"merged"},
Provides: []string{"normalized"},
ArtifactKind: dnd.SpellListKind,
ReferenceSlots: []contracts.ReferenceSlot{{
Name: spellcatalog.SpellCatalogReferenceSlot,
Description: "Optional canonical spell-name catalog used for normalization and duplicate identity.",
AcceptedMediaTypes: []string{"application/json"},
MaxBytes: 1048576,
}},
}
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
}
registry := pipeline.NewNormalizerRegistry()
if err := Register(registry); err != nil {
t.Fatalf("Register() error = %v, want nil", err)
}
registered, ok := registry.SpecForArtifact(Key, dnd.SpellListKind)
if !ok || !reflect.DeepEqual(registered, want) {
t.Fatalf("registered spec = %#v, ok = %t, want %#v", registered, ok, want)
}
if err := Register(nil); err == nil || !strings.Contains(err.Error(), "normalizer registry") {
t.Fatalf("Register(nil) error = %v, want registry error", err)
}
}
func TestNewBuildsEmbeddedAndOverlayCatalogs(t *testing.T) {
base, err := New(Options{})
if err != nil {
t.Fatalf("New() error = %v, want nil", err)
}
if base.effectiveCatalog.BaseID() != spellcatalog.SRD5E2014ID || base.effectiveCatalog.Digest() == "" || len(base.effectiveCatalog.OverlayIDs()) != 0 {
t.Fatalf("base catalog identity = %#v, want embedded catalog identity", base.effectiveCatalog)
}
if canonical, ok := base.effectiveCatalog.Lookup(" cure wounds "); !ok || canonical != "Cure Wounds" {
t.Fatalf("base catalog lookup = %q, %t, want Cure Wounds", canonical, ok)
}
if canonical, ok := base.effectiveCatalog.Lookup("Arcanist's Magic Aura"); !ok || canonical != "Arcanists Magic Aura" {
t.Fatalf("apostrophe lookup = %q, %t, want canonical curly apostrophe spelling", canonical, ok)
}
overlay, err := New(Options{}, overlayReference())
if err != nil {
t.Fatalf("New(overlay) error = %v, want nil", err)
}
if got := overlay.effectiveCatalog.OverlayIDs(); !reflect.DeepEqual(got, []string{"campaign.example"}) {
t.Fatalf("overlay IDs = %#v, want campaign.example", got)
}
if canonical, ok := overlay.effectiveCatalog.Lookup(" emberfall aegis "); !ok || canonical != "Aegis of Emberfall" {
t.Fatalf("overlay alias lookup = %q, %t, want Aegis of Emberfall", canonical, ok)
}
}
func TestNewRejectsInvalidCatalogReferencesDuringConstruction(t *testing.T) {
invalid := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
spellcatalog.SpellCatalogReferenceSlot: {
Items: []contracts.ReferenceItem{{MediaType: "text/plain", Content: []byte("not a catalog")}},
},
}}
if _, err := New(Options{}, invalid); err == nil || !strings.Contains(err.Error(), "application/json") {
t.Fatalf("New(invalid reference) error = %v, want media type failure", err)
}
tooMany := overlayReference()
slot := tooMany.Slots[spellcatalog.SpellCatalogReferenceSlot]
slot.Items = append(slot.Items, slot.Items[0])
tooMany.Slots[spellcatalog.SpellCatalogReferenceSlot] = slot
if _, err := New(Options{}, tooMany); err == nil || !strings.Contains(err.Error(), "zero or one item") {
t.Fatalf("New(duplicated reference) error = %v, want multiplicity failure", err)
}
}
func TestIdentityAndMetadataAreDefensive(t *testing.T) {
normalizer, err := New(Options{}, overlayReference())
if err != nil {
t.Fatal(err)
}
fingerprints := normalizer.CheckpointFingerprints()
if len(fingerprints) != 1 || fingerprints[0].Name != "effective_catalog" || fingerprints[0].Value != normalizer.effectiveCatalog.Digest() {
t.Fatalf("fingerprints = %#v, want effective catalog fingerprint", fingerprints)
}
fingerprints[0].Name = "changed"
fingerprints[0].Value = "changed"
if got := normalizer.CheckpointFingerprints(); len(got) != 1 || got[0].Name != "effective_catalog" || got[0].Value != normalizer.effectiveCatalog.Digest() {
t.Fatalf("fingerprints were not defensive: %#v", got)
}
metadata := normalizer.ManifestMetadata()
if metadata["catalog_base_id"] != spellcatalog.SRD5E2014ID || metadata["catalog_digest"] != normalizer.effectiveCatalog.Digest() {
t.Fatalf("metadata = %#v, want catalog identity", metadata)
}
metadata["catalog_overlay_ids"].([]string)[0] = "changed"
if got := normalizer.ManifestMetadata()["catalog_overlay_ids"].([]string); !reflect.DeepEqual(got, []string{"campaign.example"}) {
t.Fatalf("metadata overlay IDs were not defensive: %#v", got)
}
}
func TestNormalizeCanonicalizesNamesAndReportsUnresolvedNames(t *testing.T) {
normalizer := newNormalizer(t)
input := dnd.SpellList{SpellCasts: []dnd.SpellCast{
{Spell: " cure wounds "},
{Spell: "Arcanist's Magic Aura"},
{Spell: "Emberfall Aegis"},
{Spell: "Mystery\nSpell\tName"},
{Spell: "Cure Wounds"},
}}
result, err := normalizer.Normalize(context.Background(), normalizeRequest(input))
if err != nil {
t.Fatalf("Normalize() error = %v, want nil", err)
}
wantNames := []string{"Cure Wounds", "Arcanists Magic Aura", "Aegis of Emberfall", "Mystery\nSpell\tName", "Cure Wounds"}
for index, want := range wantNames {
if result.Value.SpellCasts[index].Spell != want {
t.Fatalf("spell[%d] = %q, want %q", index, result.Value.SpellCasts[index].Spell, want)
}
}
if len(result.Warnings) != 4 {
t.Fatalf("warnings = %#v, want four name warnings", result.Warnings)
}
if result.Warnings[0].ReasonCode != ReasonCodeSpellNameCanonicalized || result.Warnings[0].Scope != "spell_casts[0]" || !strings.Contains(result.Warnings[0].Message, "input index 0") {
t.Fatalf("first warning = %#v, want canonicalization warning", result.Warnings[0])
}
if result.Warnings[1].ReasonCode != ReasonCodeSpellNameCanonicalized || result.Warnings[2].ReasonCode != ReasonCodeSpellNameCanonicalized {
t.Fatalf("catalog spelling warnings = %#v", result.Warnings[1:3])
}
if result.Warnings[3].ReasonCode != ReasonCodeSpellNameUnresolved || result.Warnings[3].Scope != "spell_casts[3]" || strings.Contains(result.Warnings[3].Message, "Mystery\nSpell") || !strings.Contains(result.Warnings[3].Message, `Mystery\nSpell\tName`) {
t.Fatalf("unresolved warning = %#v, want quoted control characters", result.Warnings[3])
}
}
func TestNormalizeBoundsUnicodeNamesAndQuotesCanonicalReplacement(t *testing.T) {
longName := strings.Repeat("火", 140)
reference := spellCatalogReference(fmt.Sprintf(`{"schema_version":"notarius.dnd.spell-catalog-overlay.v1","catalogs":[{"id":"campaign.long","ruleset":"dnd-5e-2014","source":{"title":"Private campaign source"},"spells":[{"name":%q,"aliases":["long alias"]}]}]}`, longName))
normalizer, err := New(Options{}, reference)
if err != nil {
t.Fatalf("New() error = %v, want nil", err)
}
result, err := normalizer.Normalize(context.Background(), normalizeRequest(dnd.SpellList{SpellCasts: []dnd.SpellCast{{Spell: "long alias"}}}))
if err != nil {
t.Fatalf("Normalize() error = %v, want nil", err)
}
if len(result.Warnings) != 1 || result.Warnings[0].ReasonCode != ReasonCodeSpellNameCanonicalized {
t.Fatalf("warnings = %#v, want one canonicalization warning", result.Warnings)
}
if !utf8.ValidString(result.Warnings[0].Message) || !strings.Contains(result.Warnings[0].Message, "…") || strings.Contains(result.Warnings[0].Message, longName) {
t.Fatalf("warning = %q, want valid bounded Unicode diagnostic", result.Warnings[0].Message)
}
if got := result.Value.SpellCasts[0].Spell; got != longName {
t.Fatalf("canonical value = %q, want full catalog name", got)
}
}
func TestNormalizeSortsAndDeduplicatesExactSourceReferences(t *testing.T) {
normalizer := newNormalizer(t)
inputRefs := []source.SourceRef{
{SourceID: "source-b", StartUnitID: 4, EndUnitID: 5},
{SourceID: "source-a", StartUnitID: 3, EndUnitID: 4},
{SourceID: "source-a", StartUnitID: 3, EndUnitID: 4},
{SourceID: "source-a", StartUnitID: 1, EndUnitID: 2},
{SourceID: "source-a", StartUnitID: 2, EndUnitID: 3},
{SourceID: "source-a", StartUnitID: 1, EndUnitID: 4},
}
input := dnd.SpellList{SpellCasts: []dnd.SpellCast{{Spell: "Cure Wounds", SourceRefs: inputRefs}}}
result, err := normalizer.Normalize(context.Background(), normalizeRequest(input))
if err != nil {
t.Fatalf("Normalize() error = %v, want nil", err)
}
wantRefs := []source.SourceRef{
{SourceID: "source-a", StartUnitID: 1, EndUnitID: 2},
{SourceID: "source-a", StartUnitID: 1, EndUnitID: 4},
{SourceID: "source-a", StartUnitID: 2, EndUnitID: 3},
{SourceID: "source-a", StartUnitID: 3, EndUnitID: 4},
{SourceID: "source-b", StartUnitID: 4, EndUnitID: 5},
}
if !reflect.DeepEqual(result.Value.SpellCasts[0].SourceRefs, wantRefs) {
t.Fatalf("source refs = %#v, want %#v", result.Value.SpellCasts[0].SourceRefs, wantRefs)
}
if len(result.Warnings) != 1 || result.Warnings[0].ReasonCode != ReasonCodeSourceReferencesNormalized || !strings.Contains(result.Warnings[0].Message, "original count 6") || !strings.Contains(result.Warnings[0].Message, "final count 5") || !strings.Contains(result.Warnings[0].Message, "duplicates removed 1") {
t.Fatalf("warnings = %#v, want source normalization warning", result.Warnings)
}
}
func TestNormalizePreservesNilEmptyAndAdjacentOrOverlappingReferences(t *testing.T) {
normalizer := newNormalizer(t)
input := dnd.SpellList{SpellCasts: []dnd.SpellCast{
{Spell: "Cure Wounds", SourceRefs: nil},
{Spell: "Cure Wounds", SourceRefs: []source.SourceRef{}},
{Spell: "Cure Wounds", SourceRefs: []source.SourceRef{
{SourceID: "source", StartUnitID: 3, EndUnitID: 4},
{SourceID: "source", StartUnitID: 1, EndUnitID: 2},
{SourceID: "source", StartUnitID: 2, EndUnitID: 5},
}},
}}
before := input
before.SpellCasts = append([]dnd.SpellCast(nil), input.SpellCasts...)
before.SpellCasts[2].SourceRefs = append([]source.SourceRef(nil), input.SpellCasts[2].SourceRefs...)
result, err := normalizer.Normalize(context.Background(), normalizeRequest(input))
if err != nil {
t.Fatalf("Normalize() error = %v, want nil", err)
}
if result.Value.SpellCasts[0].SourceRefs != nil || result.Value.SpellCasts[1].SourceRefs == nil {
t.Fatalf("nil/empty source refs were not preserved: %#v", result.Value.SpellCasts)
}
if len(result.Value.SpellCasts[2].SourceRefs) != 3 {
t.Fatalf("adjacent/overlapping references = %#v, want all three retained", result.Value.SpellCasts[2].SourceRefs)
}
if !reflect.DeepEqual(input, before) {
t.Fatalf("Normalize() mutated input: got %#v, before %#v", input, before)
}
result.Value.SpellCasts[2].SourceRefs[0].SourceID = "changed"
if input.SpellCasts[2].SourceRefs[0].SourceID == "changed" {
t.Fatal("normalized references share input storage")
}
}
func TestNormalizeHandlesNilAndCanceledCalls(t *testing.T) {
request := normalizeRequest(dnd.SpellList{})
var normalizer *Normalizer
if _, err := normalizer.Normalize(context.Background(), request); err == nil || !strings.Contains(err.Error(), "normalizer must not be nil") {
t.Fatalf("nil receiver error = %v, want nil receiver error", err)
}
normalizer = newNormalizer(t)
if _, err := normalizer.Normalize(nil, request); err == nil || !strings.Contains(err.Error(), "context must not be nil") {
t.Fatalf("nil context error = %v, want nil context error", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := normalizer.Normalize(ctx, request); err == nil || !strings.Contains(err.Error(), "context error before normalize") {
t.Fatalf("canceled context error = %v, want canceled context error", err)
}
}
func TestNormalizeCollapsesDuplicateGroupsAfterCanonicalization(t *testing.T) {
normalizer := newNormalizer(t)
doc := sourceDocument(6)
firstEvidence := source.SourceRef{SourceID: "source", StartUnitID: 1, EndUnitID: 2}
secondEvidence := source.SourceRef{SourceID: "source", StartUnitID: 3, EndUnitID: 4}
input := dnd.SpellList{SpellCasts: []dnd.SpellCast{
{Caster: " Aria \t", Spell: " cure wounds ", SourceRefs: []source.SourceRef{secondEvidence, firstEvidence, firstEvidence}},
{Caster: "Borin", Spell: "Healing Word", SourceRefs: []source.SourceRef{firstEvidence}},
{Caster: " yle ", Spell: "Cure Wounds", SourceRefs: []source.SourceRef{firstEvidence}},
{Caster: "aria", Spell: " cure wounds ", SourceRefs: []source.SourceRef{firstEvidence, secondEvidence}},
{Caster: "KYLE", Spell: " cure wounds ", SourceRefs: []source.SourceRef{firstEvidence}},
{Caster: " kyle ", Spell: "Cure Wounds", SourceRefs: []source.SourceRef{firstEvidence}},
}}
result, err := normalizer.Normalize(context.Background(), normalizeRequestWithSource(input, doc))
if err != nil {
t.Fatalf("Normalize() error = %v, want nil", err)
}
if len(result.Value.SpellCasts) != 3 {
t.Fatalf("normalized casts = %#v, want first occurrences plus distinct cast", result.Value.SpellCasts)
}
if got := result.Value.SpellCasts[0]; got.Caster != " Aria \t" {
t.Fatalf("retained first cast = %#v, want first occurrence caster unchanged", got)
}
if got := result.Value.SpellCasts[0].SourceRefs; !reflect.DeepEqual(got, []source.SourceRef{firstEvidence, secondEvidence}) {
t.Fatalf("retained first evidence = %#v, want canonical first evidence only", got)
}
if got := result.Value.SpellCasts[2].Caster; got != " yle " {
t.Fatalf("Unicode caster output = %q, want first occurrence text unchanged", got)
}
if len(result.Warnings) != 6 {
t.Fatalf("warnings = %#v, want per-cast warnings followed by two group warnings", result.Warnings)
}
if result.Warnings[0].ReasonCode != ReasonCodeSpellNameCanonicalized || result.Warnings[0].Scope != "spell_casts[0]" {
t.Fatalf("warning[0] = %#v, want input name warning", result.Warnings[0])
}
if result.Warnings[1].ReasonCode != ReasonCodeSourceReferencesNormalized || result.Warnings[1].Scope != "spell_casts[0]" {
t.Fatalf("warning[1] = %#v, want input source warning", result.Warnings[1])
}
if result.Warnings[2].ReasonCode != ReasonCodeSpellNameCanonicalized || result.Warnings[2].Scope != "spell_casts[3]" {
t.Fatalf("warning[2] = %#v, want removed occurrence warning", result.Warnings[2])
}
if result.Warnings[3].ReasonCode != ReasonCodeSpellNameCanonicalized || result.Warnings[3].Scope != "spell_casts[4]" {
t.Fatalf("warning[3] = %#v, want removed occurrence warning", result.Warnings[3])
}
if result.Warnings[4].ReasonCode != ReasonCodeDuplicateSpellCastCollapsed || result.Warnings[4].Scope != "spell_casts[0]" || !strings.Contains(result.Warnings[4].Message, "retained input index 0") || !strings.Contains(result.Warnings[4].Message, "removed input indices [3]") {
t.Fatalf("warning[4] = %#v, want first duplicate group warning", result.Warnings[4])
}
if result.Warnings[5].ReasonCode != ReasonCodeDuplicateSpellCastCollapsed || result.Warnings[5].Scope != "spell_casts[2]" || !strings.Contains(result.Warnings[5].Message, "removed input indices [4, 5]") {
t.Fatalf("warning[5] = %#v, want second duplicate group warning", result.Warnings[5])
}
}
func TestNormalizeKeepsDistinctAndIneligibleCastsSeparate(t *testing.T) {
doc := sourceDocument(6)
ref := source.SourceRef{SourceID: "source", StartUnitID: 1, EndUnitID: 2}
otherRef := source.SourceRef{SourceID: "source", StartUnitID: 3, EndUnitID: 4}
tests := []struct {
name string
variant dnd.SpellCast
}{
{name: "distinct canonical spell", variant: dnd.SpellCast{Spell: "Healing Word", Caster: "Aria", SourceRefs: []source.SourceRef{ref}}},
{name: "distinct caster", variant: dnd.SpellCast{Spell: "Cure Wounds", Caster: "Borin", SourceRefs: []source.SourceRef{ref}}},
{name: "distinct evidence", variant: dnd.SpellCast{Spell: "Cure Wounds", Caster: "Aria", SourceRefs: []source.SourceRef{otherRef}}},
{name: "unknown spell", variant: dnd.SpellCast{Spell: "Unknown Spell", Caster: "Aria", SourceRefs: []source.SourceRef{ref}}},
{name: "empty evidence", variant: dnd.SpellCast{Spell: "Cure Wounds", Caster: "Aria"}},
{name: "invalid evidence", variant: dnd.SpellCast{Spell: "Cure Wounds", Caster: "Aria", SourceRefs: []source.SourceRef{{SourceID: "other", StartUnitID: 1, EndUnitID: 2}}}},
}
base := dnd.SpellCast{Spell: "Cure Wounds", Caster: "Aria", SourceRefs: []source.SourceRef{ref}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := newNormalizer(t).Normalize(context.Background(), normalizeRequestWithSource(dnd.SpellList{SpellCasts: []dnd.SpellCast{base, test.variant}}, doc))
if err != nil {
t.Fatalf("Normalize() error = %v, want nil", err)
}
if len(result.Value.SpellCasts) != 2 {
t.Fatalf("normalized casts = %#v, want both casts retained", result.Value.SpellCasts)
}
for _, warning := range result.Warnings {
if warning.ReasonCode == ReasonCodeDuplicateSpellCastCollapsed {
t.Fatalf("warnings = %#v, want no duplicate collapse", result.Warnings)
}
}
})
}
}
func TestNormalizeDoesNotCollapseAdjacentOrOverlappingEvidence(t *testing.T) {
doc := sourceDocument(8)
input := dnd.SpellList{SpellCasts: []dnd.SpellCast{
{Spell: "Cure Wounds", Caster: "Aria", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 1, EndUnitID: 2}}},
{Spell: "Cure Wounds", Caster: "aria", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 3, EndUnitID: 4}}},
{Spell: "Cure Wounds", Caster: "Aria", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 1, EndUnitID: 3}}},
{Spell: "Cure Wounds", Caster: "aria", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 2, EndUnitID: 4}}},
}}
result, err := newNormalizer(t).Normalize(context.Background(), normalizeRequestWithSource(input, doc))
if err != nil {
t.Fatalf("Normalize() error = %v, want nil", err)
}
if len(result.Value.SpellCasts) != len(input.SpellCasts) {
t.Fatalf("normalized casts = %#v, want adjacent and overlapping evidence retained", result.Value.SpellCasts)
}
for _, warning := range result.Warnings {
if warning.ReasonCode == ReasonCodeDuplicateSpellCastCollapsed {
t.Fatalf("warnings = %#v, want no duplicate collapse", result.Warnings)
}
}
}
func TestNormalizeBoundsDuplicateWarningIndices(t *testing.T) {
doc := sourceDocument(2)
ref := source.SourceRef{SourceID: "source", StartUnitID: 1, EndUnitID: 1}
casts := make([]dnd.SpellCast, 22)
for index := range casts {
casts[index] = dnd.SpellCast{Spell: "Cure Wounds", Caster: "Aria", SourceRefs: []source.SourceRef{ref}}
}
result, err := newNormalizer(t).Normalize(context.Background(), normalizeRequestWithSource(dnd.SpellList{SpellCasts: casts}, doc))
if err != nil {
t.Fatalf("Normalize() error = %v, want nil", err)
}
if len(result.Value.SpellCasts) != 1 || len(result.Warnings) != 1 || result.Warnings[0].ReasonCode != ReasonCodeDuplicateSpellCastCollapsed {
t.Fatalf("result = %#v, warnings = %#v, want one retained cast and one bounded warning", result.Value, result.Warnings)
}
message := result.Warnings[0].Message
if !strings.Contains(message, "removed input indices [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]") || strings.Contains(message, ", 21]") || !strings.Contains(message, "1 additional removed input indices omitted") {
t.Fatalf("warning message = %q, want 20 displayed indices and exact omitted count", message)
}
}
func TestNormalizeIsIdempotentForAlreadyNormalizedInput(t *testing.T) {
doc := sourceDocument(3)
input := dnd.SpellList{SpellCasts: []dnd.SpellCast{{
Spell: "Cure Wounds",
Caster: "Aria",
SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 1, EndUnitID: 2}},
}}}
normalizer := newNormalizer(t)
first, err := normalizer.Normalize(context.Background(), normalizeRequestWithSource(input, doc))
if err != nil {
t.Fatalf("first Normalize() error = %v, want nil", err)
}
second, err := normalizer.Normalize(context.Background(), normalizeRequestWithSource(first.Value, doc))
if err != nil {
t.Fatalf("second Normalize() error = %v, want nil", err)
}
if !reflect.DeepEqual(second.Value, first.Value) || len(first.Warnings) != 0 || len(second.Warnings) != 0 {
t.Fatalf("first = %#v/%#v, second = %#v/%#v, want identical artifacts without mutation warnings", first.Value, first.Warnings, second.Value, second.Warnings)
}
}
func TestNormalizeDuplicateOutputDoesNotShareInputSlices(t *testing.T) {
doc := sourceDocument(3)
ref := source.SourceRef{SourceID: "source", StartUnitID: 1, EndUnitID: 2}
input := dnd.SpellList{SpellCasts: []dnd.SpellCast{
{Spell: "Cure Wounds", Caster: "Aria", SourceRefs: []source.SourceRef{ref}},
{Spell: "Cure Wounds", Caster: "aria", SourceRefs: []source.SourceRef{ref}},
}}
result, err := newNormalizer(t).Normalize(context.Background(), normalizeRequestWithSource(input, doc))
if err != nil {
t.Fatalf("Normalize() error = %v, want nil", err)
}
result.Value.SpellCasts[0].SourceRefs[0].SourceID = "changed"
if input.SpellCasts[0].SourceRefs[0].SourceID != "source" || input.SpellCasts[1].SourceRefs[0].SourceID != "source" {
t.Fatalf("normalized output shares input references: input = %#v", input)
}
}
func newNormalizer(t *testing.T) *Normalizer {
t.Helper()
normalizer, err := New(Options{}, overlayReference())
if err != nil {
t.Fatalf("New() error = %v, want nil", err)
}
return normalizer
}
func normalizeRequest(value dnd.SpellList) contracts.TypedNormalizeRequest[dnd.SpellList] {
return contracts.TypedNormalizeRequest[dnd.SpellList]{
LaneID: "spells",
MergeOutput: contracts.MergeArtifact[dnd.SpellList]{
LaneID: "spells",
MergerKey: "appendorder",
SourceID: "source",
Value: value,
},
}
}
func normalizeRequestWithSource(value dnd.SpellList, doc *source.SourceDocument) contracts.TypedNormalizeRequest[dnd.SpellList] {
request := normalizeRequest(value)
request.Source = doc
return request
}
func sourceDocument(unitCount int) *source.SourceDocument {
units := make([]source.SourceUnit, unitCount)
for index := range units {
units[index] = source.SourceUnit{ID: index + 1}
}
return &source.SourceDocument{ID: "source", Units: units}
}
func overlayReference() contracts.ReferenceSet {
return spellCatalogReference(`{"schema_version":"notarius.dnd.spell-catalog-overlay.v1","catalogs":[{"id":"campaign.example","ruleset":"dnd-5e-2014","source":{"title":"Private campaign source","version":"1","url":"file:///private-source.json","license":"private"},"spells":[{"name":"Aegis of Emberfall","aliases":["Emberfall Aegis"]}]}]}`)
}
func spellCatalogReference(content string) contracts.ReferenceSet {
return contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
spellcatalog.SpellCatalogReferenceSlot: {
Items: []contracts.ReferenceItem{{
SlotName: spellcatalog.SpellCatalogReferenceSlot,
MediaType: "application/json",
Content: []byte(content),
}},
},
}}
}