Complete Phase 13 glossary module
This commit is contained in:
@@ -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 != "phase12-grammar-module" {
|
||||
t.Errorf("expected phase 'phase12-grammar-module', got %q", report.Phase)
|
||||
if report.Phase != "phase13-glossary-module" {
|
||||
t.Errorf("expected phase 'phase13-glossary-module', got %q", report.Phase)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,12 +664,13 @@ func (v fakeValidator) Validate(ctx context.Context, req contracts.ValidationReq
|
||||
type fakeStructuredLLMClient struct {
|
||||
validationResponses []validators.LLMValidationResponse
|
||||
proposalResponses []proposal_generation.StructuredCorrectionSet
|
||||
calls []string
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeStructuredLLMClient) CompleteStructured(ctx context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
|
||||
_ = ctx
|
||||
_ = req
|
||||
f.calls = append(f.calls, req.StageName)
|
||||
if f.err != nil {
|
||||
return contracts.StructuredCompletionResponse{}, f.err
|
||||
}
|
||||
@@ -923,7 +924,7 @@ func TestRunProcessProductionRegistryUnimplementedModuleFailsCleanly(t *testing.
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "glossary",
|
||||
"--modules", "homophones",
|
||||
"--work-dir", workDir,
|
||||
"--work-dir-retention", "always",
|
||||
"--report-json", reportPath,
|
||||
@@ -954,7 +955,7 @@ func TestRunProcessProductionRegistryUnimplementedModuleFailsCleanly(t *testing.
|
||||
}
|
||||
|
||||
func TestRunProcessExplicitUnimplementedModulesFailClearly(t *testing.T) {
|
||||
for _, moduleKey := range []string{"glossary", "homophones", "spoken_word"} {
|
||||
for _, moduleKey := range []string{"homophones", "spoken_word"} {
|
||||
t.Run(moduleKey, func(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
transcriptPath := writeFile(t, "transcript.json", `[
|
||||
@@ -1196,6 +1197,281 @@ func TestRunProcessDefaultBehaviorRemainsDeterministicWithoutExplicitModules(t *
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessExplicitGlossaryAppliesCorrectionAndReportsDiagnostics(t *testing.T) {
|
||||
secret := "phase13-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", "glossary",
|
||||
"--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 glossary correction applied, got %q", parsed.Segments[0].Text)
|
||||
}
|
||||
|
||||
report := readProcessReport(t, reportPath)
|
||||
if len(report.ModuleResults) != 1 || report.ModuleResults[0].ModuleKey != "glossary" {
|
||||
t.Fatalf("expected one glossary module result, got %+v", report.ModuleResults)
|
||||
}
|
||||
if len(report.ModuleResults[0].AppliedChanges) != 1 {
|
||||
t.Fatalf("expected one applied glossary 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 {
|
||||
t.Fatalf("expected module results in run-dir report")
|
||||
}
|
||||
|
||||
diagFiles, globErr := filepath.Glob(filepath.Join(runDir, "glossary", "*response-payload.json"))
|
||||
if globErr != nil {
|
||||
t.Fatalf("glob diagnostics: %v", globErr)
|
||||
}
|
||||
if len(diagFiles) == 0 {
|
||||
t.Fatalf("expected glossary diagnostics payload files in %s", filepath.Join(runDir, "glossary"))
|
||||
}
|
||||
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 TestRunProcessExplicitGlossaryRejectedAndApplicationSkipAreDistinct(t *testing.T) {
|
||||
processProposalLLMClient = &fakeStructuredLLMClient{
|
||||
proposalResponses: []proposal_generation.StructuredCorrectionSet{
|
||||
{
|
||||
Corrections: []proposal_generation.StructuredCorrectionProposal{
|
||||
{TargetSegmentID: 1, OriginalText: "there were gestures", CorrectedText: "There were gestures", Confidence: 0.99},
|
||||
{TargetSegmentID: 1, OriginalText: "there were gestures", CorrectedText: "there were jesters", Confidence: 0.99},
|
||||
{TargetSegmentID: 1, OriginalText: "hall", CorrectedText: "temple", 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", "glossary",
|
||||
"--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 TestRunProcessExplicitGlossaryRepeatedStagesUseDeterministicInstanceNamesAndSeePriorChanges(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: "JESTERS", 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,glossary",
|
||||
"--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 JESTERS in the hall." {
|
||||
t.Fatalf("expected second stage to see first stage changes, 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].ModuleInstance != "glossary_1" || report.ModuleResults[1].ModuleInstance != "glossary_2" {
|
||||
t.Fatalf("expected deterministic glossary instance names, got %+v", report.ModuleResults)
|
||||
}
|
||||
if len(report.ModuleResults[0].AppliedChanges) != 1 || len(report.ModuleResults[1].AppliedChanges) != 1 {
|
||||
t.Fatalf("expected one applied change per stage, got %+v", report.ModuleResults)
|
||||
}
|
||||
foundStage1 := false
|
||||
foundStage2 := false
|
||||
for _, call := range proposalClient.calls {
|
||||
if strings.HasPrefix(call, "glossary_1:proposal") {
|
||||
foundStage1 = true
|
||||
}
|
||||
if strings.HasPrefix(call, "glossary_2:proposal") {
|
||||
foundStage2 = true
|
||||
}
|
||||
}
|
||||
if !foundStage1 || !foundStage2 {
|
||||
t.Fatalf("expected proposal calls for glossary_1 and glossary_2, got %v", proposalClient.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessExplicitGlossaryMalformedLLMOutputFailsWithErrorLog(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", "glossary",
|
||||
"--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 glossary 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 TestRunProcessChunkingSummaryArtifactWritten(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
Reference in New Issue
Block a user