package npcs import ( "crypto/sha256" "encoding/hex" "encoding/json" "reflect" "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" ) func TestBuildNormalizeContextMaterialsUsesDocumentOrderAndOwnedInputs(t *testing.T) { doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{ {ID: 40, Kind: "narration", Text: "zero"}, {ID: 10, Kind: "speech", Text: "one", Metadata: map[string]any{"speaker": map[string]any{"name": "Mira"}}}, {ID: 70, Kind: "speech", Text: "two"}, {ID: 20, Kind: "narration", Text: "three"}, {ID: 90, Kind: "speech", Text: "four"}, {ID: 30, Kind: "narration", Text: "five"}, }} records := []dnd.NPC{ {Name: "Mira Thorn", ID: "npc:sha256:internal", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 20}}}, {Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 90, EndUnitID: 90}}}, {Name: "Broken", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 10}}}, } before := append([]dnd.NPC(nil), records...) materials, ready, err := buildNormalizeContextMaterials(doc, records, 1) if err != nil || !ready { t.Fatalf("buildNormalizeContextMaterials() = %#v, %t, %v; want ready materials", materials, ready, err) } if !reflect.DeepEqual(records, before) { t.Fatalf("records mutated to %#v", records) } for _, material := range []struct { name string data []byte }{ {name: "candidates", data: materials.Candidates.Content}, {name: "transcript", data: materials.Transcript.Content}, } { if !json.Valid(material.data) || string(material.data) == "" { t.Fatalf("%s content = %q, want JSON", material.name, material.data) } digest := sha256.Sum256(material.data) wantDigest := "sha256:" + hex.EncodeToString(digest[:]) got := materials.Candidates if material.name == "transcript" { got = materials.Transcript } if got.Name != material.name || got.MediaType != "application/json" || got.OriginURI != "" || got.Digest != wantDigest { t.Fatalf("%s material = %#v, want owned JSON material", material.name, got) } } encoded := string(materials.Candidates.Content) + string(materials.Transcript.Content) if strings.Contains(encoded, "npc:sha256:internal") || strings.Contains(encoded, doc.ID) { t.Fatalf("model material leaked private identifier or source id: %s", encoded) } var candidates normalizeCandidateInput if err := json.Unmarshal(materials.Candidates.Content, &candidates); err != nil { t.Fatal(err) } if len(candidates.NPCs) != 2 || candidates.NPCs[0].Name != "Mira Thorn" || candidates.NPCs[0].SourceRefs[0] != (normalizeCandidateSourceRef{StartUnitID: 10, EndUnitID: 20}) { t.Fatalf("candidates = %#v, want two valid current-order candidates", candidates) } var transcript normalizeTranscriptInput if err := json.Unmarshal(materials.Transcript.Content, &transcript); err != nil { t.Fatal(err) } if len(transcript.Windows) != 1 || len(transcript.Windows[0].Units) != 6 { t.Fatalf("transcript = %#v, want one coalesced window", transcript) } units := transcript.Windows[0].Units for index, wantID := range []int{40, 10, 70, 20, 90, 30} { if units[index].ID != wantID { t.Fatalf("unit %d id = %d, want source-order id %d", index, units[index].ID, wantID) } } if units[0].Cited || !units[1].Cited || !units[2].Cited || !units[3].Cited || !units[4].Cited || units[5].Cited { t.Fatalf("citation markers = %#v, want original ranges only", units) } if units[1].Metadata["speaker"].(map[string]any)["name"] != "Mira" { t.Fatalf("metadata = %#v, want copied generic metadata", units[1].Metadata) } windows, err := normalizeContextWindows(doc.Units, []sourceInterval{{start: 1, end: 1}}, make([]bool, len(doc.Units))) if err != nil { t.Fatal(err) } windows[0].Units[0].Metadata["speaker"].(map[string]any)["name"] = "changed" if doc.Units[1].Metadata["speaker"].(map[string]any)["name"] != "Mira" { t.Fatal("copied metadata aliases source document") } } func TestBuildNormalizeContextMaterialsExcludesInvalidReferencesAndCoalescesAdjacentWindows(t *testing.T) { doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{ {ID: 9}, {ID: 3}, {ID: 8}, {ID: 1}, {ID: 7}, {ID: 2}, {ID: 6}, }} records := []dnd.NPC{ {Name: "One", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 3, EndUnitID: 3}}}, {Name: "Two", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 8, EndUnitID: 8}}}, {Name: "Blank"}, {Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 99, EndUnitID: 99}}}, {Name: "Foreign", SourceRefs: []source.SourceRef{{SourceID: "other", StartUnitID: 1, EndUnitID: 1}}}, {Name: "Reversed", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 6, EndUnitID: 3}}}, } materials, ready, err := buildNormalizeContextMaterials(doc, records, 0) if err != nil || !ready { t.Fatalf("buildNormalizeContextMaterials() error = %v, ready = %t", err, ready) } var candidates normalizeCandidateInput if err := json.Unmarshal(materials.Candidates.Content, &candidates); err != nil { t.Fatal(err) } if got := []string{candidates.NPCs[0].Name, candidates.NPCs[1].Name}; !reflect.DeepEqual(got, []string{"One", "Two"}) { t.Fatalf("candidate names = %#v, want only valid records", got) } var transcript normalizeTranscriptInput if err := json.Unmarshal(materials.Transcript.Content, &transcript); err != nil { t.Fatal(err) } if len(transcript.Windows) != 1 || len(transcript.Windows[0].Units) != 2 { t.Fatalf("windows = %#v, want adjacent cited units coalesced", transcript.Windows) } if transcript.Windows[0].Units[0].ID != 3 || transcript.Windows[0].Units[1].ID != 8 { t.Fatalf("window units = %#v, want document-order adjacent units", transcript.Windows[0].Units) } }