2026-07-20 19:10:44 +00:00
package catalog
import (
"encoding/json"
"reflect"
"sort"
"strings"
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)
func TestResolveEffectiveCatalogBaseAndImmutability ( t * testing . T ) {
effective , err := ResolveEffectiveCatalog ( contracts . ReferenceSet { } )
if err != nil {
t . Fatal ( err )
}
if effective . BaseID ( ) != SRD5E2014ID || effective . Ruleset ( ) != SRD5E2014Ruleset {
t . Fatalf ( "identity = %q/%q" , effective . BaseID ( ) , effective . Ruleset ( ) )
}
if got := effective . OverlayIDs ( ) ; len ( got ) != 0 {
t . Fatalf ( "overlay IDs = %#v" , got )
}
names := effective . CanonicalNames ( )
if len ( names ) != 319 || ! sort . StringsAreSorted ( names ) {
t . Fatalf ( "canonical names count/order = %d/%t" , len ( names ) , sort . StringsAreSorted ( names ) )
}
if got , ok := effective . Lookup ( " HUNTER'S MARK " ) ; ! ok || got != "Hunter’ s Mark" {
t . Fatalf ( "Hunter's Mark lookup = %q, present=%t" , got , ok )
}
if got , ok := effective . Lookup ( "Definitely Not A Spell" ) ; ok || got != "" {
t . Fatalf ( "unknown lookup = %q, present=%t" , got , ok )
}
if ! strings . HasPrefix ( effective . Digest ( ) , "sha256:" ) {
t . Fatalf ( "digest = %q" , effective . Digest ( ) )
}
names [ 0 ] = "changed"
ids := effective . OverlayIDs ( )
ids = append ( ids , "changed" )
again , err := ResolveEffectiveCatalog ( contracts . ReferenceSet { } )
if err != nil {
t . Fatal ( err )
}
if reflect . DeepEqual ( names , again . CanonicalNames ( ) ) || len ( again . OverlayIDs ( ) ) != 0 {
t . Fatal ( "effective catalog exposed mutable result storage" )
}
}
func TestResolveEffectiveCatalogAddsAndAugmentsSpells ( t * testing . T ) {
overlay := testOverlayJSON ( t , testOverlayCatalog (
"campaign.example" ,
testOverlaySpell ( "Aegis of Emberfall" , "Emberfall Aegis" ) ,
testOverlaySpell ( "Cure Wounds" , "Healing Touch" ) ,
) )
effective , err := ResolveEffectiveCatalog ( overlayReference ( [ ] byte ( overlay ) , "application/json" ) )
if err != nil {
t . Fatal ( err )
}
if got := effective . OverlayIDs ( ) ; ! reflect . DeepEqual ( got , [ ] string { "campaign.example" } ) {
t . Fatalf ( "overlay IDs = %#v" , got )
}
if len ( effective . CanonicalNames ( ) ) != 320 {
t . Fatalf ( "canonical name count = %d, want 320" , len ( effective . CanonicalNames ( ) ) )
}
if got , ok := effective . Lookup ( "aegis of emberfall" ) ; ! ok || got != "Aegis of Emberfall" {
t . Fatalf ( "new spell lookup = %q, present=%t" , got , ok )
}
if got , ok := effective . Lookup ( "Emberfall Aegis" ) ; ! ok || got != "Aegis of Emberfall" {
t . Fatalf ( "new alias lookup = %q, present=%t" , got , ok )
}
if got , ok := effective . Lookup ( "Healing Touch" ) ; ! ok || got != "Cure Wounds" {
t . Fatalf ( "augmentation alias lookup = %q, present=%t" , got , ok )
}
for _ , name := range effective . CanonicalNames ( ) {
if name == "Emberfall Aegis" || name == "Healing Touch" {
t . Fatalf ( "alias %q was exposed as a canonical name" , name )
}
}
}
2026-08-09 02:17:15 +00:00
func TestEffectiveCatalogPromptSpellsIncludeSortedAliasesWithoutProvenance ( t * testing . T ) {
overlay := testOverlayJSON ( t , testOverlayCatalog (
"campaign.example" ,
testOverlaySpell ( "Aegis of Emberfall" , "Z Emberfall" , "Emberfall Aegis" , "emberfall aegis" ) ,
testOverlaySpell ( "Cure Wounds" , "Healing Touch" ) ,
) )
effective , err := ResolveEffectiveCatalog ( overlayReference ( [ ] byte ( overlay ) , "application/json" ) )
if err != nil {
t . Fatal ( err )
}
spells := effective . PromptSpells ( )
if len ( spells ) != len ( effective . CanonicalNames ( ) ) {
t . Fatalf ( "prompt spell count = %d, want %d" , len ( spells ) , len ( effective . CanonicalNames ( ) ) )
}
for index := 1 ; index < len ( spells ) ; index ++ {
if spells [ index - 1 ] . CanonicalName > spells [ index ] . CanonicalName {
t . Fatalf ( "prompt spells are not sorted: %q before %q" , spells [ index - 1 ] . CanonicalName , spells [ index ] . CanonicalName )
}
}
aliases := make ( map [ string ] [ ] string , len ( spells ) )
for _ , spell := range spells {
aliases [ spell . CanonicalName ] = spell . Aliases
if ! sort . StringsAreSorted ( spell . Aliases ) {
t . Fatalf ( "aliases for %q are not sorted: %#v" , spell . CanonicalName , spell . Aliases )
}
}
if got := aliases [ "Aegis of Emberfall" ] ; ! reflect . DeepEqual ( got , [ ] string { "Emberfall Aegis" , "Z Emberfall" } ) {
t . Fatalf ( "Aegis aliases = %#v, want normalized aliases once" , got )
}
if got := aliases [ "Cure Wounds" ] ; ! reflect . DeepEqual ( got , [ ] string { "Healing Touch" } ) {
t . Fatalf ( "Cure Wounds aliases = %#v" , got )
}
encoded , err := json . Marshal ( spells )
if err != nil {
t . Fatal ( err )
}
if strings . Contains ( string ( encoded ) , ` "aliases":null ` ) {
t . Fatalf ( "prompt projection encoded missing aliases as null: %s" , encoded )
}
for _ , forbidden := range [ ] string { "campaign.example" , "spells" , "source" , "license" , "ruleset" , "provenance" } {
if strings . Contains ( string ( encoded ) , forbidden ) {
t . Fatalf ( "prompt projection leaked %q: %s" , forbidden , encoded )
}
}
for index := range spells {
if spells [ index ] . CanonicalName == "Aegis of Emberfall" {
spells [ index ] . Aliases [ 0 ] = "changed"
break
}
}
for _ , spell := range effective . PromptSpells ( ) {
if spell . CanonicalName == "Aegis of Emberfall" && spell . Aliases [ 0 ] == "changed" {
t . Fatal ( "effective catalog exposed mutable prompt projection storage" )
}
}
}
2026-07-20 19:10:44 +00:00
func TestResolveEffectiveCatalogTreatsRepeatedAliasesAsIdempotent ( t * testing . T ) {
single := testOverlayJSON ( t , testOverlayCatalog (
"campaign.example" ,
testOverlaySpell ( "Aegis of Emberfall" , "Emberfall Aegis" ) ,
) )
repeated := testOverlayJSON ( t , testOverlayCatalog (
"campaign.example" ,
testOverlaySpell ( "Aegis of Emberfall" , "Emberfall Aegis" ) ,
testOverlaySpell ( "Aegis of Emberfall" , "emberfall aegis" ) ,
) )
first , err := ResolveEffectiveCatalog ( overlayReference ( [ ] byte ( single ) , "application/json" ) )
if err != nil {
t . Fatal ( err )
}
second , err := ResolveEffectiveCatalog ( overlayReference ( [ ] byte ( repeated ) , "application/json" ) )
if err != nil {
t . Fatal ( err )
}
if first . Digest ( ) != second . Digest ( ) || ! reflect . DeepEqual ( first . CanonicalNames ( ) , second . CanonicalNames ( ) ) {
t . Fatalf ( "repeated alias changed effective result: digest=%q/%q" , first . Digest ( ) , second . Digest ( ) )
}
}
func TestResolveEffectiveCatalogDigestIgnoresOrderingAndFormatting ( t * testing . T ) {
first := ` { "schema_version":"notarius.dnd.spell-catalog-overlay.v1","catalogs":[ { "id":"campaign.z","ruleset":"dnd-5e-2014","source": { "title":"Z spells","version":"1","url":"","license":""},"spells":[ { "name":"Aegis of Emberfall","aliases":["Z Alias","Another Alias"]}]}, { "id":"campaign.a","ruleset":"dnd-5e-2014","source": { "title":"A spells","version":"1","url":"","license":""},"spells":[ { "name":"Cure Wounds","aliases":["Healing Touch","Cure Mend"]}]}]} `
second := ` {
"catalogs" : [
{ "source" : { "license" : "" , "url" : "" , "version" : "1" , "title" : "A spells" } , "spells" : [ { "aliases" : [ "Cure Mend" , "Healing Touch" ] , "name" : "Cure Wounds" } ] , "ruleset" : "dnd-5e-2014" , "id" : "campaign.a" } ,
{ "spells" : [ { "aliases" : [ "Another Alias" , "Z Alias" ] , "name" : "Aegis of Emberfall" } ] , "source" : { "title" : "Z spells" , "version" : "1" , "url" : "" , "license" : "" } , "ruleset" : "dnd-5e-2014" , "id" : "campaign.z" }
] ,
"schema_version" : "notarius.dnd.spell-catalog-overlay.v1"
} `
firstCatalog , err := ResolveEffectiveCatalog ( overlayReference ( [ ] byte ( first ) , "application/json" ) )
if err != nil {
t . Fatal ( err )
}
secondCatalog , err := ResolveEffectiveCatalog ( overlayReference ( [ ] byte ( second ) , "application/json" ) )
if err != nil {
t . Fatal ( err )
}
if ! reflect . DeepEqual ( firstCatalog . CanonicalNames ( ) , secondCatalog . CanonicalNames ( ) ) || firstCatalog . Digest ( ) != secondCatalog . Digest ( ) {
t . Fatalf ( "reordered overlays changed effective result: names=%t digest=%q/%q" , reflect . DeepEqual ( firstCatalog . CanonicalNames ( ) , secondCatalog . CanonicalNames ( ) ) , firstCatalog . Digest ( ) , secondCatalog . Digest ( ) )
}
}
func TestResolveEffectiveCatalogRejectsInvalidOverlays ( t * testing . T ) {
valid := validOverlayJSON ( )
tests := [ ] struct {
name string
content string
} {
{ name : "wrong schema version" , content : strings . Replace ( valid , "notarius.dnd.spell-catalog-overlay.v1" , "notarius.dnd.spell-catalog-overlay.v2" , 1 ) } ,
{ name : "empty catalogs" , content : ` { "schema_version":"notarius.dnd.spell-catalog-overlay.v1","catalogs":[]} ` } ,
{ name : "duplicate IDs" , content : testOverlayJSON ( t , testOverlayCatalog ( "campaign.example" , testOverlaySpell ( "Aegis of Emberfall" ) ) , testOverlayCatalog ( "campaign.example" , testOverlaySpell ( "Another Spell" ) ) ) } ,
{ name : "wrong ruleset" , content : strings . Replace ( valid , ` "ruleset":"dnd-5e-2014" ` , ` "ruleset":"dnd-5e-other" ` , 1 ) } ,
{ name : "empty ID" , content : strings . Replace ( valid , ` "id":"campaign.example" ` , ` "id":"" ` , 1 ) } ,
{ name : "empty source title" , content : strings . Replace ( valid , ` "title":"Example campaign spells" ` , ` "title":"" ` , 1 ) } ,
{ name : "empty spells" , content : testOverlayJSON ( t , testOverlayCatalog ( "campaign.example" ) ) } ,
{ name : "empty spell name" , content : strings . Replace ( valid , ` "name":"Aegis of Emberfall" ` , ` "name":"" ` , 1 ) } ,
{ name : "null aliases" , content : strings . Replace ( valid , ` "aliases":["Emberfall Aegis"] ` , ` "aliases":null ` , 1 ) } ,
{ name : "unknown bundle field" , content : strings . Replace ( valid , ` "schema_version":"notarius.dnd.spell-catalog-overlay.v1","catalogs" ` , ` "schema_version":"notarius.dnd.spell-catalog-overlay.v1","unexpected":true,"catalogs" ` , 1 ) } ,
{ name : "unknown catalog field" , content : strings . Replace ( valid , ` "id":"campaign.example","ruleset" ` , ` "id":"campaign.example","unexpected":true,"ruleset" ` , 1 ) } ,
{ name : "unknown source field" , content : strings . Replace ( valid , ` "title":"Example campaign spells","version" ` , ` "title":"Example campaign spells","unexpected":true,"version" ` , 1 ) } ,
{ name : "unknown spell field" , content : strings . Replace ( valid , ` "name":"Aegis of Emberfall","aliases" ` , ` "name":"Aegis of Emberfall","unexpected":true,"aliases" ` , 1 ) } ,
{ name : "trailing JSON" , content : valid + ` { } ` } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
if _ , err := ResolveEffectiveCatalog ( overlayReference ( [ ] byte ( test . content ) , "application/json" ) ) ; err == nil {
t . Fatal ( "invalid overlay was accepted" )
}
} )
}
}
func TestResolveEffectiveCatalogRejectsReferenceMultiplicityAndMediaType ( t * testing . T ) {
valid := [ ] byte ( validOverlayJSON ( ) )
tests := [ ] struct {
name string
refs contracts . ReferenceSet
} {
{
name : "multiple items" ,
refs : contracts . ReferenceSet { Slots : map [ string ] contracts . ResolvedReferenceSlot {
SpellCatalogReferenceSlot : { Items : [ ] contracts . ReferenceItem {
{ MediaType : "application/json" , Content : valid } ,
{ MediaType : "application/json" , Content : valid } ,
} } ,
} } ,
} ,
{
name : "non JSON item" ,
refs : overlayReference ( valid , "text/plain" ) ,
} ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
if _ , err := ResolveEffectiveCatalog ( test . refs ) ; err == nil {
t . Fatal ( "invalid reference set was accepted" )
}
} )
}
for _ , refs := range [ ] contracts . ReferenceSet {
{ } , { Slots : map [ string ] contracts . ResolvedReferenceSlot { SpellCatalogReferenceSlot : { } } } ,
} {
if effective , err := ResolveEffectiveCatalog ( refs ) ; err != nil || len ( effective . CanonicalNames ( ) ) != 319 {
t . Fatalf ( "missing overlay should use base catalog: effective=%#v err=%v" , effective , err )
}
}
}
func TestResolveEffectiveCatalogRejectsCrossSpellCollisions ( t * testing . T ) {
tests := [ ] struct {
name string
spells [ ] overlaySpell
content string
} {
{
name : "same key display conflict with embedded canonical" ,
spells : [ ] overlaySpell { testOverlaySpell ( "cure wounds" ) } ,
} ,
{
name : "same key display conflict between canonical names" ,
spells : [ ] overlaySpell { testOverlaySpell ( "Moon Beam" ) , testOverlaySpell ( "moon beam" ) } ,
} ,
{
name : "canonical versus alias" ,
spells : [ ] overlaySpell { testOverlaySpell ( "Alpha Spell" , "Beta Spell" ) , testOverlaySpell ( "Beta Spell" ) } ,
} ,
{
name : "alias versus canonical" ,
spells : [ ] overlaySpell { testOverlaySpell ( "Alpha Spell" ) , testOverlaySpell ( "Beta Spell" , "Alpha Spell" ) } ,
} ,
{
name : "alias versus alias" ,
spells : [ ] overlaySpell { testOverlaySpell ( "Alpha Spell" , "Shared Name" ) , testOverlaySpell ( "Beta Spell" , "Shared Name" ) } ,
} ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
content := test . content
if content == "" {
content = testOverlayJSON ( t , testOverlayCatalog ( "campaign.example" , test . spells ... ) )
}
if _ , err := ResolveEffectiveCatalog ( overlayReference ( [ ] byte ( content ) , "application/json" ) ) ; err == nil {
t . Fatal ( "cross-spell collision was accepted" )
}
} )
}
}
func overlayReference ( content [ ] byte , mediaType string ) contracts . ReferenceSet {
return contracts . ReferenceSet { Slots : map [ string ] contracts . ResolvedReferenceSlot {
SpellCatalogReferenceSlot : {
Items : [ ] contracts . ReferenceItem { {
SlotName : SpellCatalogReferenceSlot ,
MediaType : mediaType ,
Content : append ( [ ] byte ( nil ) , content ... ) ,
} } ,
} ,
} }
}
func testOverlaySpell ( name string , aliases ... string ) overlaySpell {
content , err := json . Marshal ( aliases )
if err != nil {
panic ( err )
}
return overlaySpell { Name : name , Aliases : content }
}
func testOverlayCatalog ( id string , spells ... overlaySpell ) overlayCatalog {
return overlayCatalog {
ID : id ,
Ruleset : SRD5E2014Ruleset ,
Source : overlaySource {
Title : id + " spells" ,
Version : json . RawMessage ( ` "1" ` ) ,
URL : json . RawMessage ( ` "" ` ) ,
License : json . RawMessage ( ` "" ` ) ,
} ,
Spells : spells ,
}
}
func testOverlayJSON ( t * testing . T , catalogs ... overlayCatalog ) string {
t . Helper ( )
content , err := json . Marshal ( overlayBundle { SchemaVersion : overlaySchemaVersion , Catalogs : catalogs } )
if err != nil {
t . Fatal ( err )
}
return string ( content )
}
func validOverlayJSON ( ) string {
return ` { "schema_version":"notarius.dnd.spell-catalog-overlay.v1","catalogs":[ { "id":"campaign.example","ruleset":"dnd-5e-2014","source": { "title":"Example campaign spells","version":"1","url":"","license":""},"spells":[ { "name":"Aegis of Emberfall","aliases":["Emberfall Aegis"]}]}]} `
}