package cli import ( "reflect" "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/core/config" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs" npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs" ) func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) { components := productionTestComponents(t) catalog := catalogFromRegistries(components.registries) configPath := repositoryPath("examples", "dnd-npcs.config.yml") cfg := loadMaintainedExample(t, configPath) effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-session", Catalog: catalog}) if err != nil { t.Fatalf("Resolve() error = %v, want nil", err) } if effective.ResolvedPipeline.Chunk.Module != pipeline.DefaultChunkModule { t.Fatalf("chunk module = %q, want %q", effective.ResolvedPipeline.Chunk.Module, pipeline.DefaultChunkModule) } if len(effective.ResolvedPipeline.Steps[0].ArtifactLanes) != 1 { t.Fatalf("artifact lanes = %#v, want one NPC lane", effective.ResolvedPipeline.Steps[0].ArtifactLanes) } lane := effective.ResolvedPipeline.Steps[0].ArtifactLanes[0] if lane.ID != "npcs" || lane.ArtifactKind != dnd.NPCListKind || lane.Extract.Module != npcextract.Key || lane.Extract.Retries != 2 || lane.Merge.Module != pipeline.DefaultMergeModule || lane.Normalize.Module != npcnormalize.Key { t.Fatalf("resolved NPC lane = %#v, want typed production composition", lane) } if len(lane.ExtractReferences.Bindings) != 0 || len(lane.NormalizeReferences.Bindings) != 0 { t.Fatalf("unbound NPC references = %#v / %#v, want none", lane.ExtractReferences, lane.NormalizeReferences) } extractSpec, ok := catalog.Extractors.Spec(npcextract.Key) if !ok || !reflect.DeepEqual(extractSpec.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(extractSpec.Provides, []string{"dnd.npcs"}) { t.Fatalf("NPC extractor spec = %#v, want source and artifact capabilities", extractSpec) } mergeSpec, ok := catalog.Mergers.SpecForArtifact(pipeline.DefaultMergeModule, dnd.NPCListKind) if !ok || !reflect.DeepEqual(mergeSpec.Provides, []string{"merged"}) { t.Fatalf("NPC merger spec = %#v, want merged capability", mergeSpec) } normalizeSpec, ok := catalog.Normalizers.SpecForArtifact(npcnormalize.Key, dnd.NPCListKind) if !ok || !reflect.DeepEqual(normalizeSpec.Requires, []string{"merged"}) || !reflect.DeepEqual(normalizeSpec.Provides, []string{"normalized"}) { t.Fatalf("NPC normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec) } wantExtractChain := []pipeline.ModuleBinding{ pipeline.Binding("generic/valid_json"), pipeline.Binding("generic/valid_json_schema"), pipeline.Binding("extract/dnd/npcs/shape"), pipeline.Binding("extract/dnd/npcs/source_refs"), pipeline.Binding("extract/dnd/npcs/source_relatedness"), } wantNormalizeChain := []pipeline.ModuleBinding{ pipeline.Binding("generic/valid_json"), pipeline.Binding("generic/valid_json_schema"), pipeline.Binding("extract/dnd/npcs/shape"), pipeline.Binding("normalize/dnd/npcs/identity"), pipeline.Binding("extract/dnd/npcs/source_refs"), pipeline.Binding("extract/dnd/npcs/source_relatedness"), } if got := validatorChain(effective.ResolvedPipeline, pipeline.StageExtract, npcextract.Key); !reflect.DeepEqual(got, wantExtractChain) { t.Fatalf("NPC extract chain = %#v, want %#v", got, wantExtractChain) } if got := validatorChain(effective.ResolvedPipeline, pipeline.StageNormalize, npcnormalize.Key); !reflect.DeepEqual(got, wantNormalizeChain) { t.Fatalf("NPC normalize chain = %#v, want %#v", got, wantNormalizeChain) } if got := validatorChain(effective.ResolvedPipeline, pipeline.StageMerge, pipeline.DefaultMergeModule); len(got) != 0 { t.Fatalf("NPC merge chain = %#v, want empty", got) } } func TestProductionNPCConfigurationValidatesOptionsReferencesAndPlacement(t *testing.T) { components := productionTestComponents(t) configPath := repositoryPath("examples", "dnd-npcs.config.yml") resolve := func(mutate func(*pipeline.PipelineProfile)) error { cfg := loadMaintainedExample(t, configPath) profile := cfg.Pipelines["dnd-session"] mutate(&profile) cfg.Pipelines["dnd-session"] = profile _, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-session", Catalog: catalogFromRegistries(components.registries)}) return err } if err := resolve(func(profile *pipeline.PipelineProfile) { lane := profile.Artifacts["npcs"] lane.Extract.Options = map[string]any{"unexpected": true} profile.Artifacts["npcs"] = lane }); err == nil || !strings.Contains(err.Error(), "unknown option") { t.Fatalf("unknown extractor option error = %v, want strict option rejection", err) } if err := resolve(func(profile *pipeline.PipelineProfile) { lane := profile.Artifacts["npcs"] lane.Normalize.Options = map[string]any{"unexpected": true} profile.Artifacts["npcs"] = lane }); err == nil || !strings.Contains(err.Error(), "unknown option") { t.Fatalf("unknown normalizer option error = %v, want strict option rejection", err) } if err := resolve(func(profile *pipeline.PipelineProfile) { profile.References = pipeline.ExternalReferenceMap(map[string]string{ "players": "players.txt", "party": "party.txt", "glossary": "glossary.txt", }) }); err != nil { t.Fatalf("optional NPC references error = %v, want resolution success", err) } if err := resolve(func(profile *pipeline.PipelineProfile) { lane := profile.Artifacts["npcs"] lane.Validators = []pipeline.ModuleBinding{pipeline.Binding("normalize/dnd/npcs/identity")} profile.Artifacts["npcs"] = lane }); err == nil || !strings.Contains(err.Error(), "artifact lane level") { t.Fatalf("lane-level validator error = %v, want invalid placement rejection", err) } } func validatorChain(resolved pipeline.ResolvedPipeline, stage pipeline.ModuleStage, module string) []pipeline.ModuleBinding { for _, chain := range resolved.ValidatorChains { if chain.Stage == stage && chain.ModuleKey == module { bindings := make([]pipeline.ModuleBinding, len(chain.Validators)) for index, validator := range chain.Validators { bindings[index] = validator.Binding } return bindings } } return nil }