package shared import ( "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" ) func TestPromptInputsBuildExpectedInputs(t *testing.T) { source := contracts.NewLLMInputMaterial("source", "application/json", []byte("source text"), "sha256:source", "file:///source.json") references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{ "players": slotWithContent("players", "Alice: Aria"), "party": slotWithContent("party", "Aria: cleric"), "glossary": slotWithContent("glossary", "Brightmantle: temple"), }} inputs := PromptInputs(source, references) for _, name := range []string{"transcript", "players", "party", "glossary"} { if _, ok := inputs[name]; !ok { t.Fatalf("PromptInputs() missing %q: %#v", name, inputs) } } if _, ok := inputs["roster"]; ok { t.Fatalf("PromptInputs() included roster input: %#v", inputs) } if got := inputs["transcript"].Name; got != "transcript" { t.Fatalf("transcript name = %q, want transcript", got) } if got := string(inputs["transcript"].Content); got != "source text" { t.Fatalf("transcript content = %q, want source text", got) } if got := string(inputs["players"].Content); got != "Alice: Aria" { t.Fatalf("players content = %q, want player reference", got) } if got := string(inputs["party"].Content); got != "Aria: cleric" { t.Fatalf("party content = %q, want party reference", got) } if got := string(inputs["glossary"].Content); got != "Brightmantle: temple" { t.Fatalf("glossary content = %q, want glossary reference", got) } } func TestPromptInputsUseRosterWhenPartyIsEmpty(t *testing.T) { inputs := PromptInputs(contracts.LLMInputMaterial{}, contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{ "party": {}, "roster": slotWithContent("roster", "Legacy roster text"), }}) if got := string(inputs["party"].Content); got != "Legacy roster text" { t.Fatalf("party content = %q, want roster fallback content", got) } if _, ok := inputs["roster"]; ok { t.Fatalf("PromptInputs() included roster input: %#v", inputs) } } func TestTranscriptPromptMaterialClonesSource(t *testing.T) { source := contracts.NewLLMInputMaterial("source", "text/plain", []byte("source text"), "sha256:source", "file:///source.txt") got := TranscriptPromptMaterial(source) if got.Name != "transcript" { t.Fatalf("Name = %q, want transcript", got.Name) } if got.MediaType != source.MediaType || got.Digest != source.Digest || got.OriginURI != source.OriginURI || got.SizeBytes != source.SizeBytes { t.Fatalf("TranscriptPromptMaterial() = %#v, want cloned metadata from %#v", got, source) } source.Content[0] = 'X' if string(got.Content) != "source text" { t.Fatalf("TranscriptPromptMaterial() reused content slice: %q", got.Content) } } func TestReferencePromptMaterialUsesTextPlainAndSingleReferenceMetadata(t *testing.T) { slot := contracts.ResolvedReferenceSlot{Items: []contracts.ReferenceItem{{ Content: []byte("single reference"), Digest: "sha256:reference", Origin: contracts.ReferenceOrigin{Type: "file", URI: "file:///reference.md"}, }}} got := ReferencePromptMaterial("party", slot) if got.Name != "party" || got.MediaType != "text/plain" { t.Fatalf("ReferencePromptMaterial() name/media = %q/%q, want party/text/plain", got.Name, got.MediaType) } if got.Digest != "sha256:reference" || got.OriginURI != "file:///reference.md" { t.Fatalf("ReferencePromptMaterial() digest/origin = %q/%q, want single reference metadata", got.Digest, got.OriginURI) } if string(got.Content) != "single reference" { t.Fatalf("ReferencePromptMaterial() content = %q, want raw single reference", got.Content) } } func TestReferencePromptMaterialOmitsAggregateMetadata(t *testing.T) { got := ReferencePromptMaterial("party", contracts.ResolvedReferenceSlot{Items: []contracts.ReferenceItem{ {Content: []byte("one"), Digest: "sha256:one", Origin: contracts.ReferenceOrigin{URI: "file:///one.md"}}, {Content: []byte("two"), Digest: "sha256:two", Origin: contracts.ReferenceOrigin{URI: "file:///two.md"}}, }}) if got.Digest != "" || got.OriginURI != "" { t.Fatalf("ReferencePromptMaterial() digest/origin = %q/%q, want empty aggregate metadata", got.Digest, got.OriginURI) } } func TestReferencePromptInputRendering(t *testing.T) { if got := string(ReferencePromptInput(contracts.ResolvedReferenceSlot{})); got != " " { t.Fatalf("empty rendering = %q, want single space", got) } if got := string(ReferencePromptInput(slotWithContent("party", "single reference"))); got != "single reference" { t.Fatalf("single rendering = %q, want raw content", got) } slot := contracts.ResolvedReferenceSlot{Items: []contracts.ReferenceItem{ { SlotName: "party", MediaType: "text/plain", Content: []byte("second"), Digest: "sha256:bbb", Origin: contracts.ReferenceOrigin{Type: "file", URI: "file:///b.txt"}, SizeBytes: 6, }, { SlotName: "party", MediaType: "text/plain", Content: []byte("first"), Digest: "sha256:aaa", Origin: contracts.ReferenceOrigin{Type: "file", URI: "file:///a.txt"}, SizeBytes: 5, }, }} first := string(ReferencePromptInput(slot)) second := string(ReferencePromptInput(slot)) if first != second { t.Fatalf("ReferencePromptInput() was not deterministic:\nfirst=%q\nsecond=%q", first, second) } if !strings.Contains(first, "Reference 1\nOrigin-Type: file\nOrigin-URI: file:///a.txt\nDigest: sha256:aaa\nMedia-Type: text/plain\nSize-Bytes: 5\n\nfirst") { t.Fatalf("first reference block = %q, want sorted first reference metadata", first) } if strings.Index(first, "first") > strings.Index(first, "second") { t.Fatalf("references were not sorted deterministically: %q", first) } } func slotWithContent(name string, content string) contracts.ResolvedReferenceSlot { return contracts.ResolvedReferenceSlot{ Slot: contracts.ReferenceSlot{Name: name}, Items: []contracts.ReferenceItem{{ SlotName: name, Content: []byte(content), }}, } }