package contracts import ( "context" "reflect" "strings" "testing" "gitea.maximumdirect.net/eric/audita/internal/core/chunking" "gitea.maximumdirect.net/eric/audita/internal/core/schema" "gitea.maximumdirect.net/eric/audita/internal/framework/proposals" ) // testChunkProposalHarness is a test-only helper that composes existing // deterministic chunking and proposal-application primitives. // It isolates proposal/composition behavior from full runner orchestration. type testChunkProposalHarness struct { module TranscriptModule } func (h testChunkProposalHarness) collectEnrichedProposals( ctx context.Context, transcript *schema.Transcript, sections []chunking.Section, ) ([]proposals.EnrichedCorrectionProposal, error) { runSpecs, err := ResolveModuleRunSpecs([]string{h.module.Key()}) if err != nil { return nil, err } runSpec := runSpecs[0] runSpec.ReplacementPolicy = h.module.ReplacementPolicy() out := make([]proposals.EnrichedCorrectionProposal, 0) nextIndex := 0 for _, section := range sections { sectionMeta := SectionMetadataFromSection(section) base, err := h.module.Propose(ctx, ProposalRequest{ ExecutionContext: ExecutionContext{ WorkingTranscript: transcript, Section: §ionMeta, }, RunSpec: runSpec, }) if err != nil { return nil, err } for _, proposal := range base { sectionIndex := section.Index out = append(out, proposals.EnrichedCorrectionProposal{ CorrectionProposal: proposal, ProposalMetadata: proposals.ProposalMetadata{ ProposalIndex: nextIndex, ModuleKey: runSpec.ModuleKey, ModuleInstance: runSpec.InstanceName, SectionIndex: §ionIndex, }, }) nextIndex++ } } return out, nil } type deterministicReplacementRule struct { OriginalText string CorrectedText string Confidence float64 } type deterministicFakeModule struct { key string replacementPolicy proposals.ReplacementPolicy rules []deterministicReplacementRule } func (m deterministicFakeModule) Key() string { return m.key } func (m deterministicFakeModule) ReplacementPolicy() proposals.ReplacementPolicy { return m.replacementPolicy } func (m deterministicFakeModule) Validators() []Validator { return nil } func (m deterministicFakeModule) Propose(ctx context.Context, req ProposalRequest) ([]proposals.CorrectionProposal, error) { _ = ctx if req.WorkingTranscript == nil || req.Section == nil { return []proposals.CorrectionProposal{}, nil } out := make([]proposals.CorrectionProposal, 0) for _, seg := range req.WorkingTranscript.Segments { if seg.ID < req.Section.StartSegmentID || seg.ID > req.Section.EndSegmentID { continue } for _, rule := range m.rules { if strings.Contains(seg.Text, rule.OriginalText) { out = append(out, proposals.CorrectionProposal{ TargetSegmentID: seg.ID, OriginalText: rule.OriginalText, CorrectedText: rule.CorrectedText, Confidence: rule.Confidence, }) } } } return out, nil } func TestChunkProposalMetadataAssociation(t *testing.T) { transcript := integrationTranscriptFixture() sections := mustChunkFixtureTranscript(t, transcript) module := deterministicFakeModule{ key: "fake_rewriter", replacementPolicy: proposals.ReplacementPolicyRequireUnique, rules: []deterministicReplacementRule{ {OriginalText: "alpha", CorrectedText: "ALPHA", Confidence: 0.9}, {OriginalText: "charlie", CorrectedText: "CHARLIE", Confidence: 0.9}, }, } harness := testChunkProposalHarness{module: module} enriched, err := harness.collectEnrichedProposals(context.Background(), transcript, sections) if err != nil { t.Fatalf("collectEnrichedProposals failed: %v", err) } if len(sections) != 2 { t.Fatalf("expected 2 sections, got %d", len(sections)) } if len(enriched) != 2 { t.Fatalf("expected 2 enriched proposals, got %d", len(enriched)) } if enriched[0].SectionIndex == nil || *enriched[0].SectionIndex != 0 { t.Fatalf("expected first proposal section_index=0, got %v", enriched[0].SectionIndex) } if enriched[1].SectionIndex == nil || *enriched[1].SectionIndex != 1 { t.Fatalf("expected second proposal section_index=1, got %v", enriched[1].SectionIndex) } } func TestDeterministicProposalIndexAssignment(t *testing.T) { transcript := integrationTranscriptFixture() sections := mustChunkFixtureTranscript(t, transcript) module := deterministicFakeModule{ key: "fake_rewriter", replacementPolicy: proposals.ReplacementPolicyRequireUnique, rules: []deterministicReplacementRule{ {OriginalText: "alpha", CorrectedText: "ALPHA", Confidence: 0.9}, {OriginalText: "alpha one", CorrectedText: "ALPHA-ONE", Confidence: 0.9}, {OriginalText: "charlie", CorrectedText: "CHARLIE", Confidence: 0.9}, }, } harness := testChunkProposalHarness{module: module} first, err := harness.collectEnrichedProposals(context.Background(), transcript, sections) if err != nil { t.Fatalf("first collect failed: %v", err) } second, err := harness.collectEnrichedProposals(context.Background(), transcript, sections) if err != nil { t.Fatalf("second collect failed: %v", err) } if len(first) != len(second) { t.Fatalf("proposal length mismatch: %d vs %d", len(first), len(second)) } for i := range first { if first[i].ProposalIndex != i { t.Fatalf("expected first pass proposal index %d at slot %d, got %d", i, i, first[i].ProposalIndex) } if second[i].ProposalIndex != i { t.Fatalf("expected second pass proposal index %d at slot %d, got %d", i, i, second[i].ProposalIndex) } if first[i].TargetSegmentID != second[i].TargetSegmentID || first[i].OriginalText != second[i].OriginalText || first[i].CorrectedText != second[i].CorrectedText { t.Fatalf("proposal mismatch at index %d", i) } } } func TestApplyFakeProposalsAcrossMultipleChunksWithStaleSkip(t *testing.T) { transcript := integrationTranscriptFixture() sections := mustChunkFixtureTranscript(t, transcript) module := deterministicFakeModule{ key: "fake_rewriter", replacementPolicy: proposals.ReplacementPolicyRequireUnique, rules: []deterministicReplacementRule{ {OriginalText: "alpha", CorrectedText: "ALPHA", Confidence: 0.9}, {OriginalText: "alpha one", CorrectedText: "ALPHA-ONE", Confidence: 0.9}, {OriginalText: "charlie", CorrectedText: "CHARLIE", Confidence: 0.9}, }, } harness := testChunkProposalHarness{module: module} enriched, err := harness.collectEnrichedProposals(context.Background(), transcript, sections) if err != nil { t.Fatalf("collectEnrichedProposals failed: %v", err) } result := proposals.ApplyProposals(transcript, enriched, module.ReplacementPolicy()) if len(result.Applied) != 2 { t.Fatalf("expected 2 applied changes, got %d", len(result.Applied)) } if len(result.Skipped) != 1 { t.Fatalf("expected 1 skipped change, got %d", len(result.Skipped)) } if result.Skipped[0].SkipReason != proposals.SkipReasonMissingOriginalText { t.Fatalf("expected stale skip reason %q, got %q", proposals.SkipReasonMissingOriginalText, result.Skipped[0].SkipReason) } if got := result.Transcript.Segments[0].Text; got != "ALPHA one" { t.Fatalf("unexpected segment 1 text: %q", got) } if got := result.Transcript.Segments[2].Text; got != "CHARLIE three" { t.Fatalf("unexpected segment 3 text: %q", got) } } func TestChunkProposalCompositionDoesNotMutateNormalizedTranscript(t *testing.T) { transcript := integrationTranscriptFixture() before := cloneTranscriptForComparison(transcript) sections := mustChunkFixtureTranscript(t, transcript) module := deterministicFakeModule{ key: "fake_rewriter", replacementPolicy: proposals.ReplacementPolicyRequireUnique, rules: []deterministicReplacementRule{ {OriginalText: "alpha", CorrectedText: "ALPHA", Confidence: 0.9}, {OriginalText: "charlie", CorrectedText: "CHARLIE", Confidence: 0.9}, }, } harness := testChunkProposalHarness{module: module} enriched, err := harness.collectEnrichedProposals(context.Background(), transcript, sections) if err != nil { t.Fatalf("collectEnrichedProposals failed: %v", err) } _ = proposals.ApplyProposals(transcript, enriched, module.ReplacementPolicy()) if !reflect.DeepEqual(transcript, before) { t.Fatalf("normalized transcript mutated\nbefore=%+v\nafter=%+v", before, transcript) } } func integrationTranscriptFixture() *schema.Transcript { return &schema.Transcript{ Segments: []schema.Segment{ {ID: 1, Speaker: "A", Start: 0, End: 1, Text: "alpha one"}, {ID: 2, Speaker: "A", Start: 1, End: 2, Text: "bravo two"}, {ID: 3, Speaker: "B", Start: 2, End: 3, Text: "charlie three"}, {ID: 4, Speaker: "B", Start: 3, End: 4, Text: "delta four"}, }, } } func mustChunkFixtureTranscript(t *testing.T, transcript *schema.Transcript) []chunking.Section { t.Helper() chunker := chunking.NewChunkerWithEstimator( chunking.ChunkingConfig{MaxSectionTokens: 2, MinSectionTokens: 0}, &chunking.ConstTokenEstimator{Tokens: 1}, ) sections, err := chunker.ChunkTranscript(transcript) if err != nil { t.Fatalf("chunk transcript failed: %v", err) } if len(sections) != 2 { t.Fatalf("expected fixture chunking to produce 2 sections, got %d", len(sections)) } return sections } func cloneTranscriptForComparison(in *schema.Transcript) *schema.Transcript { if in == nil { return nil } out := &schema.Transcript{Segments: make([]schema.Segment, len(in.Segments))} for i, seg := range in.Segments { cloned := seg if seg.Categories != nil { cloned.Categories = append([]string(nil), seg.Categories...) } out.Segments[i] = cloned } return out }