package proposal_generation import ( "context" "encoding/json" "errors" "testing" "gitea.maximumdirect.net/eric/audita/v3/internal/core/config" "gitea.maximumdirect.net/eric/audita/v3/internal/core/schema" "gitea.maximumdirect.net/eric/audita/v3/internal/framework/contracts" "gitea.maximumdirect.net/eric/audita/v3/internal/framework/outcomes" "gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals" "gitea.maximumdirect.net/eric/audita/v3/internal/framework/structuredoutput" "gitea.maximumdirect.net/eric/audita/v3/internal/prompts" ) func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing.T) { client := &fakeStructuredClient{ responses: []StructuredCorrectionSet{ {Corrections: []StructuredCorrectionProposal{{SegmentReference: 1, OriginalText: "teh", CorrectedText: "the", Confidence: 0.9}}}, }, } section := contracts.SectionMetadata{Index: 7} cfg := config.Config{} cfg.Context.Description = "Hearing transcript with role titles." transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Speaker: "A", Start: 0, End: 1, Text: "teh"}}} glossary := &schema.Glossary{Entries: []schema.GlossaryEntry{{Name: "X"}}} var gotSectionIndex int var gotDescription string var gotTranscript *schema.Transcript var gotGlossary *schema.Glossary out, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{ ProposalRequest: contracts.ProposalRequest{ ExecutionContext: contracts.ExecutionContext{ Config: &cfg, WorkingTranscript: transcript, Glossary: glossary, Section: §ion, }, RunSpec: contracts.ModuleRunSpec{ ModuleKey: "grammar", InstanceName: "grammar", ReplacementPolicy: proposals.ReplacementPolicyRequireUnique, }, LLMClient: client, }, PromptID: prompts.PromptIDModuleGrammarProposal, BuildVariables: func(inTranscript *schema.Transcript, inGlossary *schema.Glossary, sectionIndex int, transcriptDescription string) (map[string]string, error) { gotSectionIndex = sectionIndex gotDescription = transcriptDescription gotTranscript = inTranscript gotGlossary = inGlossary return map[string]string{"SectionJSON": "[]"}, nil }, }) if err != nil { t.Fatalf("ExecuteModuleProposal error: %v", err) } if gotSectionIndex != 7 { t.Fatalf("section index: got=%d want=%d", gotSectionIndex, 7) } if gotDescription != cfg.Context.Description { t.Fatalf("transcript description: got=%q want=%q", gotDescription, cfg.Context.Description) } if gotTranscript != transcript { t.Fatalf("expected shared transcript pointer") } if gotGlossary != glossary { t.Fatalf("expected shared glossary pointer") } if len(client.calls) != 1 || client.calls[0].PromptID != prompts.PromptIDModuleGrammarProposal || client.calls[0].Context.SectionIndex == nil || *client.calls[0].Context.SectionIndex != 7 { t.Fatalf("unexpected stage name calls: %+v", client.calls) } if len(out.Proposals) != 1 || out.Proposals[0].CorrectedText != "the" { t.Fatalf("unexpected proposals: %+v", out) } } func TestExecuteModuleProposalValidatesInputs(t *testing.T) { if _, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{}); err == nil { t.Fatalf("expected missing message builder error") } _, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{ BuildVariables: func(transcript *schema.Transcript, glossary *schema.Glossary, sectionIndex int, transcriptDescription string) (map[string]string, error) { return nil, nil }, }) if err == nil { t.Fatalf("expected empty prompt ID error") } _, err = ExecuteModuleProposal(context.Background(), ModuleProposalRequest{ PromptID: "missing.prompt.id", BuildVariables: func(transcript *schema.Transcript, glossary *schema.Glossary, sectionIndex int, transcriptDescription string) (map[string]string, error) { return map[string]string{"SectionJSON": "[]"}, nil }, }) if err == nil { t.Fatalf("expected unknown prompt ID error") } } func TestExecuteModuleProposalPropagatesBuilderError(t *testing.T) { wantErr := errors.New("builder failed") _, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{ PromptID: prompts.PromptIDModuleGlossaryProposal, BuildVariables: func(transcript *schema.Transcript, glossary *schema.Glossary, sectionIndex int, transcriptDescription string) (map[string]string, error) { return nil, wantErr }, }) if !errors.Is(err, wantErr) { t.Fatalf("expected builder error, got %v", err) } } func TestExecuteModuleProposalPreservesFatalOperation(t *testing.T) { cause := errors.New("missing embedded asset") client := &fakeStructuredClient{err: &structuredoutput.Failure{Class: structuredoutput.FailureFatal, Reason: outcomes.ReasonSetupFailure, Message: "model setup failed", Cause: cause}} section := contracts.SectionMetadata{Index: 4} result, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{ ProposalRequest: contracts.ProposalRequest{ ExecutionContext: contracts.ExecutionContext{Section: §ion}, RunSpec: contracts.ModuleRunSpec{ModuleKey: "grammar", InstanceName: "grammar_2", ReplacementPolicy: proposals.ReplacementPolicyRequireUnique}, LLMClient: client, }, PromptID: prompts.PromptIDModuleGrammarProposal, BuildVariables: func(*schema.Transcript, *schema.Glossary, int, string) (map[string]string, error) { return map[string]string{"SectionJSON": "[]"}, nil }, }) if !errors.Is(err, cause) || len(result.Operations) != 1 || result.Operations[0].Reference.ModuleInstance != "grammar_2" || result.Operations[0].Reference.SectionIndex != 4 { t.Fatalf("fatal operation lost: %+v, %v", result, err) } } func TestModuleProposalMapsLocalReferencesWithoutGuessing(t *testing.T) { transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 71, Text: "teh cat"}, {ID: 903, Text: "uh hello"}}} client := &fakeStructuredClient{responses: []StructuredCorrectionSet{{Corrections: []StructuredCorrectionProposal{ {SegmentReference: 2, OriginalText: "uh ", CorrectedText: "", Confidence: .9}, {SegmentReference: 1, OriginalText: "teh", CorrectedText: "the", Confidence: .9}, {SegmentReference: 2, OriginalText: "hello", CorrectedText: "Hello", Confidence: .9}, {SegmentReference: 71, OriginalText: "teh", CorrectedText: "the", Confidence: .9}, }}}} result, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{ ProposalRequest: contracts.ProposalRequest{ ExecutionContext: contracts.ExecutionContext{WorkingTranscript: transcript}, RunSpec: contracts.ModuleRunSpec{ModuleKey: "grammar", InstanceName: "grammar", ReplacementPolicy: proposals.ReplacementPolicyRequireUnique}, LLMClient: client, }, PromptID: prompts.PromptIDModuleGrammarProposal, BuildVariables: BuildProposalVariables, }) if err != nil { t.Fatal(err) } var payload struct { Segments []struct { ID int `json:"id"` Text string `json:"text"` } `json:"segments"` } if err := json.Unmarshal([]byte(client.calls[0].Variables["SectionJSON"]), &payload); err != nil { t.Fatal(err) } if len(payload.Segments) != 2 || payload.Segments[0].ID != 1 || payload.Segments[1].ID != 2 || payload.Segments[1].Text != "uh hello" { t.Fatalf("request references: %+v", payload) } if len(result.Proposals) != 4 { t.Fatalf("proposals: %+v", result) } for i, id := range []int{903, 71, 903, 0} { if result.Proposals[i].TargetSegmentID != id { t.Fatalf("mapped proposal %d: %+v", i, result.Proposals[i]) } } if result.Proposals[0].CorrectedText != "" || transcript.Segments[0].ID != 71 || transcript.Segments[1].ID != 903 { t.Fatal("mapping changed deletion or source identities") } }