163 lines
8.4 KiB
Go
163 lines
8.4 KiB
Go
package itemevents
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
|
)
|
|
|
|
func TestExtractMapsAllEventKindsAndCanonicalizesEvidence(t *testing.T) {
|
|
quantity := 20
|
|
client := &fakeItemEventsLLMClient{response: extractionResponse{Events: []itemEventResponse{
|
|
{Name: "Torch", Kind: "lost", From: "party", SourceRefs: responseRefs(5, 5)},
|
|
{Name: "Moonblade", Kind: "transferred", From: "Aria", To: "Borin", SourceRefs: responseRefs(3, 3)},
|
|
{Name: "Hidden Cache", Kind: "discovered", SourceRefs: []itemEventSourceRefResponse{{StartSegment: 1, EndSegment: 1}, {StartSegment: 1, EndSegment: 1}}},
|
|
{Name: "Gold Pieces", Kind: "acquired", Quantity: &quantity, To: "party", SourceRefs: responseRefs(2, 2)},
|
|
{Name: "Healing Potion", Kind: "consumed", From: "party", SourceRefs: responseRefs(4, 4)},
|
|
}}}
|
|
|
|
result, err := newExtractor(t, client).Extract(context.Background(), extractionRequest())
|
|
if err != nil {
|
|
t.Fatalf("Extract() error = %v", err)
|
|
}
|
|
if got := []dnd.ItemEventKind{result.Value.Events[0].Kind, result.Value.Events[1].Kind, result.Value.Events[2].Kind, result.Value.Events[3].Kind, result.Value.Events[4].Kind}; !reflect.DeepEqual(got, []dnd.ItemEventKind{
|
|
dnd.ItemEventKindDiscovered, dnd.ItemEventKindAcquired, dnd.ItemEventKindTransferred, dnd.ItemEventKindConsumed, dnd.ItemEventKindLost,
|
|
}) {
|
|
t.Fatalf("event kinds = %#v, want source order", got)
|
|
}
|
|
acquired := result.Value.Events[1]
|
|
if acquired.Name != "Gold Pieces" || acquired.To != "party" || acquired.Quantity == nil || *acquired.Quantity != 20 {
|
|
t.Fatalf("acquired event = %#v", acquired)
|
|
}
|
|
if result.Value.Events[0].Quantity != nil || result.Value.Events[0].From != "" || result.Value.Events[0].To != "" {
|
|
t.Fatalf("discovered event = %#v, want omitted optional fields", result.Value.Events[0])
|
|
}
|
|
if refs := result.Value.Events[0].SourceRefs; len(refs) != 1 || refs[0] != (source.SourceRef{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 1}) {
|
|
t.Fatalf("source refs = %#v, want source ID attachment and deduplication", refs)
|
|
}
|
|
if acquired.Quantity == client.response.Events[3].Quantity {
|
|
t.Fatal("mapped quantity aliases private response")
|
|
}
|
|
if len(client.requests) != 1 {
|
|
t.Fatalf("LLM calls = %d, want 1", len(client.requests))
|
|
}
|
|
request := client.requests[0]
|
|
if request.StageName != Key || request.PromptID != PromptID || request.PromptVersion != SchemaVersion || request.ProfileID != "profile-item-events" || request.SessionID != "session-123" {
|
|
t.Fatalf("LLM request identity = %#v", request)
|
|
}
|
|
if transcript := request.Inputs["transcript"]; string(transcript.Content) != string(extractionRequest().Chunk.Content) || transcript.Name != "transcript" {
|
|
t.Fatalf("transcript input = %#v", transcript)
|
|
}
|
|
for _, name := range []string{"players", "party", "glossary"} {
|
|
if input := request.Inputs[name]; string(input.Content) != " " {
|
|
t.Fatalf("absent %s input = %#v, want retained empty prompt material", name, input)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractPreservesInvalidCandidatesAndEmptyResults(t *testing.T) {
|
|
client := &fakeItemEventsLLMClient{content: []byte(`{"events":[{"name":"","kind":"transferred","quantity":0,"from":"party","to":"Party","source_refs":[{"start_segment":99,"end_segment":-1}]}]}`)}
|
|
result, err := newExtractor(t, client).Extract(context.Background(), extractionRequest())
|
|
if err != nil {
|
|
t.Fatalf("Extract() error = %v", err)
|
|
}
|
|
event := result.Value.Events[0]
|
|
if event.Name != "" || event.Kind != "transferred" || event.Quantity == nil || *event.Quantity != 0 || event.From != "party" || event.To != "Party" {
|
|
t.Fatalf("invalid candidate = %#v, want values preserved", event)
|
|
}
|
|
if refs := event.SourceRefs; len(refs) != 1 || refs[0] != (source.SourceRef{SourceID: "session-alpha", StartUnitID: 99, EndUnitID: -1}) {
|
|
t.Fatalf("source refs = %#v, want invalid range preserved", refs)
|
|
}
|
|
|
|
empty, err := newExtractor(t, &fakeItemEventsLLMClient{response: extractionResponse{Events: []itemEventResponse{}}}).Extract(context.Background(), extractionRequest())
|
|
if err != nil || empty.Value.Events == nil || len(empty.Value.Events) != 0 {
|
|
t.Fatalf("empty provider result = %#v, %v; want valid empty list", empty.Value, err)
|
|
}
|
|
}
|
|
|
|
func TestExtractMapsNullableResponseFieldsToAbsentArtifactFields(t *testing.T) {
|
|
client := &fakeItemEventsLLMClient{content: []byte(`{"events":[{"name":"Hidden Cache","kind":"discovered","quantity":null,"from":null,"to":null,"source_refs":[{"start_segment":1,"end_segment":1}]}]}`)}
|
|
result, err := newExtractor(t, client).Extract(context.Background(), extractionRequest())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(result.Value.Events) != 1 {
|
|
t.Fatalf("events = %#v, want one", result.Value.Events)
|
|
}
|
|
event := result.Value.Events[0]
|
|
if event.Quantity != nil || event.From != "" || event.To != "" {
|
|
t.Fatalf("nullable response fields mapped to artifact values: %#v", event)
|
|
}
|
|
}
|
|
|
|
func TestExtractUsesSourceDocumentOrderForCandidates(t *testing.T) {
|
|
client := &fakeItemEventsLLMClient{response: extractionResponse{Events: []itemEventResponse{
|
|
{Name: "Later", Kind: "discovered", SourceRefs: responseRefs(10, 10)},
|
|
{Name: "Earlier", Kind: "discovered", SourceRefs: responseRefs(100, 100)},
|
|
}}}
|
|
req := extractionRequest()
|
|
req.Source.Units = []source.SourceUnit{{ID: 100}, {ID: 10}}
|
|
req.Chunk.Units = append([]source.SourceUnit(nil), req.Source.Units...)
|
|
req.Chunk.Ref = source.SourceRef{SourceID: req.Source.ID, StartUnitID: 100, EndUnitID: 10}
|
|
result, err := newExtractor(t, client).Extract(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := []string{result.Value.Events[0].Name, result.Value.Events[1].Name}; !reflect.DeepEqual(got, []string{"Earlier", "Later"}) {
|
|
t.Fatalf("event order = %#v, want source document order", got)
|
|
}
|
|
}
|
|
|
|
func TestExtractUsesOnlySupportedPromptInputs(t *testing.T) {
|
|
client := &fakeItemEventsLLMClient{response: extractionResponse{Events: []itemEventResponse{}}}
|
|
req := extractionRequest()
|
|
req.References = contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
|
|
"players": {Slot: contracts.ReferenceSlot{Name: "players"}, Items: []contracts.ReferenceItem{{SlotName: "players", Content: []byte("Dana: Aria")}}},
|
|
"party": {Slot: contracts.ReferenceSlot{Name: "party"}, Items: []contracts.ReferenceItem{{SlotName: "party", Content: []byte("Aria: ranger")}}},
|
|
"glossary": {Slot: contracts.ReferenceSlot{Name: "glossary"}, Items: []contracts.ReferenceItem{{SlotName: "glossary", Content: []byte("Moonblade: heirloom")}}},
|
|
"npcs": {Slot: contracts.ReferenceSlot{Name: "npc_registry"}, Items: []contracts.ReferenceItem{{SlotName: "npc_registry", Content: []byte("must not be used")}}},
|
|
}}
|
|
if _, err := newExtractor(t, client).Extract(context.Background(), req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inputs := client.requests[0].Inputs
|
|
if string(inputs["players"].Content) != "Dana: Aria" || string(inputs["party"].Content) != "Aria: ranger" || string(inputs["glossary"].Content) != "Moonblade: heirloom" {
|
|
t.Fatalf("reference inputs = %#v", inputs)
|
|
}
|
|
if _, ok := inputs["npcs"]; ok {
|
|
t.Fatalf("unexpected generated-lane input: %#v", inputs)
|
|
}
|
|
}
|
|
|
|
func TestExtractRetainsLocalErrorContext(t *testing.T) {
|
|
request := extractionRequest()
|
|
extractor := newExtractor(t, &fakeItemEventsLLMClient{})
|
|
var nilExtractor *Extractor
|
|
request.SourceInput = contracts.NewLLMInputMaterial("source", "application/json", []byte(`{"different":true}`), "sha256:other", "file:///other.json")
|
|
for _, test := range []struct {
|
|
name string
|
|
extractor *Extractor
|
|
req contracts.TypedExtractionRequest
|
|
want string
|
|
}{
|
|
{"nil extractor", nilExtractor, extractionRequest(), "extractor"},
|
|
{"nil client", &Extractor{}, extractionRequest(), "LLM client"},
|
|
{"preflight", extractor, request, "must match chunk"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if _, err := test.extractor.Extract(context.Background(), test.req); err == nil || !strings.Contains(err.Error(), "dnd item events") || !strings.Contains(err.Error(), test.want) {
|
|
t.Fatalf("Extract() error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
if _, err := newExtractor(t, &fakeItemEventsLLMClient{err: errors.New("provider unavailable")}).Extract(context.Background(), extractionRequest()); err == nil || !strings.Contains(err.Error(), "provider unavailable") {
|
|
t.Fatalf("provider error = %v", err)
|
|
}
|
|
}
|