2026-07-21 02:50:37 +00:00
package integration_test
import (
"context"
"encoding/json"
"fmt"
"os"
"reflect"
"strings"
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
dndregister "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/register"
2026-07-22 14:30:20 +00:00
npcshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcs/shape"
2026-07-21 02:50:37 +00:00
genericregister "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/register"
"gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript"
seriatimregister "gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/register"
)
func TestRunnerProcessesSeriatimInputWithProductionDNDNPCPipeline ( t * testing . T ) {
raw := readNPCFixture ( t )
doc , err := transcript . New ( ) . Parse ( context . Background ( ) , contracts . ParseRequest { Raw : raw } )
if err != nil {
t . Fatalf ( "Parse() error = %v, want nil" , err )
}
registries := productionNPCRegistries ( t )
configValue := loadNPCPipelineConfig ( t )
effective , err := configValue . Resolve ( config . ResolveInput { PipelineID : "dnd-npcs-fixture" , Catalog : moduleCatalog ( registries ) } )
if err != nil {
t . Fatalf ( "Resolve() error = %v, want nil" , err )
}
client := & fakeNPCProductionLLMClient { response : npcProductionResponse {
NPCs : [ ] npcProductionRecord {
{
Name : "Mira Thorn" ,
Aliases : [ ] string { "The Greencloak" } ,
Description : "The first named NPC encountered." ,
Relationships : [ ] npcProductionRelationship {
{ Target : "Hooded Guard" , Relationship : "works with" } ,
} ,
SourceRefs : [ ] npcProductionSourceRef { { StartUnitID : 1 , EndUnitID : 1 } } ,
} ,
{
Name : "The Greencloak" ,
Aliases : [ ] string { "Mira" } ,
Description : "A later description that must not replace the first." ,
Relationships : [ ] npcProductionRelationship {
{ Target : "Hooded Guard" , Relationship : "trusts" } ,
} ,
SourceRefs : [ ] npcProductionSourceRef { { StartUnitID : 2 , EndUnitID : 2 } } ,
} ,
{
Name : "Hooded Guard" ,
Aliases : [ ] string { } ,
Description : "An unnamed but distinguishable sentry." ,
Relationships : [ ] npcProductionRelationship {
{ Target : "The Greencloak" , Relationship : "reports to" } ,
} ,
SourceRefs : [ ] npcProductionSourceRef { { StartUnitID : 3 , EndUnitID : 3 } } ,
} ,
} } ,
}
output , err := runPreparedPipeline ( t , registries , effective . ResolvedPipeline , client , pipeline . RunInput { RawInput : raw } )
if err != nil {
t . Fatalf ( "Run() error = %v, want nil" , err )
}
if len ( output . NormalizeOutputs ) != 1 {
t . Fatalf ( "normalize outputs = %d, want one NPC output; rejected=%#v" , len ( output . NormalizeOutputs ) , output . Rejected )
}
serialized := output . NormalizeOutputs [ 0 ]
if serialized . LaneID != "npcs" || serialized . NormalizerKey != npcs . Key || serialized . Artifact . Schema . ID != npccodec . SchemaID || serialized . Artifact . Schema . Version != npccodec . SchemaVersion {
t . Fatalf ( "serialized output = %#v, want durable NPC lane schema" , serialized )
}
value , err := npccodec . New ( ) . Decode ( serialized . Artifact . Content )
if err != nil {
t . Fatalf ( "Decode(output) error = %v, want durable NPC payload" , err )
}
if len ( value . NPCs ) != 2 {
t . Fatalf ( "NPC output = %#v, want repeated name consolidated and group/PC omitted" , value . NPCs )
}
first , second := value . NPCs [ 0 ] , value . NPCs [ 1 ]
if first . Name != "Mira Thorn" || first . Description != "The first named NPC encountered." || ! reflect . DeepEqual ( first . Aliases , [ ] string { "The Greencloak" , "Mira" } ) {
t . Fatalf ( "first NPC = %#v, want consolidated Mira identity" , first )
}
if first . ID != identity . DeriveID ( first . Name ) || second . Name != "Hooded Guard" || second . ID != identity . DeriveID ( second . Name ) {
t . Fatalf ( "NPC IDs = %q/%q, want derived IDs" , first . ID , second . ID )
}
if first . Relationships [ 0 ] . Target != "Hooded Guard" || second . Relationships [ 0 ] . Target != "Mira Thorn" {
t . Fatalf ( "relationship targets = %q/%q, want canonical target rewrite" , first . Relationships [ 0 ] . Target , second . Relationships [ 0 ] . Target )
}
for _ , npc := range value . NPCs {
for _ , ref := range npc . SourceRefs {
if ref . SourceID != doc . ID {
t . Fatalf ( "NPC source ref = %#v, want source document %q" , ref , doc . ID )
}
}
}
if ! hasNPCWarning ( output . Warnings , "duplicate_npc_collapsed" ) || ! hasNPCWarning ( output . Warnings , "relationship_target_canonicalized" ) {
t . Fatalf ( "warnings = %#v, want consolidation and target warnings" , output . Warnings )
}
if output . Manifest . ValidationStatus != "approved" || len ( output . Manifest . ArtifactLanes ) != 1 {
t . Fatalf ( "manifest = %#v, want approved NPC lane" , output . Manifest )
}
lane := output . Manifest . ArtifactLanes [ 0 ]
if lane . ID != "npcs" || lane . Extractor != npcs . Key || lane . Merger != pipeline . DefaultMergeModule || lane . Normalizer != npcs . Key {
t . Fatalf ( "manifest lane = %#v, want NPC production composition" , lane )
}
normalizerMetadata , ok := lane . Metadata [ "normalizer" ] . ( map [ string ] any )
if ! ok || normalizerMetadata [ "identity_policy" ] != identity . Policy || normalizerMetadata [ "normalization_policy" ] != "dnd.npcs.normalize.v1" {
t . Fatalf ( "normalizer metadata = %#v, want identity and normalization policies" , lane . Metadata )
}
var npcOutputFile * contracts . OutputFile
for index := range output . OutputFiles {
if output . OutputFiles [ index ] . Name == "lanes/npcs.json" {
npcOutputFile = & output . OutputFiles [ index ]
break
}
}
if npcOutputFile == nil || npcOutputFile . ContentType != npccodec . MediaType {
t . Fatalf ( "output files = %#v, want JSON NPC lane file" , output . OutputFiles )
}
if len ( client . requests ) != 1 || client . requests [ 0 ] . PromptID != npcs . PromptID {
t . Fatalf ( "LLM requests = %#v, want one NPC prompt request" , client . requests )
}
}
2026-07-22 14:30:20 +00:00
func TestProductionNPCPipelineAttributesInvalidShapeBeforeSchemaValidation ( t * testing . T ) {
registries := productionNPCRegistries ( t )
effective , err := loadNPCPipelineConfig ( t ) . Resolve ( config . ResolveInput {
PipelineID : "dnd-npcs-fixture" ,
Catalog : moduleCatalog ( registries ) ,
} )
if err != nil {
t . Fatalf ( "Resolve() error = %v, want nil" , err )
}
client := & fakeNPCProductionLLMClient { response : npcProductionResponse { NPCs : [ ] npcProductionRecord { {
Name : "" ,
Aliases : [ ] string { } ,
Description : "A participant." ,
Relationships : [ ] npcProductionRelationship { } ,
SourceRefs : [ ] npcProductionSourceRef { { StartUnitID : 1 , EndUnitID : 1 } } ,
} } } }
output , err := runPreparedPipeline ( t , registries , effective . ResolvedPipeline , client , pipeline . RunInput { RawInput : readNPCFixture ( t ) } )
if err != nil {
t . Fatalf ( "Run() error = %v, want non-fatal rejected output" , err )
}
if len ( client . requests ) != 3 || len ( output . Rejected ) != 1 || len ( output . NormalizeOutputs ) != 0 {
t . Fatalf ( "LLM requests = %d rejected = %#v normalized = %#v, want exhausted shape rejection" , len ( client . requests ) , output . Rejected , output . NormalizeOutputs )
}
rejection := output . Rejected [ 0 ]
if rejection . ReasonCode != npcshape . ReasonCode || rejection . ValidatorName != npcshape . Key || rejection . AttemptCount != 3 {
t . Fatalf ( "rejection = %#v, want exhausted NPC shape rejection" , rejection )
}
}
2026-07-21 02:50:37 +00:00
type npcProductionResponse struct {
NPCs [ ] npcProductionRecord ` json:"npcs" `
}
type npcProductionRecord struct {
Name string ` json:"name" `
Aliases [ ] string ` json:"aliases" `
Description string ` json:"description" `
Relationships [ ] npcProductionRelationship ` json:"relationships" `
SourceRefs [ ] npcProductionSourceRef ` json:"source_refs" `
}
type npcProductionRelationship struct {
Target string ` json:"target" `
Relationship string ` json:"relationship" `
}
type npcProductionSourceRef struct {
StartUnitID int ` json:"start_unit_id" `
EndUnitID int ` json:"end_unit_id" `
}
type fakeNPCProductionLLMClient struct {
response npcProductionResponse
requests [ ] contracts . StructuredCompletionRequest
}
func ( client * fakeNPCProductionLLMClient ) CompleteStructured ( _ context . Context , req contracts . StructuredCompletionRequest , out any ) ( contracts . StructuredCompletionResponse , error ) {
client . requests = append ( client . requests , req )
content , err := json . Marshal ( client . response )
if err != nil {
return contracts . StructuredCompletionResponse { } , err
}
if err := json . Unmarshal ( content , out ) ; err != nil {
return contracts . StructuredCompletionResponse { } , fmt . Errorf ( "populate NPC structured target: %w" , err )
}
return contracts . StructuredCompletionResponse { Content : content } , nil
}
func productionNPCRegistries ( t * testing . T ) pipeline . Registries {
t . Helper ( )
registries := 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 ( ) ,
}
assets := llm . NewAssetRegistry ( )
for _ , registration := range [ ] struct {
name string
fn func ( pipeline . Registries , * llm . AssetRegistry ) error
} {
{ name : "generic" , fn : genericregister . Register } ,
{ name : "seriatim" , fn : seriatimregister . Register } ,
{ name : "dnd" , fn : dndregister . Register } ,
} {
if err := registration . fn ( registries , assets ) ; err != nil {
t . Fatalf ( "register %s modules: %v" , registration . name , err )
}
}
return registries
}
func moduleCatalog ( registries pipeline . Registries ) pipeline . ModuleCatalog {
return pipeline . ModuleCatalog {
Inputs : registries . Inputs , Chunkers : registries . Chunkers , ArtifactCodecs : registries . ArtifactCodecs ,
Extractors : registries . Extractors , Mergers : registries . Mergers , Normalizers : registries . Normalizers ,
Validators : registries . Validators , ValidatorChains : registries . ValidatorChains , Outputs : registries . Outputs ,
}
}
func loadNPCPipelineConfig ( t * testing . T ) config . Config {
t . Helper ( )
data , err := os . ReadFile ( "testdata/dnd_npcs_pipeline.yml" )
if err != nil {
t . Fatalf ( "ReadFile(dnd_npcs_pipeline.yml) error = %v" , err )
}
fileConfig , err := config . ParseFileConfigYAML ( data )
if err != nil {
t . Fatalf ( "ParseFileConfigYAML() error = %v" , err )
}
cfg := config . Default ( )
if err := cfg . ApplyFileConfig ( fileConfig ) ; err != nil {
t . Fatalf ( "ApplyFileConfig() error = %v" , err )
}
return cfg
}
func readNPCFixture ( t * testing . T ) [ ] byte {
t . Helper ( )
raw , err := os . ReadFile ( "testdata/seriatim_npc_session.json" )
if err != nil {
t . Fatalf ( "ReadFile(seriatim_npc_session.json) error = %v" , err )
}
return raw
}
func hasNPCWarning ( warnings [ ] contracts . Warning , reason string ) bool {
for _ , warning := range warnings {
if warning . ReasonCode == reason && strings . HasPrefix ( warning . Scope , "npcs[" ) {
return true
}
}
return false
}