From 5ad661f95f811674c40378041bede1c4c2c8451c Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 26 Jul 2026 02:38:03 +0000 Subject: [PATCH] Redact NPC normalization context errors --- .../dnd/normalize/npcs/context_material.go | 6 +-- .../npcs/semantic_normalizer_test.go | 38 +++++++++++++++---- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/internal/modules/dnd/normalize/npcs/context_material.go b/internal/modules/dnd/normalize/npcs/context_material.go index 2e5e4c1..f6ce328 100644 --- a/internal/modules/dnd/normalize/npcs/context_material.go +++ b/internal/modules/dnd/normalize/npcs/context_material.go @@ -119,15 +119,15 @@ func buildNormalizeContextMaterials(doc *source.SourceDocument, records []dnd.NP windows, err := normalizeContextWindows(doc.Units, coalesceIntervals(intervals), cited) if err != nil { - return normalizeContextMaterials{}, false, fmt.Errorf("build NPC normalization context: copy source metadata: %w", err) + return normalizeContextMaterials{}, false, fmt.Errorf("build NPC normalization context: invalid source metadata") } candidateContent, err := json.Marshal(normalizeCandidateInput{NPCs: candidates}) if err != nil { - return normalizeContextMaterials{}, false, fmt.Errorf("build NPC normalization context: encode candidates: %w", err) + return normalizeContextMaterials{}, false, fmt.Errorf("build NPC normalization context: invalid candidate material") } transcriptContent, err := json.Marshal(normalizeTranscriptInput{Windows: windows}) if err != nil { - return normalizeContextMaterials{}, false, fmt.Errorf("build NPC normalization context: encode transcript: %w", err) + return normalizeContextMaterials{}, false, fmt.Errorf("build NPC normalization context: invalid transcript material") } return normalizeContextMaterials{ Candidates: newNormalizeInputMaterial("candidates", candidateContent), diff --git a/internal/modules/dnd/normalize/npcs/semantic_normalizer_test.go b/internal/modules/dnd/normalize/npcs/semantic_normalizer_test.go index 2cbdd12..8b89ac2 100644 --- a/internal/modules/dnd/normalize/npcs/semantic_normalizer_test.go +++ b/internal/modules/dnd/normalize/npcs/semantic_normalizer_test.go @@ -136,17 +136,39 @@ func TestNormalizeInvalidStructuredOutputAndOperationalErrorsRemainDistinct(t *t } } -func TestNormalizeRejectsContextEncodingFailuresWithoutLeakingContent(t *testing.T) { +func TestNormalizeRedactsContextMaterialFailures(t *testing.T) { client := &recordingNPCNormalizerClient{} normalizer := newNormalizer(t, client) - doc := semanticDocument() - doc.Units[0].Metadata = map[string]any{"invalid": math.NaN()} - input := dnd.NPCList{NPCs: []dnd.NPC{ - {Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}, - {Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}}, + const ( + metadataKey = "normalizer-sensitive-metadata-key" + metadataValue = "normalizer-sensitive-metadata-value" + sourceID = "normalizer-sensitive-source-id" + originPath = "file:///normalizer-sensitive-origin.json" + transcript = "normalizer-sensitive-transcript" + firstName = "Normalizer Sensitive" + secondName = "Normalizer Sensitive Alias" + ) + doc := &source.SourceDocument{ID: sourceID, Units: []source.SourceUnit{ + {ID: 10, Kind: "speech", Text: transcript, Metadata: map[string]any{metadataKey: math.NaN(), "value": metadataValue}}, + {ID: 20, Kind: "speech", Text: "other context"}, }} - if _, err := normalizer.Normalize(context.Background(), normalizeRequestWithSource(input, doc)); err == nil || !strings.Contains(err.Error(), "build semantic context") || strings.Contains(err.Error(), doc.Units[0].Text) || len(client.requests) != 0 { - t.Fatalf("Normalize() error = %v, calls = %d; want safe preparation error before completion", err, len(client.requests)) + input := dnd.NPCList{NPCs: []dnd.NPC{ + {Name: firstName, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}, + {Name: secondName, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}}, + }} + request := normalizeRequestWithSource(input, doc) + request.SourceInput = contracts.NewLLMInputMaterial("source", "application/json", []byte(transcript), "sha256:test", originPath) + _, err := normalizer.Normalize(context.Background(), request) + if err == nil || !strings.Contains(err.Error(), "build NPC normalization context: invalid source metadata") { + t.Fatalf("Normalize() error = %v; want content-safe context-material failure", err) + } + for _, forbidden := range []string{metadataKey, metadataValue, transcript, firstName, secondName, sourceID, originPath, "float64", "non-finite"} { + if strings.Contains(err.Error(), forbidden) { + t.Fatalf("Normalize() error leaked %q: %v", forbidden, err) + } + } + if len(client.requests) != 0 { + t.Fatalf("completion calls = %d, want context failure before completion", len(client.requests)) } }