Register the D&D spell normalizer defaults

This commit is contained in:
2026-07-20 21:06:30 +00:00
parent be22852daa
commit 79b9fffcaf
8 changed files with 97 additions and 11 deletions

View File

@@ -12,6 +12,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/core/debugbundle"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
)
func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
@@ -31,7 +32,9 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
t.Fatalf("materialize maintained example references: %v", err)
}
if example.name == "production" {
if len(materialized.ArtifactLanes) != 1 || len(materialized.ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 {
if len(materialized.ArtifactLanes) != 1 ||
len(materialized.ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 ||
len(materialized.ArtifactLanes[0].NormalizeReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 {
t.Fatalf("production spell catalog reference was not materialized: %#v", materialized.ArtifactLanes)
}
}
@@ -80,7 +83,7 @@ func TestMaintainedMinimalInvocationProducesJSONBundle(t *testing.T) {
t.Fatalf("manifest lanes = %#v, want exactly spells", manifest.ArtifactLanes)
}
lane := manifest.ArtifactLanes[0]
if lane.ID != "spells" || lane.Extractor != "dnd/spells" || lane.Merger != "appendorder" || lane.Normalizer != "noop" {
if lane.ID != "spells" || lane.Extractor != "dnd/spells" || lane.Merger != "appendorder" || lane.Normalizer != spellnormalize.Key {
t.Fatalf("manifest lane = %#v, want production spells composition", lane)
}
if len(manifest.References) != 0 {

View File

@@ -24,6 +24,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes"
spellcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/spells"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
"gitea.maximumdirect.net/eric/notarius/internal/modules/generic/normalize/noop"
)
@@ -35,7 +36,7 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
assertProductionContains(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes", "generic"})
assertProductionContains(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells"})
assertProductionContains(t, "mergers", registries.Mergers.RegisteredKeys(), []string{"appendorder"})
assertProductionContains(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{"noop"})
assertProductionContains(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{"noop", spellnormalize.Key})
assertProductionContains(t, "outputs", registries.Outputs.RegisteredKeys(), []string{"json"})
assertProductionContains(t, "validators", registries.Validators.RegisteredKeys(), []string{
"extract/dnd/spells/catalog",
@@ -50,6 +51,7 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
assertProductionContains(t, "artifact codec kinds", registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind})
assertProductionContains(t, "merger variants", registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind})
assertProductionContains(t, "normalizer variants", registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind})
assertProductionContains(t, "spell normalizer variants", registries.Normalizers.RegisteredArtifactKinds(spellnormalize.Key), []contracts.ArtifactKind{dnd.SpellListKind})
wantChain := []pipeline.ModuleBinding{
pipeline.Binding("generic/valid_json"),
@@ -62,6 +64,9 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, spells.Key); !reflect.DeepEqual(got, wantChain) {
t.Fatalf("spell validator chain = %#v, want %#v", got, wantChain)
}
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, spellnormalize.Key); !reflect.DeepEqual(got, wantChain) {
t.Fatalf("spell normalize validator chain = %#v, want %#v", got, wantChain)
}
assetNames := productionAssetNames(t, components.assets.PromptFS)
requiredAssets := []string{
@@ -93,6 +98,9 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
if got := catalog.ValidatorChains.Validators(pipeline.StageExtract, spells.Key); !reflect.DeepEqual(got, wantChain) {
t.Fatalf("catalog validator chain = %#v, want %#v", got, wantChain)
}
if got := catalog.ValidatorChains.Validators(pipeline.StageNormalize, spellnormalize.Key); !reflect.DeepEqual(got, wantChain) {
t.Fatalf("catalog spell normalize validator chain = %#v, want %#v", got, wantChain)
}
}
@@ -140,9 +148,13 @@ func TestProductionSpellValidatorsPrepareFromMaterializedCatalog(t *testing.T) {
if err != nil {
t.Fatalf("materialize production spell references: %v", err)
}
items := materialized.ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["spell_catalog"].Items
if len(items) != 1 || items[0].MediaType != "application/json" || len(items[0].Content) == 0 {
t.Fatalf("materialized spell catalog items = %#v, want one JSON item", items)
extractItems := materialized.ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["spell_catalog"].Items
normalizeItems := materialized.ArtifactLanes[0].NormalizeReferences.ReferenceSet.Slots["spell_catalog"].Items
if len(extractItems) != 1 || extractItems[0].MediaType != "application/json" || len(extractItems[0].Content) == 0 {
t.Fatalf("materialized extract spell catalog items = %#v, want one JSON item", extractItems)
}
if len(normalizeItems) != 1 || normalizeItems[0].MediaType != "application/json" || !reflect.DeepEqual(normalizeItems[0].Content, extractItems[0].Content) {
t.Fatalf("materialized normalize spell catalog items = %#v, want an independent binding of the extract catalog", normalizeItems)
}
if _, err := pipeline.Prepare(materialized, components.registries, pipeline.ModuleDependencies{LLM: &productionFakeLLMClient{}}); err != nil {
t.Fatalf("prepare production spell pipeline from materialized catalog: %v", err)
@@ -235,7 +247,7 @@ func TestProductionConfigValidationCoversModuleAndVariantFailures(t *testing.T)
name: "invalid artifact variant",
content: base,
options: productionCLIOptionsWithoutSpellNormalizer(t),
fragments: []string{"normalizer", "noop", string(dnd.SpellListKind), "variant"},
fragments: []string{"normalizer", spellnormalize.Key, string(dnd.SpellListKind), "variant"},
},
{
name: "deterministic validator with profile",
@@ -261,6 +273,27 @@ func TestProductionConfigValidationCoversModuleAndVariantFailures(t *testing.T)
}
}
func TestProductionNormalizeValidatorOverrideRemainsAuthoritative(t *testing.T) {
base := string(readRepositoryFile(t, "examples", "dnd-spells.config.yml"))
content := replaceRequiredOnce(t, base, " normalize: dnd/spells\n", " normalize:\n module: dnd/spells\n validators:\n - module: generic/always_accept\n")
path := writeProductionContractConfig(t, content)
components := productionTestComponents(t)
effective, err := loadMaintainedExample(t, path).Resolve(resolveInputForMaintainedExample(components, "dnd-session"))
if err != nil {
t.Fatalf("resolve normalize override: %v", err)
}
for _, chain := range effective.ResolvedPipeline.ValidatorChains {
if chain.Stage != pipeline.StageNormalize || chain.ModuleKey != spellnormalize.Key {
continue
}
if len(chain.Validators) != 1 || chain.Validators[0].Binding.Module != "generic/always_accept" {
t.Fatalf("normalize validator chain = %#v, want explicit always-accept override", chain)
}
return
}
t.Fatalf("resolved validator chains = %#v, want normalize chain for %q", effective.ResolvedPipeline.ValidatorChains, spellnormalize.Key)
}
func TestProductionSceneRunRecordsChunkerWarningsAndProvenance(t *testing.T) {
outputRoot := filepath.Join(t.TempDir(), "output")
configPath := writeProductionContractConfig(t, productionRunConfig(outputRoot, "dnd/scenes"))

View File

@@ -17,6 +17,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
spellcatalog "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/spells/catalog"
)
@@ -128,8 +129,28 @@ func TestSemanticSpellCatalogFingerprintChangesCheckpointIdentityWithoutReferenc
t.Fatal(err)
}
fingerprints := prepared.CheckpointFingerprints()
if len(fingerprints) != 2 || fingerprints[0].Value != fingerprints[1].Value {
t.Fatalf("prepared fingerprints = %#v, want matching extractor and validator catalog identities", fingerprints)
wantNames := map[string]struct{}{
"extract:spells:" + spells.Key + ":effective_catalog": {},
"extract:spells:" + spells.Key + ":validator:3:extract/dnd/spells/catalog:effective_catalog": {},
"normalize:spells:" + spellnormalize.Key + ":effective_catalog": {},
"normalize:spells:" + spellnormalize.Key + ":validator:3:extract/dnd/spells/catalog:effective_catalog": {},
}
seen := make(map[string]string, len(fingerprints))
for _, fingerprint := range fingerprints {
if _, ok := wantNames[fingerprint.Name]; ok {
seen[fingerprint.Name] = fingerprint.Value
}
}
if len(seen) != len(wantNames) {
t.Fatalf("prepared fingerprints = %#v, want scoped extractor and normalize catalog identities", fingerprints)
}
var catalogDigest string
for name, value := range seen {
if catalogDigest == "" {
catalogDigest = value
} else if value != catalogDigest {
t.Fatalf("prepared fingerprint %q = %q, want shared semantic catalog digest %q", name, value, catalogDigest)
}
}
identityFor := func(values []pipeline.CheckpointFingerprint) checkpoint.Identity {

View File

@@ -106,8 +106,8 @@ func TestProductionSpellCatalogValidationRetries(t *testing.T) {
if len(value.SpellCasts) != 1 || value.SpellCasts[0].Spell != tt.wantSpell {
t.Fatalf("normalized spell list = %#v, want accepted overlay spell", value)
}
if len(output.Warnings) != 1 || output.Warnings[0].ReasonCode != tt.wantWarningCode {
t.Fatalf("warnings = %#v, want only accepted-attempt warning", output.Warnings)
if len(output.Warnings) != 2 || output.Warnings[0].ReasonCode != tt.wantWarningCode || output.Warnings[1].ReasonCode != tt.wantWarningCode {
t.Fatalf("warnings = %#v, want accepted-attempt warnings from extract and normalize validation", output.Warnings)
}
})
}