Simplify D&D scene chunking responses
This commit is contained in:
@@ -110,32 +110,13 @@ func wantReferenceSlots() []contracts.ReferenceSlot {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanReturnsSceneRangesAndAnnotationsFromStructuredOutput(t *testing.T) {
|
||||
func TestPlanReturnsAnnotationFreeSceneRangesFromStructuredOutput(t *testing.T) {
|
||||
client := &fakeScenesLLMClient{
|
||||
response: chunkResponse{
|
||||
Scenes: []sceneResponse{
|
||||
{
|
||||
StartUnitID: shared.UnitRefFromInt(1),
|
||||
EndUnitID: shared.UnitRefFromInt(2),
|
||||
ShortTitle: " Goblin parley ",
|
||||
PrimaryMode: "Discussion",
|
||||
MainParticipants: []string{" Aria ", "Goblin scout"},
|
||||
Summary: " The party negotiates with a scout. ",
|
||||
BoundaryNote: " The scene covers the discussion before fighting starts. ",
|
||||
BoundaryConfidence: "High",
|
||||
},
|
||||
{
|
||||
StartUnitID: shared.UnitRefFromInt(3),
|
||||
EndUnitID: shared.UnitRefFromInt(4),
|
||||
ShortTitle: "Ambush at the gate",
|
||||
PrimaryMode: "Combat",
|
||||
MainParticipants: []string{"Aria", "Goblin ambushers"},
|
||||
Summary: "The goblins attack at the gate.",
|
||||
BoundaryNote: "Combat begins and resolves the immediate threat.",
|
||||
BoundaryConfidence: "Medium",
|
||||
},
|
||||
scene(1, 2),
|
||||
scene(3, 4),
|
||||
},
|
||||
BoundaryCaveats: []string{" The transition into combat is gradual. "},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -185,55 +166,67 @@ func TestPlanReturnsSceneRangesAndAnnotationsFromStructuredOutput(t *testing.T)
|
||||
t.Fatalf("ranges = %#v, want two", result.Plan.Ranges)
|
||||
}
|
||||
for i, want := range wantRanges {
|
||||
if result.Plan.Ranges[i].StartUnitID != want.StartUnitID || result.Plan.Ranges[i].EndUnitID != want.EndUnitID {
|
||||
got := result.Plan.Ranges[i]
|
||||
if got.StartUnitID != want.StartUnitID || got.EndUnitID != want.EndUnitID {
|
||||
t.Fatalf("range[%d] = %#v, want %#v", i, result.Plan.Ranges[i], want)
|
||||
}
|
||||
if got.Annotations != nil {
|
||||
t.Fatalf("range[%d] annotations = %#v, want absent", i, got.Annotations)
|
||||
}
|
||||
}
|
||||
var firstAnnotation map[string]any
|
||||
if err := json.Unmarshal(result.Plan.Ranges[0].Annotations[annotationNamespace], &firstAnnotation); err != nil {
|
||||
t.Fatalf("decode first scene annotation: %v", err)
|
||||
if result.Plan.Annotations != nil {
|
||||
t.Fatalf("plan annotations = %#v, want absent", result.Plan.Annotations)
|
||||
}
|
||||
wantFirst := map[string]any{
|
||||
"short_title": "Goblin parley", "primary_mode": "Discussion",
|
||||
"main_participants": []any{"Aria", "Goblin scout"},
|
||||
"summary": "The party negotiates with a scout.",
|
||||
"boundary_note": "The scene covers the discussion before fighting starts.",
|
||||
"boundary_confidence": "High",
|
||||
if result.Warnings != nil {
|
||||
t.Fatalf("warnings = %#v, want absent", result.Warnings)
|
||||
}
|
||||
if !reflect.DeepEqual(firstAnnotation, wantFirst) {
|
||||
t.Fatalf("first scene annotation = %#v, want %#v", firstAnnotation, wantFirst)
|
||||
}
|
||||
if len(firstAnnotation) != 6 {
|
||||
t.Fatalf("first scene annotation keys = %#v, want exact six fields", firstAnnotation)
|
||||
}
|
||||
var planAnnotation map[string]any
|
||||
if err := json.Unmarshal(result.Plan.Annotations[annotationNamespace], &planAnnotation); err != nil {
|
||||
t.Fatalf("decode plan annotation: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(planAnnotation, map[string]any{"boundary_caveats": []any{"The transition into combat is gradual."}}) || len(planAnnotation) != 1 {
|
||||
t.Fatalf("plan annotation = %#v, want normalized caveats only", planAnnotation)
|
||||
}
|
||||
if got := result.Warnings; len(got) != 1 ||
|
||||
got[0].Scope != Key ||
|
||||
got[0].ReasonCode != "scene_boundary_caveat" ||
|
||||
got[0].Message != "The transition into combat is gradual." {
|
||||
t.Fatalf("Warnings = %#v, want boundary caveat warning", got)
|
||||
}
|
||||
|
||||
func TestPlanUsesDocumentOrderForNonconsecutiveUnitIDs(t *testing.T) {
|
||||
doc := &source.SourceDocument{
|
||||
ID: "session-nonnumeric",
|
||||
Kind: "transcript",
|
||||
Format: "application/vnd.seriatim.minimal+json",
|
||||
Digest: "sha256:nonnumeric",
|
||||
Units: []source.SourceUnit{
|
||||
{ID: 10, Kind: "transcript_segment", Text: "The party arrives.", Ref: source.SourceRef{SourceID: "session-nonnumeric", StartUnitID: 10, EndUnitID: 10}},
|
||||
{ID: 3, Kind: "transcript_segment", Text: "The party explores.", Ref: source.SourceRef{SourceID: "session-nonnumeric", StartUnitID: 3, EndUnitID: 3}},
|
||||
{ID: 20, Kind: "transcript_segment", Text: "The party rests.", Ref: source.SourceRef{SourceID: "session-nonnumeric", StartUnitID: 20, EndUnitID: 20}},
|
||||
},
|
||||
}
|
||||
req := chunkRequest()
|
||||
req.Source = doc
|
||||
|
||||
t.Run("accepts document-ordered ranges", func(t *testing.T) {
|
||||
client := &fakeScenesLLMClient{response: chunkResponse{Scenes: []sceneResponse{
|
||||
scene(10, 3),
|
||||
scene(20, 20),
|
||||
}}}
|
||||
result, err := newChunker(t, client).Plan(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Plan() error = %v, want nil", err)
|
||||
}
|
||||
if got := result.Plan.Ranges; !reflect.DeepEqual(got, []source.ChunkRange{{StartUnitID: 10, EndUnitID: 3}, {StartUnitID: 20, EndUnitID: 20}}) {
|
||||
t.Fatalf("ranges = %#v, want source-document order", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects reversed document positions", func(t *testing.T) {
|
||||
client := &fakeScenesLLMClient{response: chunkResponse{Scenes: []sceneResponse{
|
||||
scene(3, 10),
|
||||
scene(20, 20),
|
||||
}}}
|
||||
_, err := newChunker(t, client).Plan(context.Background(), req)
|
||||
if err == nil || !strings.Contains(err.Error(), "appears after") {
|
||||
t.Fatalf("Plan() error = %v, want document-position reversal", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPlanPassesReferencesAsPromptInputs(t *testing.T) {
|
||||
client := &fakeScenesLLMClient{response: chunkResponse{
|
||||
Scenes: []sceneResponse{
|
||||
{
|
||||
StartUnitID: shared.UnitRefFromInt(1),
|
||||
EndUnitID: shared.UnitRefFromInt(4),
|
||||
ShortTitle: "Ambush",
|
||||
PrimaryMode: "Combat",
|
||||
MainParticipants: []string{"Aria"},
|
||||
Summary: "The party is ambushed.",
|
||||
BoundaryNote: "One scene covers the short fixture.",
|
||||
BoundaryConfidence: "High",
|
||||
},
|
||||
scene(1, 4),
|
||||
},
|
||||
}}
|
||||
req := chunkRequest()
|
||||
@@ -298,51 +291,6 @@ func TestPromptInputsMapLegacyRosterReferenceToParty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanRejectsWhitespaceOnlyBoundaryCaveats(t *testing.T) {
|
||||
client := &fakeScenesLLMClient{
|
||||
response: chunkResponse{
|
||||
Scenes: validSceneResponse().Scenes,
|
||||
BoundaryCaveats: []string{
|
||||
" ",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := newChunker(t, client).Plan(context.Background(), chunkRequest())
|
||||
if err == nil {
|
||||
t.Fatal("Plan() error = nil, want malformed structured output error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "dnd scenes chunker") || !strings.Contains(err.Error(), "malformed structured output") || !strings.Contains(err.Error(), "boundary_caveats[0]") {
|
||||
t.Fatalf("Plan() error = %q, want malformed boundary caveat context", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanDefensivelyCopiesAnnotationValues(t *testing.T) {
|
||||
doc := sceneSourceDocument()
|
||||
client := &fakeScenesLLMClient{response: validSceneResponse()}
|
||||
|
||||
result, err := newChunker(t, client).Plan(context.Background(), contracts.ChunkRequest{
|
||||
Source: doc,
|
||||
SourceInput: sceneSourceInput(),
|
||||
SessionID: "session-123",
|
||||
LLMProfile: "profile-scenes",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Plan() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
client.response.Scenes[0].MainParticipants[0] = "mutated"
|
||||
var annotation struct {
|
||||
MainParticipants []string `json:"main_participants"`
|
||||
}
|
||||
if err := json.Unmarshal(result.Plan.Ranges[0].Annotations[annotationNamespace], &annotation); err != nil {
|
||||
t.Fatalf("decode annotation: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(annotation.MainParticipants, []string{"Aria"}) {
|
||||
t.Fatalf("participants = %#v, want defensive copy", annotation.MainParticipants)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkerManifestMetadataIncludesPromptAndSchemaProvenance(t *testing.T) {
|
||||
metadata := newChunker(t, &fakeScenesLLMClient{}).ManifestMetadata()
|
||||
|
||||
@@ -455,38 +403,6 @@ func TestPlanRejectsMalformedStructuredOutput(t *testing.T) {
|
||||
}),
|
||||
want: "final scene",
|
||||
},
|
||||
{
|
||||
name: "empty metadata field",
|
||||
response: replaceScenes(validSceneResponse(), []sceneResponse{
|
||||
{
|
||||
StartUnitID: shared.UnitRefFromInt(1),
|
||||
EndUnitID: shared.UnitRefFromInt(4),
|
||||
ShortTitle: " ",
|
||||
PrimaryMode: "Narrative",
|
||||
MainParticipants: []string{"Aria"},
|
||||
Summary: "Summary.",
|
||||
BoundaryNote: "Note.",
|
||||
BoundaryConfidence: "High",
|
||||
},
|
||||
}),
|
||||
want: "short_title",
|
||||
},
|
||||
{
|
||||
name: "empty participant",
|
||||
response: replaceScenes(validSceneResponse(), []sceneResponse{
|
||||
{
|
||||
StartUnitID: shared.UnitRefFromInt(1),
|
||||
EndUnitID: shared.UnitRefFromInt(4),
|
||||
ShortTitle: "Title",
|
||||
PrimaryMode: "Narrative",
|
||||
MainParticipants: []string{"Aria", " "},
|
||||
Summary: "Summary.",
|
||||
BoundaryNote: "Note.",
|
||||
BoundaryConfidence: "High",
|
||||
},
|
||||
}),
|
||||
want: "main_participants",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -559,7 +475,6 @@ func validSceneResponse() chunkResponse {
|
||||
Scenes: []sceneResponse{
|
||||
scene(1, 4),
|
||||
},
|
||||
BoundaryCaveats: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,14 +485,8 @@ func replaceScenes(response chunkResponse, scenes []sceneResponse) chunkResponse
|
||||
|
||||
func scene(startUnitID int, endUnitID int) sceneResponse {
|
||||
return sceneResponse{
|
||||
StartUnitID: shared.UnitRefFromInt(startUnitID),
|
||||
EndUnitID: shared.UnitRefFromInt(endUnitID),
|
||||
ShortTitle: "Scene title",
|
||||
PrimaryMode: "Narrative",
|
||||
MainParticipants: []string{"Aria"},
|
||||
Summary: "A compact summary.",
|
||||
BoundaryNote: "The source units form one coherent scene.",
|
||||
BoundaryConfidence: "High",
|
||||
StartUnitID: shared.UnitRefFromInt(startUnitID),
|
||||
EndUnitID: shared.UnitRefFromInt(endUnitID),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user