2026-07-23 20:02:37 +00:00
package scenedescriptions
import (
"bytes"
"encoding/json"
"os"
"reflect"
"strings"
"testing"
2026-07-23 23:03:37 +00:00
"github.com/santhosh-tekuri/jsonschema/v6"
2026-07-23 20:02:37 +00:00
"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"
)
func validList ( ) dnd . SceneDescriptionList {
return dnd . SceneDescriptionList { Scenes : [ ] dnd . SceneDescription { {
ID : "chunk-000001" ,
SourceRef : source . SourceRef { SourceID : "session-alpha" , StartUnitID : 1 , EndUnitID : 2 } ,
Kind : dnd . SceneKindNarrative ,
Title : "At the city gate" ,
Summary : "The party enters the city after speaking with its guard." ,
} } }
}
func TestCodecMatchesMaintainedDurableFixture ( t * testing . T ) {
raw , err := os . ReadFile ( "testdata/dnd_scene_descriptions.v1.json" )
if err != nil {
t . Fatal ( err )
}
codec := New ( )
value , err := codec . Decode ( raw )
if err != nil {
t . Fatalf ( "Decode() error = %v" , err )
}
if want := validList ( ) ; ! reflect . DeepEqual ( value , want ) {
t . Fatalf ( "Decode() = %#v, want %#v" , value , want )
}
encoded , err := codec . Encode ( value )
if err != nil {
t . Fatalf ( "Encode() error = %v" , err )
}
var compact bytes . Buffer
if err := json . Compact ( & compact , raw ) ; err != nil {
t . Fatal ( err )
}
if ! bytes . Equal ( encoded , compact . Bytes ( ) ) {
t . Fatalf ( "Encode() = %s, want %s" , encoded , compact . Bytes ( ) )
}
}
func TestCodecOwnsDurableSchemaAndMetadata ( t * testing . T ) {
codec := New ( )
schema := codec . Schema ( )
if codec . Kind ( ) != dnd . SceneDescriptionListKind || codec . MediaType ( ) != MediaType {
t . Fatalf ( "codec identity = %q/%q" , codec . Kind ( ) , codec . MediaType ( ) )
}
if schema . ID != SchemaID || schema . Name != SchemaName || schema . Version != SchemaVersion || ! json . Valid ( schema . JSONSchema ) {
t . Fatalf ( "schema = %#v" , schema )
}
registry := pipeline . NewArtifactCodecRegistry ( )
if err := pipeline . RegisterArtifactCodec ( registry , codec ) ; err != nil {
t . Fatal ( err )
}
spec , ok := registry . Spec ( dnd . SceneDescriptionListKind )
if ! ok || spec . SchemaDigest != contracts . DigestArtifactSchema ( schema ) {
t . Fatalf ( "registered spec = %#v, %t" , spec , ok )
}
schema . JSONSchema [ 0 ] = '['
if next := codec . Schema ( ) ; ! json . Valid ( next . JSONSchema ) || next . JSONSchema [ 0 ] == '[' {
t . Fatal ( "Schema() returned shared bytes" )
}
metadata := codec . Metadata ( validList ( ) )
metadata [ "other" ] = true
if next := codec . Metadata ( validList ( ) ) ; len ( next ) != 1 || next [ "scene_count" ] != 1 {
t . Fatalf ( "Metadata() = %#v" , next )
}
encoded , err := codec . Encode ( validList ( ) )
if err != nil {
t . Fatal ( err )
}
encoded [ 0 ] = '['
if next , err := codec . Encode ( validList ( ) ) ; err != nil || ! json . Valid ( next ) || next [ 0 ] == '[' {
t . Fatalf ( "Encode() returned shared bytes: %s, %v" , next , err )
}
}
2026-07-23 23:03:37 +00:00
func TestDurableSchemaRequiresAtLeastOneScene ( t * testing . T ) {
schemaDocument , err := jsonschema . UnmarshalJSON ( bytes . NewReader ( New ( ) . Schema ( ) . JSONSchema ) )
if err != nil {
t . Fatal ( err )
}
compiler := jsonschema . NewCompiler ( )
if err := compiler . AddResource ( "schema.json" , schemaDocument ) ; err != nil {
t . Fatal ( err )
}
compiled , err := compiler . Compile ( "schema.json" )
if err != nil {
t . Fatal ( err )
}
for _ , test := range [ ] struct {
name string
content [ ] byte
valid bool
} {
{ name : "one scene" , content : mustEncode ( t , validList ( ) ) , valid : true } ,
{ name : "empty scenes" , content : [ ] byte ( ` { "scenes":[]} ` ) } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
instance , err := jsonschema . UnmarshalJSON ( bytes . NewReader ( test . content ) )
if err != nil {
t . Fatal ( err )
}
err = compiled . Validate ( instance )
if ( err == nil ) != test . valid {
t . Fatalf ( "Validate() error = %v, want valid=%t" , err , test . valid )
}
} )
}
}
2026-07-23 20:02:37 +00:00
func TestCodecStrictlyRejectsInvalidDurableBoundaries ( t * testing . T ) {
validJSON := ` { "scenes":[ { "id":"chunk-000001","source_ref": { "source_id":"session","start_unit_id":1,"end_unit_id":1},"kind":"narrative","title":"Arrival","summary":"The party arrives."}]} `
tests := [ ] struct { name , raw , want string } {
{ "malformed" , ` { ` , "decode dnd scene description list" } ,
{ "unknown top-level" , ` { "scenes":[],"unexpected":true} ` , "unknown field" } ,
{ "unknown scene field" , strings . Replace ( validJSON , ` "kind":"narrative" ` , ` "kind":"narrative","unexpected":true ` , 1 ) , "unknown field" } ,
{ "unknown source field" , strings . Replace ( validJSON , ` "end_unit_id":1 ` , ` "end_unit_id":1,"unexpected":true ` , 1 ) , "unknown field" } ,
{ "trailing" , ` { "scenes":[]} { } ` , "multiple JSON values" } ,
{ "missing scenes" , ` { } ` , "scenes must be present" } ,
2026-07-23 23:03:37 +00:00
{ "empty scenes" , ` { "scenes":[]} ` , "scenes must not be empty" } ,
2026-07-23 20:02:37 +00:00
{ "invalid kind" , strings . Replace ( validJSON , ` "kind":"narrative" ` , ` "kind":"other" ` , 1 ) , "kind must be supported" } ,
{ "blank title" , strings . Replace ( validJSON , ` "title":"Arrival" ` , ` "title":" " ` , 1 ) , "title must not be empty" } ,
{ "blank summary" , strings . Replace ( validJSON , ` "summary":"The party arrives." ` , ` "summary":" " ` , 1 ) , "summary must not be empty" } ,
{ "blank ID" , strings . Replace ( validJSON , ` "id":"chunk-000001" ` , ` "id":" " ` , 1 ) , "id must not be empty" } ,
{ "blank source ID" , strings . Replace ( validJSON , ` "source_id":"session" ` , ` "source_id":" " ` , 1 ) , "source_id must not be empty" } ,
{ "invalid source start" , strings . Replace ( validJSON , ` "start_unit_id":1 ` , ` "start_unit_id":0 ` , 1 ) , "start_unit_id must be positive" } ,
{ "invalid source end" , strings . Replace ( validJSON , ` "end_unit_id":1 ` , ` "end_unit_id":0 ` , 1 ) , "end_unit_id must be positive" } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
if _ , err := New ( ) . Decode ( [ ] byte ( test . raw ) ) ; err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Decode() error = %v, want %q" , err , test . want )
}
} )
}
}
func TestCodecCandidatePreservesValidatorOwnedValues ( t * testing . T ) {
candidates := [ ] dnd . SceneDescriptionList {
{ } ,
{ Scenes : [ ] dnd . SceneDescription { } } ,
{ Scenes : [ ] dnd . SceneDescription { { ID : " " , Kind : "unsupported" , Title : " " , Summary : " " , SourceRef : source . SourceRef { } } } } ,
{ Scenes : [ ] dnd . SceneDescription { { ID : " " , Kind : "unsupported" , Title : " " , Summary : " " , SourceRef : source . SourceRef { StartUnitID : 0 , EndUnitID : - 1 } } } } ,
}
for _ , candidate := range candidates {
content , err := New ( ) . EncodeCandidate ( candidate )
if err != nil || ! json . Valid ( content ) {
t . Fatalf ( "EncodeCandidate() = %s, %v" , content , err )
}
decoded , err := New ( ) . DecodeCandidate ( content )
if err != nil || ! reflect . DeepEqual ( decoded , candidate ) {
t . Fatalf ( "DecodeCandidate() = %#v, %v; want %#v" , decoded , err , candidate )
}
}
}
2026-08-09 02:03:27 +00:00
func TestCodecRejectsInvalidDurableRangesWhileCandidatesPreserveThem ( t * testing . T ) {
codec := New ( )
for _ , test := range [ ] struct {
name string
mutate func ( * dnd . SceneDescriptionList )
want string
} {
{ name : "nonpositive start" , mutate : func ( value * dnd . SceneDescriptionList ) { value . Scenes [ 0 ] . SourceRef . StartUnitID = 0 } , want : "start_unit_id" } ,
{ name : "nonpositive end" , mutate : func ( value * dnd . SceneDescriptionList ) { value . Scenes [ 0 ] . SourceRef . EndUnitID = 0 } , want : "end_unit_id" } ,
{ name : "reversed" , mutate : func ( value * dnd . SceneDescriptionList ) {
value . Scenes [ 0 ] . SourceRef . StartUnitID = 2
value . Scenes [ 0 ] . SourceRef . EndUnitID = 1
} , want : "must not exceed" } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
candidate := validList ( )
test . mutate ( & candidate )
content , err := codec . EncodeCandidate ( candidate )
if err != nil {
t . Fatalf ( "EncodeCandidate() error = %v" , err )
}
decoded , err := codec . DecodeCandidate ( content )
if err != nil || ! reflect . DeepEqual ( decoded , candidate ) {
t . Fatalf ( "DecodeCandidate() = %#v, %v; want %#v" , decoded , err , candidate )
}
if _ , err := codec . Encode ( candidate ) ; err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Encode() error = %v, want %q" , err , test . want )
}
if _ , err := codec . Decode ( content ) ; err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Decode() error = %v, want %q" , err , test . want )
}
} )
}
}
2026-07-23 20:02:37 +00:00
func TestCodecRejectsRequiredApprovedValues ( t * testing . T ) {
tests := [ ] struct {
name string
value dnd . SceneDescriptionList
want string
} {
{ "nil scenes" , dnd . SceneDescriptionList { } , "scenes must be present" } ,
2026-07-23 23:03:37 +00:00
{ "empty scenes" , dnd . SceneDescriptionList { Scenes : [ ] dnd . SceneDescription { } } , "scenes must not be empty" } ,
2026-07-23 20:02:37 +00:00
{ "empty ID" , mutate ( validList ( ) , func ( value * dnd . SceneDescriptionList ) { value . Scenes [ 0 ] . ID = " " } ) , "id must not be empty" } ,
{ "unsupported kind" , mutate ( validList ( ) , func ( value * dnd . SceneDescriptionList ) { value . Scenes [ 0 ] . Kind = "unsupported" } ) , "kind must be supported" } ,
{ "empty title" , mutate ( validList ( ) , func ( value * dnd . SceneDescriptionList ) { value . Scenes [ 0 ] . Title = " " } ) , "title must not be empty" } ,
{ "empty summary" , mutate ( validList ( ) , func ( value * dnd . SceneDescriptionList ) { value . Scenes [ 0 ] . Summary = " " } ) , "summary must not be empty" } ,
{ "empty source ID" , mutate ( validList ( ) , func ( value * dnd . SceneDescriptionList ) { value . Scenes [ 0 ] . SourceRef . SourceID = " " } ) , "source_id must not be empty" } ,
{ "non-positive start" , mutate ( validList ( ) , func ( value * dnd . SceneDescriptionList ) { value . Scenes [ 0 ] . SourceRef . StartUnitID = 0 } ) , "start_unit_id must be positive" } ,
{ "non-positive end" , mutate ( validList ( ) , func ( value * dnd . SceneDescriptionList ) { value . Scenes [ 0 ] . SourceRef . EndUnitID = 0 } ) , "end_unit_id must be positive" } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
if _ , err := New ( ) . Encode ( test . value ) ; err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Encode() error = %v, want %q" , err , test . want )
}
} )
}
}
2026-07-23 23:03:37 +00:00
func mustEncode ( t * testing . T , value dnd . SceneDescriptionList ) [ ] byte {
t . Helper ( )
content , err := json . Marshal ( value )
if err != nil {
t . Fatal ( err )
}
return content
}
2026-07-23 20:02:37 +00:00
func mutate ( value dnd . SceneDescriptionList , change func ( * dnd . SceneDescriptionList ) ) dnd . SceneDescriptionList {
change ( & value )
return value
}