package proposal_generation import ( "context" "encoding/json" "errors" "strings" "testing" "gitea.maximumdirect.net/eric/audita/v3/internal/core/config" domain "gitea.maximumdirect.net/eric/audita/v3/internal/core/glossary" "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.Stage{} 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 := []domain.Entry{{ID: "x", Name: "X"}} out, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{ ProposalRequest: contracts.ProposalRequest{ ExecutionContext: contracts.ExecutionContext{ Settings: cfg, WorkingTranscript: transcript, Glossary: glossary, ProtectedTerms: []domain.Term{{Spelling: "Separate protection"}, {Spelling: "Exempt boundary", Exempt: true}}, Section: §ion, }, RunSpec: contracts.ModuleRunSpec{ ModuleKey: "grammar", InstanceName: "grammar", ReplacementPolicy: proposals.ReplacementPolicyRequireUnique, }, LLMClient: client, }, PromptID: prompts.PromptIDModuleGrammarProposal, }) if err != nil { t.Fatalf("ExecuteModuleProposal error: %v", err) } 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) } variables := client.calls[0].Variables if !strings.Contains(variables["SectionJSON"], `"section_index": 7`) || !strings.Contains(variables["SectionJSON"], "teh") || !strings.Contains(variables["TranscriptDescriptionBlock"], cfg.Context.Description) || !strings.Contains(variables["GlossaryJSON"], `"name": "X"`) { t.Fatalf("missing section, glossary, or description: %+v", variables) } if got := client.calls[0].Variables["ProtectedTermsJSON"]; got != `["Separate protection"]` { t.Fatalf("protected spellings = %s", got) } if len(out.Proposals) != 1 || out.Proposals[0].CorrectedText != "the" { t.Fatalf("unexpected proposals: %+v", out) } } func TestExecuteModuleProposalValidatesInputs(t *testing.T) { for _, promptID := range []string{"", "missing.prompt.id"} { if _, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{PromptID: promptID}); err == nil { t.Fatalf("invalid prompt ID accepted: %q", promptID) } } } 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, }) 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, }) 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") } }