Complete Phase 14 homophones module
This commit is contained in:
@@ -460,7 +460,7 @@ func extractErrorPhase(err error) (phase string, message string) {
|
||||
|
||||
func buildProcessReport(status string, inv processInvocation, runDir *diagnostics.RunDirectory, startedAt, completedAt time.Time, errorMessage string, errorPhase string, normalizationSummary *normalization.NormalizationSummary, chunkingSummary *chunking.Summary, runOutput *runner.RunOutput) reporting.ProcessReport {
|
||||
report := reporting.ProcessReport{
|
||||
Phase: "phase13-glossary-module",
|
||||
Phase: "phase14-homophones-module",
|
||||
Status: status,
|
||||
Operation: "process",
|
||||
TranscriptPath: inv.TranscriptPath,
|
||||
|
||||
@@ -616,8 +616,8 @@ func TestRunProcessReportJSONIncludesChunkingSummary(t *testing.T) {
|
||||
if report.Chunking.MaxSectionTokens == 0 {
|
||||
t.Errorf("expected max_section_tokens in report")
|
||||
}
|
||||
if report.Phase != "phase13-glossary-module" {
|
||||
t.Errorf("expected phase 'phase13-glossary-module', got %q", report.Phase)
|
||||
if report.Phase != "phase14-homophones-module" {
|
||||
t.Errorf("expected phase 'phase14-homophones-module', got %q", report.Phase)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -924,7 +924,7 @@ func TestRunProcessProductionRegistryUnimplementedModuleFailsCleanly(t *testing.
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "homophones",
|
||||
"--modules", "spoken_word",
|
||||
"--work-dir", workDir,
|
||||
"--work-dir-retention", "always",
|
||||
"--report-json", reportPath,
|
||||
@@ -955,7 +955,7 @@ func TestRunProcessProductionRegistryUnimplementedModuleFailsCleanly(t *testing.
|
||||
}
|
||||
|
||||
func TestRunProcessExplicitUnimplementedModulesFailClearly(t *testing.T) {
|
||||
for _, moduleKey := range []string{"homophones", "spoken_word"} {
|
||||
for _, moduleKey := range []string{"spoken_word"} {
|
||||
t.Run(moduleKey, func(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
transcriptPath := writeFile(t, "transcript.json", `[
|
||||
@@ -1472,6 +1472,324 @@ func TestRunProcessExplicitGlossaryMalformedLLMOutputFailsWithErrorLog(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessExplicitHomophonesAppliesCorrectionAndReportsDiagnostics(t *testing.T) {
|
||||
secret := "phase14-secret"
|
||||
proposalClient := &fakeStructuredLLMClient{
|
||||
proposalResponses: []proposal_generation.StructuredCorrectionSet{
|
||||
{
|
||||
Corrections: []proposal_generation.StructuredCorrectionProposal{
|
||||
{TargetSegmentID: 1, OriginalText: "gestures", CorrectedText: "Jesters", Confidence: 0.95},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
validationClient := &fakeStructuredLLMClient{
|
||||
validationResponses: []validators.LLMValidationResponse{
|
||||
{Validations: []validators.LLMValidationDecision{{CorrectionIndex: 0, Approved: true, Confidence: 0.99, Reason: "spoken plausible"}}},
|
||||
{Validations: []validators.LLMValidationDecision{{CorrectionIndex: 0, Approved: true, Confidence: 0.99, Reason: secret}}},
|
||||
},
|
||||
}
|
||||
processProposalLLMClient = proposalClient
|
||||
processValidationLLMClient = validationClient
|
||||
t.Setenv("AUDITA_LLM_API_KEY", secret)
|
||||
t.Setenv("AUDITA_VALIDATION_LLM_API_KEY", secret)
|
||||
t.Cleanup(func() {
|
||||
processProposalLLMClient = nil
|
||||
processValidationLLMClient = nil
|
||||
})
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
workDir := t.TempDir()
|
||||
reportPath := filepath.Join(t.TempDir(), "report.json")
|
||||
outputPath := filepath.Join(t.TempDir(), "out.json")
|
||||
transcriptPath := writeFile(t, "transcript.json", `[
|
||||
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"There were gestures in the hall."}
|
||||
]`)
|
||||
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "homophones",
|
||||
"--output", outputPath,
|
||||
"--report-json", reportPath,
|
||||
"--work-dir", workDir,
|
||||
"--work-dir-retention", "always",
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
if stdout.Len() != 0 {
|
||||
t.Fatalf("expected empty stdout with --output, got %q", stdout.String())
|
||||
}
|
||||
|
||||
parsed, err := schema.ParseTranscriptJSON(readFile(t, outputPath))
|
||||
if err != nil {
|
||||
t.Fatalf("parse output: %v", err)
|
||||
}
|
||||
if parsed.Segments[0].Text != "There were Jesters in the hall." {
|
||||
t.Fatalf("expected homophones correction applied, got %q", parsed.Segments[0].Text)
|
||||
}
|
||||
|
||||
report := readProcessReport(t, reportPath)
|
||||
if len(report.ModuleResults) != 1 || report.ModuleResults[0].ModuleKey != "homophones" {
|
||||
t.Fatalf("expected one homophones module result, got %+v", report.ModuleResults)
|
||||
}
|
||||
if len(report.ModuleResults[0].AppliedChanges) != 1 {
|
||||
t.Fatalf("expected one applied homophones change, got %+v", report.ModuleResults[0].AppliedChanges)
|
||||
}
|
||||
if len(report.ModuleResults[0].ValidatorDecisions) == 0 {
|
||||
t.Fatalf("expected validator decisions in report")
|
||||
}
|
||||
|
||||
runDir := onlyRunDir(t, workDir)
|
||||
runReport := readProcessReport(t, filepath.Join(runDir, "report.json"))
|
||||
if len(runReport.ModuleResults) != 1 || runReport.ModuleResults[0].ModuleKey != "homophones" {
|
||||
t.Fatalf("expected homophones module results in run-dir report, got %+v", runReport.ModuleResults)
|
||||
}
|
||||
|
||||
diagFiles, globErr := filepath.Glob(filepath.Join(runDir, "homophones", "*response-payload.json"))
|
||||
if globErr != nil {
|
||||
t.Fatalf("glob diagnostics: %v", globErr)
|
||||
}
|
||||
if len(diagFiles) == 0 {
|
||||
t.Fatalf("expected homophones diagnostics payload files in %s", filepath.Join(runDir, "homophones"))
|
||||
}
|
||||
for _, f := range diagFiles {
|
||||
raw := string(readFile(t, f))
|
||||
if strings.Contains(raw, secret) {
|
||||
t.Fatalf("secret leaked in diagnostics %q: %s", f, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessExplicitHomophonesRejectedAndApplicationSkipAreDistinct(t *testing.T) {
|
||||
processProposalLLMClient = &fakeStructuredLLMClient{
|
||||
proposalResponses: []proposal_generation.StructuredCorrectionSet{
|
||||
{
|
||||
Corrections: []proposal_generation.StructuredCorrectionProposal{
|
||||
{TargetSegmentID: 1, OriginalText: "hall", CorrectedText: "temple", Confidence: 0.99},
|
||||
{TargetSegmentID: 1, OriginalText: "there were gestures in the hall", CorrectedText: "there were gestures in the temple", Confidence: 0.99},
|
||||
{TargetSegmentID: 1, OriginalText: "and there were gestures.", CorrectedText: "and there were jesters.", Confidence: 0.99},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
processValidationLLMClient = &fakeStructuredLLMClient{
|
||||
validationResponses: []validators.LLMValidationResponse{
|
||||
{
|
||||
Validations: []validators.LLMValidationDecision{
|
||||
{CorrectionIndex: 0, Approved: true, Confidence: 0.9, Reason: "ok"},
|
||||
{CorrectionIndex: 1, Approved: true, Confidence: 0.9, Reason: "ok"},
|
||||
{CorrectionIndex: 2, Approved: false, Confidence: 0.9, Reason: "reject"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Validations: []validators.LLMValidationDecision{
|
||||
{CorrectionIndex: 0, Approved: true, Confidence: 0.9, Reason: "ok"},
|
||||
{CorrectionIndex: 1, Approved: true, Confidence: 0.9, Reason: "ok"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
processProposalLLMClient = nil
|
||||
processValidationLLMClient = nil
|
||||
})
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
reportPath := filepath.Join(t.TempDir(), "report.json")
|
||||
outputPath := filepath.Join(t.TempDir(), "out.json")
|
||||
transcriptPath := writeFile(t, "transcript.json", `[
|
||||
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"there were gestures in the hall and there were gestures."}
|
||||
]`)
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "homophones",
|
||||
"--output", outputPath,
|
||||
"--report-json", reportPath,
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
|
||||
report := readProcessReport(t, reportPath)
|
||||
if len(report.ModuleResults) != 1 {
|
||||
t.Fatalf("expected one module result")
|
||||
}
|
||||
module := report.ModuleResults[0]
|
||||
if len(module.ValidatorRejected) != 1 {
|
||||
t.Fatalf("expected one validator rejection, got %+v", module.ValidatorRejected)
|
||||
}
|
||||
if len(module.SkippedChanges) != 1 {
|
||||
t.Fatalf("expected one application skip, got %+v", module.SkippedChanges)
|
||||
}
|
||||
if module.ValidatorRejected[0].ReasonCode == string(module.SkippedChanges[0].SkipReason) {
|
||||
t.Fatalf("validator rejection and application skip should remain distinct")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessExplicitHomophonesProtectedGlossaryTermRejected(t *testing.T) {
|
||||
processProposalLLMClient = &fakeStructuredLLMClient{
|
||||
proposalResponses: []proposal_generation.StructuredCorrectionSet{
|
||||
{
|
||||
Corrections: []proposal_generation.StructuredCorrectionProposal{
|
||||
{TargetSegmentID: 1, OriginalText: "Audita", CorrectedText: "audita", Confidence: 0.99},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
processProposalLLMClient = nil
|
||||
processValidationLLMClient = nil
|
||||
})
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
reportPath := filepath.Join(t.TempDir(), "report.json")
|
||||
outputPath := filepath.Join(t.TempDir(), "out.json")
|
||||
transcriptPath := writeFile(t, "transcript.json", `[
|
||||
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"Audita held the line."}
|
||||
]`)
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "homophones",
|
||||
"--output", outputPath,
|
||||
"--report-json", reportPath,
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
|
||||
parsed, err := schema.ParseTranscriptJSON(readFile(t, outputPath))
|
||||
if err != nil {
|
||||
t.Fatalf("parse output: %v", err)
|
||||
}
|
||||
if parsed.Segments[0].Text != "Audita held the line." {
|
||||
t.Fatalf("expected protected glossary term to remain unchanged, got %q", parsed.Segments[0].Text)
|
||||
}
|
||||
|
||||
report := readProcessReport(t, reportPath)
|
||||
if len(report.ModuleResults) != 1 {
|
||||
t.Fatalf("expected one module result, got %+v", report.ModuleResults)
|
||||
}
|
||||
module := report.ModuleResults[0]
|
||||
if len(module.AppliedChanges) != 0 {
|
||||
t.Fatalf("expected no applied changes, got %+v", module.AppliedChanges)
|
||||
}
|
||||
if len(module.ValidatorRejected) != 1 {
|
||||
t.Fatalf("expected one validator rejection, got %+v", module.ValidatorRejected)
|
||||
}
|
||||
if module.ValidatorRejected[0].ReasonCode != validators.ReasonProtectedGlossaryTerm {
|
||||
t.Fatalf("expected protected glossary term rejection, got %+v", module.ValidatorRejected[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessExplicitHomophonesMalformedLLMOutputFailsWithErrorLog(t *testing.T) {
|
||||
processProposalLLMClient = &fakeStructuredLLMClient{err: errors.New("malformed structured output")}
|
||||
t.Cleanup(func() { processProposalLLMClient = nil })
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
workDir := t.TempDir()
|
||||
reportPath := filepath.Join(t.TempDir(), "report.json")
|
||||
transcriptPath := writeFile(t, "transcript.json", `[
|
||||
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"hello"}
|
||||
]`)
|
||||
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "homophones",
|
||||
"--work-dir", workDir,
|
||||
"--work-dir-retention", "always",
|
||||
"--report-json", reportPath,
|
||||
}, &stdout, &stderr)
|
||||
if exitCode == 0 {
|
||||
t.Fatal("expected failure")
|
||||
}
|
||||
if stdout.Len() != 0 {
|
||||
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "runner_execution") {
|
||||
t.Fatalf("expected runner_execution error, got %q", stderr.String())
|
||||
}
|
||||
runDir := onlyRunDir(t, workDir)
|
||||
if _, err := os.Stat(filepath.Join(runDir, "error.log")); err != nil {
|
||||
t.Fatalf("expected error.log on failed homophones run: %v", err)
|
||||
}
|
||||
report := readProcessReport(t, reportPath)
|
||||
if report.Status != "failed" || report.ErrorPhase != "runner_execution" {
|
||||
t.Fatalf("expected failed runner_execution report, got %+v", report)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessExplicitGlossaryThenHomophonesSeesWorkingTranscriptChanges(t *testing.T) {
|
||||
proposalClient := &fakeStructuredLLMClient{
|
||||
proposalResponses: []proposal_generation.StructuredCorrectionSet{
|
||||
{
|
||||
Corrections: []proposal_generation.StructuredCorrectionProposal{
|
||||
{TargetSegmentID: 1, OriginalText: "gestures", CorrectedText: "Jesters", Confidence: 0.99},
|
||||
},
|
||||
},
|
||||
{
|
||||
Corrections: []proposal_generation.StructuredCorrectionProposal{
|
||||
{TargetSegmentID: 1, OriginalText: "Jesters", CorrectedText: "Jester", Confidence: 0.99},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
validationClient := &fakeStructuredLLMClient{
|
||||
validationResponses: []validators.LLMValidationResponse{
|
||||
{Validations: []validators.LLMValidationDecision{{CorrectionIndex: 0, Approved: true, Confidence: 0.99, Reason: "ok"}}},
|
||||
{Validations: []validators.LLMValidationDecision{{CorrectionIndex: 0, Approved: true, Confidence: 0.99, Reason: "ok"}}},
|
||||
{Validations: []validators.LLMValidationDecision{{CorrectionIndex: 0, Approved: true, Confidence: 0.99, Reason: "ok"}}},
|
||||
{Validations: []validators.LLMValidationDecision{{CorrectionIndex: 0, Approved: true, Confidence: 0.99, Reason: "ok"}}},
|
||||
},
|
||||
}
|
||||
processProposalLLMClient = proposalClient
|
||||
processValidationLLMClient = validationClient
|
||||
t.Cleanup(func() {
|
||||
processProposalLLMClient = nil
|
||||
processValidationLLMClient = nil
|
||||
})
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
reportPath := filepath.Join(t.TempDir(), "report.json")
|
||||
outputPath := filepath.Join(t.TempDir(), "out.json")
|
||||
transcriptPath := writeFile(t, "transcript.json", `[
|
||||
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"There were gestures in the hall."}
|
||||
]`)
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "glossary,homophones",
|
||||
"--output", outputPath,
|
||||
"--report-json", reportPath,
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
|
||||
parsed, err := schema.ParseTranscriptJSON(readFile(t, outputPath))
|
||||
if err != nil {
|
||||
t.Fatalf("parse output: %v", err)
|
||||
}
|
||||
if parsed.Segments[0].Text != "There were Jester in the hall." {
|
||||
t.Fatalf("expected homophones stage to see glossary output, got %q", parsed.Segments[0].Text)
|
||||
}
|
||||
|
||||
report := readProcessReport(t, reportPath)
|
||||
if len(report.ModuleResults) != 2 {
|
||||
t.Fatalf("expected two module results, got %+v", report.ModuleResults)
|
||||
}
|
||||
if report.ModuleResults[0].ModuleKey != "glossary" || report.ModuleResults[1].ModuleKey != "homophones" {
|
||||
t.Fatalf("expected glossary then homophones results, got %+v", report.ModuleResults)
|
||||
}
|
||||
if len(report.ModuleResults[0].AppliedChanges) != 1 || len(report.ModuleResults[1].AppliedChanges) != 1 {
|
||||
t.Fatalf("expected one applied change per module, got %+v", report.ModuleResults)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessChunkingSummaryArtifactWritten(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
Reference in New Issue
Block a user