Add assembled spell pipeline and checkpoint coverage

This commit is contained in:
2026-07-20 21:16:08 +00:00
parent 79b9fffcaf
commit ae97adb8b0
2 changed files with 303 additions and 7 deletions

View File

@@ -14,6 +14,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/checkpoint"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"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"
@@ -42,6 +43,18 @@ func TestSpellCatalogBytesAffectCheckpointIdentityButNotSemanticDigest(t *testin
t.Fatalf("spell catalog bindings = %#v, want catalog binding", bindings)
}
resolved.ArtifactLanes[0].ExtractReferences.Bindings[catalogBindingIndex].Source = overlayPath
normalizeBindings := resolved.ArtifactLanes[0].NormalizeReferences.Bindings
normalizeCatalogBindingIndex := -1
for index, binding := range normalizeBindings {
if binding.SlotName == spellcatalog.SpellCatalogReferenceSlot {
normalizeCatalogBindingIndex = index
break
}
}
if normalizeCatalogBindingIndex < 0 {
t.Fatalf("normalize spell catalog bindings = %#v, want catalog binding", normalizeBindings)
}
resolved.ArtifactLanes[0].NormalizeReferences.Bindings[normalizeCatalogBindingIndex].Source = overlayPath
if err := os.WriteFile(overlayPath, []byte(reorderedOverlayA), 0o600); err != nil {
t.Fatal(err)
@@ -52,6 +65,7 @@ func TestSpellCatalogBytesAffectCheckpointIdentityButNotSemanticDigest(t *testin
}
identityA := catalogCheckpointIdentity(t, materializedA)
metadataA := catalogExtractorMetadata(t, materializedA)
normalizerMetadataA := catalogNormalizerMetadata(t, materializedA)
referenceA := catalogReference(t, materializedA)
if err := os.WriteFile(overlayPath, []byte(reorderedOverlayB), 0o600); err != nil {
@@ -63,6 +77,7 @@ func TestSpellCatalogBytesAffectCheckpointIdentityButNotSemanticDigest(t *testin
}
identityB := catalogCheckpointIdentity(t, materializedB)
metadataB := catalogExtractorMetadata(t, materializedB)
normalizerMetadataB := catalogNormalizerMetadata(t, materializedB)
referenceB := catalogReference(t, materializedB)
if identityA.Digest == identityB.Digest {
@@ -82,6 +97,14 @@ func TestSpellCatalogBytesAffectCheckpointIdentityButNotSemanticDigest(t *testin
if got, want := metadataA["catalog_overlay_ids"], []string{"campaign.a", "campaign.b"}; !reflect.DeepEqual(got, want) || !reflect.DeepEqual(metadataB["catalog_overlay_ids"], want) {
t.Fatalf("extractor overlay IDs = %#v and %#v, want %#v", got, metadataB["catalog_overlay_ids"], want)
}
normalizerDigestA, ok := normalizerMetadataA["catalog_digest"].(string)
normalizerDigestB, okB := normalizerMetadataB["catalog_digest"].(string)
if !ok || !okB || normalizerDigestA != digestA || normalizerDigestB != digestB {
t.Fatalf("normalizer catalog digests = %#v and %#v, want extractor semantic digests %q and %q", normalizerMetadataA["catalog_digest"], normalizerMetadataB["catalog_digest"], digestA, digestB)
}
if got, want := normalizerMetadataA["catalog_overlay_ids"], []string{"campaign.a", "campaign.b"}; !reflect.DeepEqual(got, want) || !reflect.DeepEqual(normalizerMetadataB["catalog_overlay_ids"], want) {
t.Fatalf("normalizer overlay IDs = %#v and %#v, want %#v", got, normalizerMetadataB["catalog_overlay_ids"], want)
}
}
func TestConfiguredSpellCatalogBindingChangesResolvedPipelineIdentity(t *testing.T) {
@@ -205,6 +228,18 @@ func TestChangedSemanticSpellCatalogFingerprintCannotResumeRecordedCheckpoint(t
if err := recorder.SourceSucceeded(materialized.Input.Module, &doc); err != nil {
t.Fatal(err)
}
normalizeDependencies := []pipeline.CheckpointFingerprint{{Name: "artifact[0]", Value: "sha256:merged-artifact"}}
normalizeSchema := contracts.ArtifactSchema{ID: "notarius.dnd.spells", Name: "notarius_dnd_spells", Version: "v1"}
normalizeArtifact := pipeline.CheckpointArtifact{
LaneID: "spells", ModuleKey: spellnormalize.Key, SourceID: doc.ID,
SchemaDigest: contracts.DigestArtifactSchema(normalizeSchema),
Artifact: contracts.SerializedArtifact{
Kind: dnd.SpellListKind, Schema: normalizeSchema, MediaType: "application/json", Content: []byte(`{"spell_casts":[]}`),
},
}
if err := recorder.NormalizeSucceeded("spells", spellnormalize.Key, normalizeDependencies, normalizeArtifact, nil); err != nil {
t.Fatal(err)
}
_, sameLoader, err := checkpointHandlersForRun(settings, Options{}, materialized, fingerprints, []byte("same input"), nil, nil, "", "", true)
if err != nil {
@@ -213,6 +248,9 @@ func TestChangedSemanticSpellCatalogFingerprintCannotResumeRecordedCheckpoint(t
if _, decision := sameLoader.Source(materialized.Input.Module); !decision.Reused {
t.Fatalf("same fingerprint decision = %#v, want reuse", decision)
}
if restored, decision := sameLoader.Normalize("spells", spellnormalize.Key, normalizeDependencies); !decision.Reused || string(restored.Output.Artifact.Content) != `{"spell_casts":[]}` {
t.Fatalf("same normalize checkpoint = %#v, decision=%#v, want reuse", restored, decision)
}
changed := append([]pipeline.CheckpointFingerprint(nil), fingerprints...)
changed[0].Value = "sha256:changed-effective-catalog"
_, changedLoader, err := checkpointHandlersForRun(settings, Options{}, materialized, changed, []byte("same input"), nil, nil, "", "", true)
@@ -222,6 +260,9 @@ func TestChangedSemanticSpellCatalogFingerprintCannotResumeRecordedCheckpoint(t
if _, decision := changedLoader.Source(materialized.Input.Module); decision.Reused {
t.Fatalf("changed fingerprint decision = %#v, want cold miss", decision)
}
if _, decision := changedLoader.Normalize("spells", spellnormalize.Key, normalizeDependencies); decision.Reused {
t.Fatalf("changed normalize fingerprint decision = %#v, want normalize checkpoint cold miss", decision)
}
}
func TestMaintainedProductionOverlayRunAlignsGroundingValidationAndProvenance(t *testing.T) {
@@ -255,21 +296,35 @@ func TestMaintainedProductionOverlayRunAlignsGroundingValidationAndProvenance(t
if got := stringValues(extractorMetadata["catalog_overlay_ids"]); !reflect.DeepEqual(got, []string{"notarius.example-campaign"}) {
t.Fatalf("catalog overlay IDs = %#v, want maintained overlay", got)
}
normalizerMetadata, ok := lane.Metadata["normalizer"].(map[string]any)
if !ok {
t.Fatalf("lane metadata = %#v, want normalizer metadata", lane.Metadata)
}
if normalizerMetadata["catalog_base_id"] != spellcatalog.SRD5E2014ID || !strings.HasPrefix(stringValue(normalizerMetadata["catalog_digest"]), "sha256:") || !reflect.DeepEqual(stringValues(normalizerMetadata["catalog_overlay_ids"]), []string{"notarius.example-campaign"}) {
t.Fatalf("normalizer catalog metadata = %#v, want base ID, semantic digest, and overlay IDs", normalizerMetadata)
}
if normalizerMetadata["catalog_digest"] != extractorMetadata["catalog_digest"] || !reflect.DeepEqual(stringValues(normalizerMetadata["catalog_overlay_ids"]), stringValues(extractorMetadata["catalog_overlay_ids"])) {
t.Fatalf("extractor metadata = %#v, normalizer metadata = %#v, want shared catalog identity", extractorMetadata, normalizerMetadata)
}
var catalogProvenance *artifacts.ReferenceProvenance
var catalogProvenances []artifacts.ReferenceProvenance
for index := range manifest.References {
reference := &manifest.References[index]
if reference.SlotName == spellcatalog.SpellCatalogReferenceSlot {
catalogProvenance = reference
break
catalogProvenances = append(catalogProvenances, *reference)
}
}
if catalogProvenance == nil {
t.Fatalf("manifest references = %#v, want spell catalog provenance", manifest.References)
if len(catalogProvenances) != 2 {
t.Fatalf("manifest references = %#v, want independently materialized extract and normalize catalog provenance", manifest.References)
}
overlayBytes := readRepositoryFile(t, "examples", "dnd-spells-catalog.json")
if catalogProvenance.Stage != "extract" || catalogProvenance.LaneID != "spells" || catalogProvenance.OriginType != "file" || catalogProvenance.MediaType != "application/json" || catalogProvenance.SizeBytes != int64(len(overlayBytes)) || catalogProvenance.Digest != digestBytes(overlayBytes) || !strings.Contains(catalogProvenance.OriginURI, "dnd-spells-catalog.json") {
t.Fatalf("catalog provenance = %#v, want extract origin, media, size, and raw digest", catalogProvenance)
for _, catalogProvenance := range catalogProvenances {
if catalogProvenance.Stage != "extract" && catalogProvenance.Stage != "normalize" {
t.Fatalf("catalog provenance = %#v, want extract or normalize scope", catalogProvenance)
}
if catalogProvenance.LaneID != "spells" || catalogProvenance.OriginType != "file" || catalogProvenance.MediaType != "application/json" || catalogProvenance.SizeBytes != int64(len(overlayBytes)) || catalogProvenance.Digest != digestBytes(overlayBytes) || !strings.Contains(catalogProvenance.OriginURI, "dnd-spells-catalog.json") {
t.Fatalf("catalog provenance = %#v, want raw overlay provenance in both scopes", catalogProvenance)
}
}
manifestBytes, err := json.Marshal(manifest)
if err != nil {
@@ -325,6 +380,16 @@ func catalogExtractorMetadata(t *testing.T, resolved pipeline.ResolvedPipeline)
return extractor.ManifestMetadata()
}
func catalogNormalizerMetadata(t *testing.T, resolved pipeline.ResolvedPipeline) map[string]any {
t.Helper()
lane := resolved.ArtifactLanes[0]
normalizer, err := spellnormalize.New(spellnormalize.Options{}, lane.NormalizeReferences.ReferenceSet)
if err != nil {
t.Fatalf("construct normalizer: %v", err)
}
return normalizer.ManifestMetadata()
}
func catalogReference(t *testing.T, resolved pipeline.ResolvedPipeline) artifacts.ReferenceProvenance {
t.Helper()
for _, reference := range pipeline.ReferenceProvenance(resolved) {