Complete Phase 7 runner orchestration
This commit is contained in:
@@ -2,7 +2,9 @@ package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -11,6 +13,8 @@ import (
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/normalization"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/reporting"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
||||
)
|
||||
|
||||
func TestRunRootHelp(t *testing.T) {
|
||||
@@ -609,8 +613,188 @@ func TestRunProcessReportJSONIncludesChunkingSummary(t *testing.T) {
|
||||
if report.Chunking.MaxSectionTokens == 0 {
|
||||
t.Errorf("expected max_section_tokens in report")
|
||||
}
|
||||
if report.Phase != "phase3-chunking" {
|
||||
t.Errorf("expected phase 'phase3-chunking', got %q", report.Phase)
|
||||
if report.Phase != "phase7-runner" {
|
||||
t.Errorf("expected phase 'phase7-runner', got %q", report.Phase)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeModuleFactory struct {
|
||||
modules map[string]contracts.TranscriptModule
|
||||
}
|
||||
|
||||
func (f fakeModuleFactory) ModuleForSpec(spec contracts.ModuleRunSpec) (contracts.TranscriptModule, error) {
|
||||
m, ok := f.modules[spec.InstanceName]
|
||||
if !ok {
|
||||
return nil, errors.New("module not registered")
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type fakeModule struct {
|
||||
key string
|
||||
policy proposals.ReplacementPolicy
|
||||
proposeF func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error)
|
||||
}
|
||||
|
||||
func (m fakeModule) Key() string { return m.key }
|
||||
func (m fakeModule) ReplacementPolicy() proposals.ReplacementPolicy { return m.policy }
|
||||
func (m fakeModule) Validators() []contracts.Validator { return nil }
|
||||
func (m fakeModule) Propose(ctx context.Context, req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
if m.proposeF == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return m.proposeF(req)
|
||||
}
|
||||
|
||||
func TestRunProcessInjectedFactoryExecutesRunnerAndReportsModules(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m1": fakeModule{key: "m1", policy: proposals.ReplacementPolicyRequireUnique, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
return []proposals.CorrectionProposal{
|
||||
{TargetSegmentID: 1, OriginalText: "Hello", CorrectedText: "Hi", Confidence: 1},
|
||||
}, nil
|
||||
}},
|
||||
"m2": fakeModule{key: "m2", policy: proposals.ReplacementPolicyRequireUnique, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
if req.WorkingTranscript.Segments[0].Text != "Hi world" {
|
||||
t.Fatalf("expected module 2 to see module 1 changes, got %q", req.WorkingTranscript.Segments[0].Text)
|
||||
}
|
||||
return []proposals.CorrectionProposal{
|
||||
{TargetSegmentID: 1, OriginalText: "Hi", CorrectedText: "Hey", Confidence: 1},
|
||||
{TargetSegmentID: 1, OriginalText: "missing", CorrectedText: "noop", Confidence: 1},
|
||||
}, nil
|
||||
}},
|
||||
}}
|
||||
t.Cleanup(func() { processModuleFactory = 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":"Hello world"}
|
||||
]`)
|
||||
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "m1,m2",
|
||||
"--output", outputPath,
|
||||
"--report-json", reportPath,
|
||||
"--work-dir", workDir,
|
||||
"--work-dir-retention", "always",
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got exit %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
if stdout.Len() != 0 {
|
||||
t.Fatalf("expected empty stdout with --output, got %q", stdout.String())
|
||||
}
|
||||
|
||||
out := readFile(t, outputPath)
|
||||
parsed, err := schema.ParseTranscriptJSON(out)
|
||||
if err != nil {
|
||||
t.Fatalf("parse output transcript: %v", err)
|
||||
}
|
||||
if parsed.Segments[0].Text != "Hey world" {
|
||||
t.Fatalf("expected runner-applied output text, got %q", parsed.Segments[0].Text)
|
||||
}
|
||||
|
||||
report := readProcessReport(t, reportPath)
|
||||
if report.ModulesSummary == nil || report.ModulesSummary.ModuleCount != 2 {
|
||||
t.Fatalf("expected module summary for 2 modules, got %+v", report.ModulesSummary)
|
||||
}
|
||||
if report.ModulesSummary.TotalAppliedChanges != 2 || report.ModulesSummary.TotalSkippedChanges != 1 {
|
||||
t.Fatalf("unexpected module totals: %+v", report.ModulesSummary)
|
||||
}
|
||||
if len(report.ModuleResults) != 2 {
|
||||
t.Fatalf("expected 2 module results, got %d", len(report.ModuleResults))
|
||||
}
|
||||
if len(report.ModuleResults[1].SkippedChanges) != 1 {
|
||||
t.Fatalf("expected skipped change for module 2, got %+v", report.ModuleResults[1].SkippedChanges)
|
||||
}
|
||||
|
||||
runDirReport := readProcessReport(t, filepath.Join(onlyRunDir(t, workDir), "report.json"))
|
||||
if len(runDirReport.ModuleResults) != 2 {
|
||||
t.Fatalf("expected module results in run-dir report")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessInjectedFactorySkippedKeepsAutoRetention(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m1": fakeModule{key: "m1", policy: proposals.ReplacementPolicyRequireUnique, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
return []proposals.CorrectionProposal{
|
||||
{TargetSegmentID: 1, OriginalText: "word", CorrectedText: "term", Confidence: 1},
|
||||
}, nil
|
||||
}},
|
||||
}}
|
||||
t.Cleanup(func() { processModuleFactory = nil })
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
workDir := t.TempDir()
|
||||
transcriptPath := writeFile(t, "transcript.json", `[
|
||||
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"word word"}
|
||||
]`)
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "m1",
|
||||
"--work-dir", workDir,
|
||||
"--work-dir-retention", "auto",
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
if _, err := os.Stat(onlyRunDir(t, workDir)); err != nil {
|
||||
t.Fatalf("expected run dir retained for skipped corrections under auto: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessInjectedFactoryFailureWritesFailedReport(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m1": fakeModule{key: "m1", policy: proposals.ReplacementPolicyRequireUnique, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
return nil, errors.New("test failure")
|
||||
}},
|
||||
}}
|
||||
t.Cleanup(func() { processModuleFactory = 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", "m1",
|
||||
"--work-dir", workDir,
|
||||
"--work-dir-retention", "always",
|
||||
"--report-json", reportPath,
|
||||
}, &stdout, &stderr)
|
||||
if exitCode == 0 {
|
||||
t.Fatal("expected failure exit code")
|
||||
}
|
||||
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 failure on stderr, got %q", stderr.String())
|
||||
}
|
||||
|
||||
report := readProcessReport(t, reportPath)
|
||||
if report.Status != "failed" {
|
||||
t.Fatalf("expected failed report status, got %q", report.Status)
|
||||
}
|
||||
if report.ModulesSummary == nil || report.ModulesSummary.FailedModuleInstance == "" {
|
||||
t.Fatalf("expected failed module summary, got %+v", report.ModulesSummary)
|
||||
}
|
||||
if len(report.ModuleResults) != 1 || report.ModuleResults[0].Status != "failed" {
|
||||
t.Fatalf("expected failed module result, got %+v", report.ModuleResults)
|
||||
}
|
||||
|
||||
runPath := onlyRunDir(t, workDir)
|
||||
if _, err := os.Stat(filepath.Join(runPath, "error.log")); err != nil {
|
||||
t.Fatalf("expected error.log for failed runner module: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user