161 lines
8.6 KiB
Go
161 lines
8.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"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/pipeline"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
|
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
|
|
combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns"
|
|
)
|
|
|
|
func TestProductionCombatConfigurationResolvesTypedLane(t *testing.T) {
|
|
components := productionTestComponents(t)
|
|
configPath := repositoryPath("examples", "dnd-combat-turns.config.yml")
|
|
cfg := loadMaintainedExample(t, configPath)
|
|
effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-combat", Catalog: catalogFromRegistries(components.registries)})
|
|
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 combat lane", effective.ResolvedPipeline.Steps[0].ArtifactLanes)
|
|
}
|
|
lane := effective.ResolvedPipeline.Steps[0].ArtifactLanes[0]
|
|
if lane.ID != "combat" || lane.ArtifactKind != dnd.CombatTurnListKind || lane.Extract.Module != combatextract.Key || lane.Extract.Retries != 2 || lane.Merge.Module != pipeline.DefaultMergeModule || lane.Normalize.Module != combatnormalize.Key {
|
|
t.Fatalf("resolved combat lane = %#v, want typed production composition", lane)
|
|
}
|
|
|
|
catalog := catalogFromRegistries(components.registries)
|
|
extractSpec, ok := catalog.Extractors.Spec(combatextract.Key)
|
|
if !ok || !reflect.DeepEqual(extractSpec.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(extractSpec.Provides, []string{"dnd.combat_turns"}) {
|
|
t.Fatalf("combat extractor spec = %#v, want source and artifact capabilities", extractSpec)
|
|
}
|
|
normalizeSpec, ok := catalog.Normalizers.SpecForArtifact(combatnormalize.Key, dnd.CombatTurnListKind)
|
|
if !ok || !reflect.DeepEqual(normalizeSpec.Requires, []string{"merged"}) || !reflect.DeepEqual(normalizeSpec.Provides, []string{"normalized"}) {
|
|
t.Fatalf("combat normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec)
|
|
}
|
|
mergeSpec, ok := catalog.Mergers.SpecForArtifact(pipeline.DefaultMergeModule, dnd.CombatTurnListKind)
|
|
if !ok || !reflect.DeepEqual(mergeSpec.Provides, []string{"merged"}) {
|
|
t.Fatalf("combat merger spec = %#v, want merged capability", mergeSpec)
|
|
}
|
|
codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.CombatTurnListKind)
|
|
if !ok || codecSpec.Schema.ID != "notarius.dnd.combat_turns" || codecSpec.Schema.Version != "v1" {
|
|
t.Fatalf("combat codec spec = %#v, want compatible durable schema", codecSpec)
|
|
}
|
|
if !hasReferenceSlot(extractSpec.ReferenceSlots, "npcs") || !hasReferenceSlot(normalizeSpec.ReferenceSlots, "npcs") {
|
|
t.Fatalf("combat reference slots = %#v / %#v, want stage-local NPC slots", extractSpec.ReferenceSlots, normalizeSpec.ReferenceSlots)
|
|
}
|
|
|
|
wantExtractChain := []pipeline.ModuleBinding{
|
|
pipeline.Binding("generic/valid_json"),
|
|
pipeline.Binding("generic/valid_json_schema"),
|
|
pipeline.Binding("extract/dnd/combat-turns/shape"),
|
|
pipeline.Binding("extract/dnd/combat-turns/source_refs"),
|
|
pipeline.Binding("extract/dnd/combat-turns/source_relatedness"),
|
|
}
|
|
wantNormalizeChain := []pipeline.ModuleBinding{
|
|
pipeline.Binding("generic/valid_json"),
|
|
pipeline.Binding("generic/valid_json_schema"),
|
|
pipeline.Binding("extract/dnd/combat-turns/shape"),
|
|
pipeline.Binding("normalize/dnd/combat-turns/invariants"),
|
|
pipeline.Binding("extract/dnd/combat-turns/source_refs"),
|
|
pipeline.Binding("extract/dnd/combat-turns/source_relatedness"),
|
|
}
|
|
if got := validatorChain(effective.ResolvedPipeline, pipeline.StageExtract, combatextract.Key); !reflect.DeepEqual(got, wantExtractChain) {
|
|
t.Fatalf("combat extract chain = %#v, want %#v", got, wantExtractChain)
|
|
}
|
|
if got := validatorChain(effective.ResolvedPipeline, pipeline.StageNormalize, combatnormalize.Key); !reflect.DeepEqual(got, wantNormalizeChain) {
|
|
t.Fatalf("combat normalize chain = %#v, want %#v", got, wantNormalizeChain)
|
|
}
|
|
if got := validatorChain(effective.ResolvedPipeline, pipeline.StageMerge, pipeline.DefaultMergeModule); len(got) != 0 {
|
|
t.Fatalf("combat merge chain = %#v, want empty", got)
|
|
}
|
|
|
|
bound, err := cfg.Resolve(config.ResolveInput{
|
|
PipelineID: "dnd-combat",
|
|
Catalog: catalog,
|
|
ReferenceOverrides: []pipeline.ReferenceBinding{
|
|
{Stage: pipeline.StageExtract, LaneID: "combat", SlotName: "npcs", Source: "npc-run/lanes/npcs.json", BindingSource: contracts.ReferenceBindingSourceCLI},
|
|
{Stage: pipeline.StageNormalize, LaneID: "combat", SlotName: "npcs", Source: "npc-run/lanes/npcs.json", BindingSource: contracts.ReferenceBindingSourceCLI},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Resolve(bound references) error = %v, want nil", err)
|
|
}
|
|
boundLane := bound.ResolvedPipeline.Steps[0].ArtifactLanes[0]
|
|
if len(boundLane.ExtractReferences.Bindings) != 1 || len(boundLane.NormalizeReferences.Bindings) != 1 || boundLane.ExtractReferences.Bindings[0].SlotName != "npcs" || boundLane.NormalizeReferences.Bindings[0].SlotName != "npcs" {
|
|
t.Fatalf("bound combat references = %#v / %#v, want one independent NPC binding per stage", boundLane.ExtractReferences, boundLane.NormalizeReferences)
|
|
}
|
|
}
|
|
|
|
func TestProductionCombatConfigurationRejectsLooseOptionsAndLaneValidators(t *testing.T) {
|
|
components := productionTestComponents(t)
|
|
configPath := repositoryPath("examples", "dnd-combat-turns.config.yml")
|
|
resolve := func(mutate func(*pipeline.PipelineProfile)) error {
|
|
cfg := loadMaintainedExample(t, configPath)
|
|
profile := cfg.Pipelines["dnd-combat"]
|
|
mutate(&profile)
|
|
cfg.Pipelines["dnd-combat"] = profile
|
|
_, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-combat", Catalog: catalogFromRegistries(components.registries)})
|
|
return err
|
|
}
|
|
if err := resolve(func(profile *pipeline.PipelineProfile) {
|
|
lane := profile.Artifacts["combat"]
|
|
lane.Extract.Options = map[string]any{"unexpected": true}
|
|
profile.Artifacts["combat"] = 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["combat"]
|
|
lane.Normalize.Options = map[string]any{"unexpected": true}
|
|
profile.Artifacts["combat"] = 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) {
|
|
lane := profile.Artifacts["combat"]
|
|
lane.Validators = []pipeline.ModuleBinding{pipeline.Binding("generic/always_accept")}
|
|
profile.Artifacts["combat"] = lane
|
|
}); err == nil || !strings.Contains(err.Error(), "artifact lane level") {
|
|
t.Fatalf("lane-level validator error = %v, want invalid placement rejection", err)
|
|
}
|
|
}
|
|
|
|
func TestProductionCombatConfigurationResolvesTypedUnconditionalValidators(t *testing.T) {
|
|
components := productionTestComponents(t)
|
|
cfg := loadMaintainedExample(t, repositoryPath("examples", "dnd-combat-turns.config.yml"))
|
|
profile := cfg.Pipelines["dnd-combat"]
|
|
lane := profile.Artifacts["combat"]
|
|
lane.Extract.Validators = pipeline.ValidatorOverride{Set: true, Validators: []pipeline.ModuleBinding{pipeline.Binding("generic/always_accept")}}
|
|
lane.Normalize.Validators = pipeline.ValidatorOverride{Set: true, Validators: []pipeline.ModuleBinding{pipeline.Binding("generic/always_reject")}}
|
|
profile.Artifacts["combat"] = lane
|
|
cfg.Pipelines["dnd-combat"] = profile
|
|
effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-combat", Catalog: catalogFromRegistries(components.registries)})
|
|
if err != nil {
|
|
t.Fatalf("Resolve() error = %v, want typed unconditional validators to resolve", err)
|
|
}
|
|
if got := validatorChain(effective.ResolvedPipeline, pipeline.StageExtract, combatextract.Key); !reflect.DeepEqual(got, []pipeline.ModuleBinding{pipeline.Binding("generic/always_accept")}) {
|
|
t.Fatalf("extract override chain = %#v, want typed always-accept", got)
|
|
}
|
|
if got := validatorChain(effective.ResolvedPipeline, pipeline.StageNormalize, combatnormalize.Key); !reflect.DeepEqual(got, []pipeline.ModuleBinding{pipeline.Binding("generic/always_reject")}) {
|
|
t.Fatalf("normalize override chain = %#v, want typed always-reject", got)
|
|
}
|
|
}
|
|
|
|
func hasReferenceSlot(slots []contracts.ReferenceSlot, name string) bool {
|
|
for _, slot := range slots {
|
|
if slot.Name == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|