131 lines
6.4 KiB
Go
131 lines
6.4 KiB
Go
package scenedescriptions
|
|
|
|
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 TestExtractMapsExactlyOneSceneToTheChunk(t *testing.T) {
|
|
client := &fakeSceneDescriptionsLLMClient{response: extractionResponse{
|
|
Kind: dnd.SceneKindCombat, Title: " Fight at the Watchtower ", Summary: " Bandits attack the party. ",
|
|
}}
|
|
request := extractionRequest()
|
|
result, err := newExtractor(t, client).Extract(context.Background(), request)
|
|
if err != nil {
|
|
t.Fatalf("Extract() error = %v, want nil", err)
|
|
}
|
|
want := dnd.SceneDescriptionList{Scenes: []dnd.SceneDescription{{
|
|
ID: request.Chunk.ID, SourceRef: request.Chunk.Ref, Kind: dnd.SceneKindCombat,
|
|
Title: "Fight at the Watchtower", Summary: "Bandits attack the party.",
|
|
}}}
|
|
if !reflect.DeepEqual(result.Value, want) {
|
|
t.Fatalf("Value = %#v, want %#v", result.Value, want)
|
|
}
|
|
if len(client.requests) != 1 {
|
|
t.Fatalf("LLM calls = %d, want 1", len(client.requests))
|
|
}
|
|
completed := client.requests[0]
|
|
if completed.StageName != Key || completed.PromptID != PromptID || completed.PromptVersion != SchemaVersion || completed.ProfileID != "profile-scene-descriptions" || completed.SessionID != "session-123" {
|
|
t.Fatalf("LLM request identity = %#v", completed)
|
|
}
|
|
transcript := completed.Inputs["transcript"]
|
|
if transcript.Name != "transcript" || transcript.MediaType != "application/json" || transcript.Digest != "sha256:chunk" || transcript.OriginURI != "file:///session-alpha.json" || string(transcript.Content) != string(request.Chunk.Content) {
|
|
t.Fatalf("transcript input = %#v, want chunk-scoped material", transcript)
|
|
}
|
|
}
|
|
|
|
func TestExtractPassesOptionalReferencesAndUsesEmptyPlaceholders(t *testing.T) {
|
|
client := &fakeSceneDescriptionsLLMClient{response: extractionResponse{Kind: dnd.SceneKindNarrative, Title: "Arrival", Summary: "The party arrives."}}
|
|
req := extractionRequest()
|
|
if _, err := newExtractor(t, client).Extract(context.Background(), req); err != nil {
|
|
t.Fatalf("Extract() error = %v, want nil", err)
|
|
}
|
|
for _, slot := range []string{"players", "party", "glossary"} {
|
|
if got := string(client.requests[0].Inputs[slot].Content); got != " " {
|
|
t.Fatalf("empty %s input = %q, want explicit placeholder", slot, got)
|
|
}
|
|
}
|
|
|
|
req.References = contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
|
|
"players": {Slot: contracts.ReferenceSlot{Name: "players"}, Items: []contracts.ReferenceItem{{SlotName: "players", Content: []byte("Dana: Mira")}}},
|
|
"party": {Slot: contracts.ReferenceSlot{Name: "party"}, Items: []contracts.ReferenceItem{{SlotName: "party", Content: []byte("Mira: ranger")}}},
|
|
"glossary": {Slot: contracts.ReferenceSlot{Name: "glossary"}, Items: []contracts.ReferenceItem{{SlotName: "glossary", Content: []byte("Greencloak: local title")}}},
|
|
}}
|
|
if _, err := newExtractor(t, client).Extract(context.Background(), req); err != nil {
|
|
t.Fatalf("Extract() error = %v, want nil", err)
|
|
}
|
|
inputs := client.requests[1].Inputs
|
|
if string(inputs["players"].Content) != "Dana: Mira" || string(inputs["party"].Content) != "Mira: ranger" || string(inputs["glossary"].Content) != "Greencloak: local title" {
|
|
t.Fatalf("reference inputs = %#v", inputs)
|
|
}
|
|
if strings.Contains(string(inputs["transcript"].Content), "local title") {
|
|
t.Fatal("transcript input contains reference content")
|
|
}
|
|
}
|
|
|
|
func TestExtractReturnsSemanticallyInvalidResponseForDeterministicValidation(t *testing.T) {
|
|
schema, err := loadResponseSchema()
|
|
if err != nil {
|
|
t.Fatalf("loadResponseSchema() error = %v", err)
|
|
}
|
|
if err := validateJSONSchema(t, map[string]any{"kind": "unrecognized", "title": " ", "summary": ""}, schema.JSONSchema); err != nil {
|
|
t.Fatalf("semantic candidate rejected by private schema: %v", err)
|
|
}
|
|
client := &fakeSceneDescriptionsLLMClient{response: extractionResponse{
|
|
Kind: dnd.SceneKind("unrecognized"), Title: " ", Summary: "",
|
|
}}
|
|
result, err := newExtractor(t, client).Extract(context.Background(), extractionRequest())
|
|
if err != nil {
|
|
t.Fatalf("Extract() error = %v, want nil", err)
|
|
}
|
|
scene := result.Value.Scenes[0]
|
|
if scene.Kind != dnd.SceneKind("unrecognized") || scene.Title != "" || scene.Summary != "" {
|
|
t.Fatalf("scene = %#v, want semantic candidates returned for deterministic validation", scene)
|
|
}
|
|
}
|
|
|
|
func TestExtractValidatesRequestsAndSurfacesProviderFailures(t *testing.T) {
|
|
request := extractionRequest()
|
|
extractor := newExtractor(t, &fakeSceneDescriptionsLLMClient{response: extractionResponse{Kind: dnd.SceneKindMeta, Title: "Table talk", Summary: "The group discusses rules."}})
|
|
var nilExtractor *Extractor
|
|
for _, test := range []struct {
|
|
name string
|
|
extractor *Extractor
|
|
req contracts.TypedExtractionRequest
|
|
want string
|
|
}{
|
|
{name: "nil extractor", extractor: nilExtractor, req: request, want: "extractor"},
|
|
{name: "nil LLM client", extractor: &Extractor{}, req: request, want: "LLM client"},
|
|
{name: "wrapped preflight failure", extractor: extractor, req: mismatchedSourceInputRequest(request), want: "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 scene descriptions") || !strings.Contains(err.Error(), test.want) {
|
|
t.Fatalf("Extract() error = %v, want contextual validation error", err)
|
|
}
|
|
})
|
|
}
|
|
_, err := newExtractor(t, &fakeSceneDescriptionsLLMClient{err: errors.New("provider unavailable")}).Extract(context.Background(), request)
|
|
if err == nil || !strings.Contains(err.Error(), "dnd scene descriptions") || !strings.Contains(err.Error(), "provider unavailable") {
|
|
t.Fatalf("provider Extract() error = %v, want contextual provider error", err)
|
|
}
|
|
}
|
|
|
|
func TestMapResponseUsesOnlyChunkIdentityAndRange(t *testing.T) {
|
|
chunk := &source.Chunk{ID: "source:chunk:7", Ref: source.SourceRef{SourceID: "source", StartUnitID: 12, EndUnitID: 14}}
|
|
got := mapResponse(extractionResponse{Kind: dnd.SceneKindRecap, Title: " Recap ", Summary: " Summary "}, chunk)
|
|
want := dnd.SceneDescriptionList{Scenes: []dnd.SceneDescription{{
|
|
ID: "source:chunk:7", SourceRef: source.SourceRef{SourceID: "source", StartUnitID: 12, EndUnitID: 14},
|
|
Kind: dnd.SceneKindRecap, Title: "Recap", Summary: "Summary",
|
|
}}}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("mapResponse() = %#v, want %#v", got, want)
|
|
}
|
|
}
|