Generate and materialize canonical chunk plans
This commit is contained in:
@@ -25,7 +25,7 @@ func TestNewModuleSpecAndRegister(t *testing.T) {
|
||||
Key: Key,
|
||||
Stage: pipeline.StageChunk,
|
||||
Requires: []string{"source.transcript"},
|
||||
Provides: []string{"chunks", "chunks.scenes"},
|
||||
Provides: []string{"chunks"},
|
||||
ReferenceSlots: wantReferenceSlots(),
|
||||
}
|
||||
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
|
||||
@@ -110,7 +110,7 @@ func wantReferenceSlots() []contracts.ReferenceSlot {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkReturnsSceneChunksFromStructuredOutput(t *testing.T) {
|
||||
func TestPlanReturnsSceneRangesAndAnnotationsFromStructuredOutput(t *testing.T) {
|
||||
client := &fakeScenesLLMClient{
|
||||
response: chunkResponse{
|
||||
Scenes: []sceneResponse{
|
||||
@@ -139,9 +139,9 @@ func TestChunkReturnsSceneChunksFromStructuredOutput(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result, err := newChunker(t, client).Chunk(context.Background(), chunkRequest())
|
||||
result, err := newChunker(t, client).Plan(context.Background(), chunkRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("Chunk() error = %v, want nil", err)
|
||||
t.Fatalf("Plan() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
if len(client.requests) != 1 {
|
||||
@@ -177,36 +177,41 @@ func TestChunkReturnsSceneChunksFromStructuredOutput(t *testing.T) {
|
||||
t.Fatalf("glossary input = %q, want empty reference placeholder", got)
|
||||
}
|
||||
|
||||
if got := chunkIDs(result.Chunks); !reflect.DeepEqual(got, []string{"scene-000001", "scene-000002"}) {
|
||||
t.Fatalf("chunk IDs = %#v, want deterministic scene IDs", got)
|
||||
if result.Plan.SourceDigest != "sha256:source" {
|
||||
t.Fatalf("SourceDigest = %q, want source digest", result.Plan.SourceDigest)
|
||||
}
|
||||
gotUnits := [][]int{unitIDs(result.Chunks[0].Units), unitIDs(result.Chunks[1].Units)}
|
||||
wantUnits := [][]int{{1, 2}, {3, 4}}
|
||||
if !reflect.DeepEqual(gotUnits, wantUnits) {
|
||||
t.Fatalf("chunk units = %#v, want %#v", gotUnits, wantUnits)
|
||||
wantRanges := []source.ChunkRange{{StartUnitID: 1, EndUnitID: 2}, {StartUnitID: 3, EndUnitID: 4}}
|
||||
if len(result.Plan.Ranges) != len(wantRanges) {
|
||||
t.Fatalf("ranges = %#v, want two", result.Plan.Ranges)
|
||||
}
|
||||
first := result.Chunks[0]
|
||||
if first.SourceID != "session-alpha" || first.Index != 0 {
|
||||
t.Fatalf("first chunk = %#v, want source and index fields", first)
|
||||
for i, want := range wantRanges {
|
||||
if result.Plan.Ranges[i].StartUnitID != want.StartUnitID || result.Plan.Ranges[i].EndUnitID != want.EndUnitID {
|
||||
t.Fatalf("range[%d] = %#v, want %#v", i, result.Plan.Ranges[i], want)
|
||||
}
|
||||
}
|
||||
if first.Ref != (source.SourceRef{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}) {
|
||||
t.Fatalf("first ref = %#v, want session-alpha:1-2", first.Ref)
|
||||
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 first.MediaType != "application/json" || len(first.Content) == 0 {
|
||||
t.Fatalf("first payload = media type %q length %d, want JSON content", first.MediaType, len(first.Content))
|
||||
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 first.Metadata["scene_title"] != "Goblin parley" ||
|
||||
first.Metadata["primary_mode"] != "Discussion" ||
|
||||
first.Metadata["summary"] != "The party negotiates with a scout." ||
|
||||
first.Metadata["boundary_note"] != "The scene covers the discussion before fighting starts." ||
|
||||
first.Metadata["boundary_confidence"] != "High" ||
|
||||
first.Metadata["start_unit_id"] != 1 ||
|
||||
first.Metadata["end_unit_id"] != 2 ||
|
||||
first.Metadata["unit_count"] != 2 {
|
||||
t.Fatalf("first metadata = %#v, want scene metadata", first.Metadata)
|
||||
if !reflect.DeepEqual(firstAnnotation, wantFirst) {
|
||||
t.Fatalf("first scene annotation = %#v, want %#v", firstAnnotation, wantFirst)
|
||||
}
|
||||
if got, ok := first.Metadata["main_participants"].([]string); !ok || !reflect.DeepEqual(got, []string{"Aria", "Goblin scout"}) {
|
||||
t.Fatalf("main_participants = %#v, want trimmed participant slice", first.Metadata["main_participants"])
|
||||
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 ||
|
||||
@@ -216,7 +221,7 @@ func TestChunkReturnsSceneChunksFromStructuredOutput(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkPassesReferencesAsPromptInputs(t *testing.T) {
|
||||
func TestPlanPassesReferencesAsPromptInputs(t *testing.T) {
|
||||
client := &fakeScenesLLMClient{response: chunkResponse{
|
||||
Scenes: []sceneResponse{
|
||||
{
|
||||
@@ -255,8 +260,8 @@ func TestChunkPassesReferencesAsPromptInputs(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := newChunker(t, client).Chunk(context.Background(), req); err != nil {
|
||||
t.Fatalf("Chunk() error = %v, want nil", err)
|
||||
if _, err := newChunker(t, client).Plan(context.Background(), req); err != nil {
|
||||
t.Fatalf("Plan() error = %v, want nil", err)
|
||||
}
|
||||
request := client.requests[0]
|
||||
if got := string(request.Inputs["players"].Content); got != "Alice: Aria" {
|
||||
@@ -293,7 +298,7 @@ func TestPromptInputsMapLegacyRosterReferenceToParty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkRejectsWhitespaceOnlyBoundaryCaveats(t *testing.T) {
|
||||
func TestPlanRejectsWhitespaceOnlyBoundaryCaveats(t *testing.T) {
|
||||
client := &fakeScenesLLMClient{
|
||||
response: chunkResponse{
|
||||
Scenes: validSceneResponse().Scenes,
|
||||
@@ -303,46 +308,38 @@ func TestChunkRejectsWhitespaceOnlyBoundaryCaveats(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
_, err := newChunker(t, client).Chunk(context.Background(), chunkRequest())
|
||||
_, err := newChunker(t, client).Plan(context.Background(), chunkRequest())
|
||||
if err == nil {
|
||||
t.Fatal("Chunk() error = nil, want malformed structured output error")
|
||||
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("Chunk() error = %q, want malformed boundary caveat context", err.Error())
|
||||
t.Fatalf("Plan() error = %q, want malformed boundary caveat context", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkDefensivelyCopiesSourceUnitsAndMetadata(t *testing.T) {
|
||||
func TestPlanDefensivelyCopiesAnnotationValues(t *testing.T) {
|
||||
doc := sceneSourceDocument()
|
||||
client := &fakeScenesLLMClient{response: validSceneResponse()}
|
||||
|
||||
result, err := newChunker(t, client).Chunk(context.Background(), contracts.ChunkRequest{
|
||||
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("Chunk() error = %v, want nil", err)
|
||||
t.Fatalf("Plan() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
doc.Units[0].ID = 99
|
||||
doc.Units[0].Ref.SourceID = "mutated"
|
||||
doc.Units[0].Metadata["speaker"] = "mutated"
|
||||
client.response.Scenes[0].MainParticipants[0] = "mutated"
|
||||
|
||||
if result.Chunks[0].Units[0].ID != 1 {
|
||||
t.Fatalf("chunk unit ID changed after source mutation: %#v", result.Chunks[0].Units[0])
|
||||
var annotation struct {
|
||||
MainParticipants []string `json:"main_participants"`
|
||||
}
|
||||
if got := result.Chunks[0].Units[0].Ref.SourceID; got != "session-alpha" {
|
||||
t.Fatalf("chunk unit ref changed after source mutation: %#v", result.Chunks[0].Units[0].Ref)
|
||||
if err := json.Unmarshal(result.Plan.Ranges[0].Annotations[annotationNamespace], &annotation); err != nil {
|
||||
t.Fatalf("decode annotation: %v", err)
|
||||
}
|
||||
if result.Chunks[0].Units[0].Metadata["speaker"] != "Alice" {
|
||||
t.Fatalf("chunk unit metadata changed after source mutation: %#v", result.Chunks[0].Units[0].Metadata)
|
||||
}
|
||||
participants, ok := result.Chunks[0].Metadata["main_participants"].([]string)
|
||||
if !ok || participants[0] != "Aria" {
|
||||
t.Fatalf("participants = %#v, want defensive copy", result.Chunks[0].Metadata["main_participants"])
|
||||
if !reflect.DeepEqual(annotation.MainParticipants, []string{"Aria"}) {
|
||||
t.Fatalf("participants = %#v, want defensive copy", annotation.MainParticipants)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +372,7 @@ func TestChunkerManifestMetadataIncludesPromptAndSchemaProvenance(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkRejectsInvalidRequests(t *testing.T) {
|
||||
func TestPlanRejectsInvalidRequests(t *testing.T) {
|
||||
validClient := &fakeScenesLLMClient{response: validSceneResponse()}
|
||||
validReq := chunkRequest()
|
||||
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||
@@ -402,18 +399,18 @@ func TestChunkRejectsInvalidRequests(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := tt.chunker.Chunk(tt.ctx, tt.req)
|
||||
_, err := tt.chunker.Plan(tt.ctx, tt.req)
|
||||
if err == nil {
|
||||
t.Fatal("Chunk() error = nil, want error")
|
||||
t.Fatal("Plan() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "dnd scenes") || !strings.Contains(err.Error(), tt.want) {
|
||||
t.Fatalf("Chunk() error = %q, want module context and %q", err.Error(), tt.want)
|
||||
t.Fatalf("Plan() error = %q, want module context and %q", err.Error(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkRejectsMalformedStructuredOutput(t *testing.T) {
|
||||
func TestPlanRejectsMalformedStructuredOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
response chunkResponse
|
||||
@@ -495,26 +492,26 @@ func TestChunkRejectsMalformedStructuredOutput(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := &fakeScenesLLMClient{response: tt.response}
|
||||
_, err := newChunker(t, client).Chunk(context.Background(), chunkRequest())
|
||||
_, err := newChunker(t, client).Plan(context.Background(), chunkRequest())
|
||||
if err == nil {
|
||||
t.Fatal("Chunk() error = nil, want error")
|
||||
t.Fatal("Plan() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "dnd scenes") || !strings.Contains(err.Error(), tt.want) {
|
||||
t.Fatalf("Chunk() error = %q, want module context and %q", err.Error(), tt.want)
|
||||
t.Fatalf("Plan() error = %q, want module context and %q", err.Error(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkWrapsLLMClientError(t *testing.T) {
|
||||
func TestPlanWrapsLLMClientError(t *testing.T) {
|
||||
client := &fakeScenesLLMClient{err: errors.New("provider unavailable")}
|
||||
|
||||
_, err := newChunker(t, client).Chunk(context.Background(), chunkRequest())
|
||||
_, err := newChunker(t, client).Plan(context.Background(), chunkRequest())
|
||||
if err == nil {
|
||||
t.Fatal("Chunk() error = nil, want LLM error")
|
||||
t.Fatal("Plan() error = nil, want LLM error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "dnd scenes") || !strings.Contains(err.Error(), "provider unavailable") {
|
||||
t.Fatalf("Chunk() error = %q, want wrapped LLM context", err.Error())
|
||||
t.Fatalf("Plan() error = %q, want wrapped LLM context", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user