209 lines
6.3 KiB
Go
209 lines
6.3 KiB
Go
package glossary
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/config"
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
|
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
|
"gitea.maximumdirect.net/eric/audita/internal/framework/proposal_generation"
|
|
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
|
)
|
|
|
|
type fakeLLMClient struct {
|
|
responses []proposal_generation.StructuredCorrectionSet
|
|
err error
|
|
calls []contracts.StructuredCompletionRequest
|
|
}
|
|
|
|
func (f *fakeLLMClient) CompleteStructured(ctx context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
|
|
_ = ctx
|
|
f.calls = append(f.calls, req)
|
|
if f.err != nil {
|
|
return contracts.StructuredCompletionResponse{}, f.err
|
|
}
|
|
target, ok := out.(*proposal_generation.StructuredCorrectionSet)
|
|
if !ok {
|
|
return contracts.StructuredCompletionResponse{}, errors.New("unexpected output type")
|
|
}
|
|
if len(f.responses) == 0 {
|
|
return contracts.StructuredCompletionResponse{}, errors.New("unexpected call")
|
|
}
|
|
*target = f.responses[0]
|
|
f.responses = f.responses[1:]
|
|
return contracts.StructuredCompletionResponse{}, nil
|
|
}
|
|
|
|
type countingScheduler struct {
|
|
runs int
|
|
}
|
|
|
|
func (s *countingScheduler) Run(ctx context.Context, fn func(context.Context) error) error {
|
|
s.runs++
|
|
return fn(ctx)
|
|
}
|
|
|
|
func tinyTranscript() *schema.Transcript {
|
|
return &schema.Transcript{Segments: []schema.Segment{
|
|
{ID: 1, Speaker: "A", Start: 0, End: 1, Text: "There were gestures in the hall.", Categories: []string{"session"}},
|
|
}}
|
|
}
|
|
|
|
func tinyGlossary() *schema.Glossary {
|
|
return &schema.Glossary{Entries: []schema.GlossaryEntry{
|
|
{Name: "Jesters", Aliases: []string{"Jester"}, Category: "faction", Summary: "Guild members", Plural: "Jesters"},
|
|
}}
|
|
}
|
|
|
|
func TestBuildProposalMessagesContainsGlossaryContextAndConstraints(t *testing.T) {
|
|
msgs, err := BuildProposalMessages(tinyTranscript(), tinyGlossary(), 0, "")
|
|
if err != nil {
|
|
t.Fatalf("BuildProposalMessages error: %v", err)
|
|
}
|
|
if len(msgs) != 2 {
|
|
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
|
}
|
|
combined := msgs[0].Content + "\n" + msgs[1].Content
|
|
for _, want := range []string{
|
|
"Glossary:",
|
|
"Transcript section:",
|
|
`"section_index": 0`,
|
|
`"Aliases":`,
|
|
`"Jester"`,
|
|
`"Category": "faction"`,
|
|
`"Summary": "Guild members"`,
|
|
`"Plural": "Jesters"`,
|
|
"glossary-supported corrections",
|
|
"acoustically similar",
|
|
"Do not make generic grammar, spelling, capitalization, style, or filler-word edits",
|
|
} {
|
|
if !strings.Contains(combined, want) {
|
|
t.Fatalf("expected prompt to contain %q", want)
|
|
}
|
|
}
|
|
if strings.Contains(strings.ToLower(combined), "summarize the transcript") {
|
|
t.Fatalf("prompt should not invite summarization")
|
|
}
|
|
}
|
|
|
|
func TestBuildProposalMessagesIncludesTranscriptDescriptionGuidance(t *testing.T) {
|
|
msgs, err := BuildProposalMessages(tinyTranscript(), tinyGlossary(), 0, "Campaign scene in a crowded harbor.")
|
|
if err != nil {
|
|
t.Fatalf("BuildProposalMessages error: %v", err)
|
|
}
|
|
combined := msgs[0].Content + "\n" + msgs[1].Content
|
|
for _, want := range []string{
|
|
"Transcript description (background context only):",
|
|
"Campaign scene in a crowded harbor.",
|
|
"must not override the transcript content",
|
|
"Do not invent corrections, facts, names, events, motivations, or speaker intent based on this description.",
|
|
} {
|
|
if !strings.Contains(combined, want) {
|
|
t.Fatalf("expected prompt to contain %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGlossaryModuleReplacementPolicy(t *testing.T) {
|
|
m, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New error: %v", err)
|
|
}
|
|
if m.ReplacementPolicy() != proposals.ReplacementPolicyReplaceAll {
|
|
t.Fatalf("unexpected replacement policy: %q", m.ReplacementPolicy())
|
|
}
|
|
}
|
|
|
|
func TestGlossaryModuleValidatorChain(t *testing.T) {
|
|
m, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New error: %v", err)
|
|
}
|
|
got := make([]string, 0, len(m.Validators()))
|
|
for _, v := range m.Validators() {
|
|
got = append(got, v.Name())
|
|
}
|
|
want := []string{
|
|
"proposal_shape",
|
|
"no_effect",
|
|
"original_text_presence",
|
|
"confidence_threshold",
|
|
"protected_terms",
|
|
"non_empty_corrected_text",
|
|
"spoken_form_plausibility",
|
|
"meaning_reversal_review",
|
|
}
|
|
if strings.Join(got, ",") != strings.Join(want, ",") {
|
|
t.Fatalf("unexpected validator chain\n got: %v\nwant: %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestGlossaryModuleProposeMapsCorrectionsAndWritesDiagnostics(t *testing.T) {
|
|
secret := "glossary-secret"
|
|
client := &fakeLLMClient{
|
|
responses: []proposal_generation.StructuredCorrectionSet{
|
|
{
|
|
Corrections: []proposal_generation.StructuredCorrectionProposal{
|
|
{TargetSegmentID: 1, OriginalText: "gestures", CorrectedText: "Jesters", Confidence: 0.94},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
scheduler := &countingScheduler{}
|
|
cfg := config.Default()
|
|
cfg.PrimaryLLM.APIKey = secret
|
|
m, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New error: %v", err)
|
|
}
|
|
diagDir := t.TempDir()
|
|
out, err := m.Propose(context.Background(), contracts.ProposalRequest{
|
|
ExecutionContext: contracts.ExecutionContext{
|
|
Config: &cfg,
|
|
WorkingTranscript: tinyTranscript(),
|
|
Glossary: tinyGlossary(),
|
|
DiagnosticsDir: diagDir,
|
|
},
|
|
RunSpec: contracts.ModuleRunSpec{
|
|
ModuleKey: "glossary",
|
|
InstanceName: "glossary",
|
|
ReplacementPolicy: proposals.ReplacementPolicyReplaceAll,
|
|
},
|
|
LLMClient: client,
|
|
LLMScheduler: scheduler,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Propose error: %v", err)
|
|
}
|
|
if scheduler.runs != 1 {
|
|
t.Fatalf("expected scheduler run count 1, got %d", scheduler.runs)
|
|
}
|
|
if len(client.calls) != 1 || client.calls[0].StageName != "glossary:proposal" {
|
|
t.Fatalf("expected one glossary:proposal call, got %+v", client.calls)
|
|
}
|
|
if len(out.Proposals) != 1 || out.Proposals[0].CorrectedText != "Jesters" {
|
|
t.Fatalf("unexpected proposals: %+v", out)
|
|
}
|
|
diagFiles, globErr := filepath.Glob(filepath.Join(diagDir, "glossary", "*proposal*response-payload.json"))
|
|
if globErr != nil {
|
|
t.Fatalf("glob error: %v", globErr)
|
|
}
|
|
if len(diagFiles) == 0 {
|
|
t.Fatalf("expected diagnostics response payload under %s", filepath.Join(diagDir, "glossary"))
|
|
}
|
|
for _, f := range diagFiles {
|
|
raw, readErr := os.ReadFile(f)
|
|
if readErr != nil {
|
|
t.Fatalf("read diag %q: %v", f, readErr)
|
|
}
|
|
if strings.Contains(string(raw), secret) {
|
|
t.Fatalf("secret leaked in diagnostics: %s", string(raw))
|
|
}
|
|
}
|
|
}
|