731 lines
22 KiB
Go
731 lines
22 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/normalization"
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/reporting"
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
|
)
|
|
|
|
func TestRunRootHelp(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode := Run([]string{"--help"}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d", exitCode)
|
|
}
|
|
if !strings.Contains(stdout.String(), "audita <command> [options]") {
|
|
t.Fatalf("expected root usage in stdout, got %q", stdout.String())
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected empty stderr, got %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunProcessHelpListsExpectedFlags(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode := Run([]string{"process", "--help"}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d", exitCode)
|
|
}
|
|
|
|
for _, expectedFlag := range []string{
|
|
"--glossary",
|
|
"--output",
|
|
"--report-json",
|
|
"--modules",
|
|
"--llm-api-key",
|
|
"--validation-llm-api-key",
|
|
"--model",
|
|
"--validation-model",
|
|
"--base-url",
|
|
"--validation-base-url",
|
|
"--llm-timeout-seconds",
|
|
"--validation-llm-timeout-seconds",
|
|
"--validation-max-prompt-tokens",
|
|
"--target-sections",
|
|
"--max-retries",
|
|
"--validation-max-retries",
|
|
"--validation-llm-concurrency",
|
|
"--max-section-tokens",
|
|
"--min-section-tokens",
|
|
"--glossary-confidence-threshold",
|
|
"--grammar-confidence-threshold",
|
|
"--homophones-confidence-threshold",
|
|
"--spoken-word-confidence-threshold",
|
|
"--normalize-max-segment-gap",
|
|
"--normalize-ellipsis-gap",
|
|
"--normalize-max-segment-duration",
|
|
"--normalize-max-segment-tokens",
|
|
"--work-dir",
|
|
"--work-dir-retention",
|
|
} {
|
|
if !strings.Contains(stdout.String(), expectedFlag) {
|
|
t.Fatalf("expected process help to include %q", expectedFlag)
|
|
}
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected empty stderr, got %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunProcessMissingTranscriptPath(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode := Run([]string{"process", "--glossary", fixturePath("tiny_glossary.yaml")}, &stdout, &stderr)
|
|
if exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout, got %q", stdout.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "expected exactly 1 transcript JSON path argument") {
|
|
t.Fatalf("expected missing transcript error, got %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunProcessMissingGlossary(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode := Run([]string{"process", fixturePath("tiny_transcript.json")}, &stdout, &stderr)
|
|
if exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout, got %q", stdout.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "--glossary is required") {
|
|
t.Fatalf("expected missing glossary error, got %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunProcessAcceptsBothTranscriptTopLevelForms(t *testing.T) {
|
|
testCases := []string{
|
|
schemaFixturePath("transcript_bare_array.json"),
|
|
schemaFixturePath("transcript_object.json"),
|
|
}
|
|
|
|
for _, transcriptPath := range testCases {
|
|
t.Run(filepath.Base(transcriptPath), func(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode := Run([]string{"process", transcriptPath, "--glossary", fixturePath("tiny_glossary.yaml")}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected empty stderr on success, got %q", stderr.String())
|
|
}
|
|
|
|
parsed, err := schema.ParseTranscriptJSON(stdout.Bytes())
|
|
if err != nil {
|
|
t.Fatalf("expected normalized transcript JSON output, got %v", err)
|
|
}
|
|
if len(parsed.Segments) == 0 {
|
|
t.Fatalf("expected at least one output segment")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunProcessOutputToFileLeavesStdoutEmpty(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptPath := fixturePath("tiny_transcript.json")
|
|
glossaryPath := fixturePath("tiny_glossary.yaml")
|
|
outputPath := filepath.Join(t.TempDir(), "normalized.json")
|
|
|
|
exitCode := Run([]string{"process", transcriptPath, "--glossary", glossaryPath, "--output", outputPath}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout with --output, got %q", stdout.String())
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected empty stderr on success, got %q", stderr.String())
|
|
}
|
|
|
|
outputBytes := readFile(t, outputPath)
|
|
if !json.Valid(outputBytes) {
|
|
t.Fatalf("expected valid JSON output file, got %q", string(outputBytes))
|
|
}
|
|
}
|
|
|
|
func TestRunProcessInvalidTranscriptSchema(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode := Run([]string{"process", schemaFixturePath("transcript_empty_speaker.json"), "--glossary", fixturePath("tiny_glossary.yaml")}, &stdout, &stderr)
|
|
if exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code for invalid transcript schema")
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout, got %q", stdout.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "transcript_schema") {
|
|
t.Fatalf("expected transcript schema phase in stderr, got %q", stderr.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "speaker") {
|
|
t.Fatalf("expected speaker validation details, got %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunProcessInvalidGlossarySchema(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode := Run([]string{"process", fixturePath("tiny_transcript.json"), "--glossary", schemaFixturePath("glossary_missing_fields.yaml")}, &stdout, &stderr)
|
|
if exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code for invalid glossary schema")
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout, got %q", stdout.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "glossary_schema") {
|
|
t.Fatalf("expected glossary schema phase in stderr, got %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunProcessCLIOverridesEnvironment(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptPath := writeFile(t, "transcript.json", `[
|
|
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"Hello"},
|
|
{"id":2,"speaker":"Alice","start":1.5,"end":2.5,"text":"world"}
|
|
]`)
|
|
|
|
t.Setenv("AUDITA_NORMALIZE_MAX_SEGMENT_GAP", "0")
|
|
|
|
exitCode := Run([]string{
|
|
"process",
|
|
transcriptPath,
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--normalize-max-segment-gap",
|
|
"2.0",
|
|
}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
|
|
transcript, err := schema.ParseTranscriptJSON(stdout.Bytes())
|
|
if err != nil {
|
|
t.Fatalf("expected valid transcript JSON output: %v", err)
|
|
}
|
|
if len(transcript.Segments) != 1 {
|
|
t.Fatalf("expected merged output from CLI override, got %d segments", len(transcript.Segments))
|
|
}
|
|
}
|
|
|
|
func TestRunProcessReportJSONSuccessIncludesNormalizationSummary(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptPath := writeFile(t, "transcript.json", `[
|
|
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"Hello"},
|
|
{"id":2,"speaker":"Alice","start":1.5,"end":2.5,"text":"world"}
|
|
]`)
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
outputPath := filepath.Join(t.TempDir(), "normalized.json")
|
|
|
|
exitCode := Run([]string{
|
|
"process",
|
|
transcriptPath,
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--output",
|
|
outputPath,
|
|
"--report-json",
|
|
reportPath,
|
|
}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout with --output, got %q", stdout.String())
|
|
}
|
|
|
|
report := readProcessReport(t, reportPath)
|
|
if report.Status != "success" {
|
|
t.Fatalf("expected success status, got %q", report.Status)
|
|
}
|
|
if report.Operation != "process" {
|
|
t.Fatalf("expected operation process, got %q", report.Operation)
|
|
}
|
|
if report.InputSegmentCount == nil || *report.InputSegmentCount != 2 {
|
|
t.Fatalf("expected input segment count 2, got %v", report.InputSegmentCount)
|
|
}
|
|
if report.NormalizedSegmentCount == nil || *report.NormalizedSegmentCount != 1 {
|
|
t.Fatalf("expected normalized segment count 1, got %v", report.NormalizedSegmentCount)
|
|
}
|
|
if report.NormalizationMerges == nil || *report.NormalizationMerges != 1 {
|
|
t.Fatalf("expected 1 merge, got %v", report.NormalizationMerges)
|
|
}
|
|
}
|
|
|
|
func TestRunProcessReportJSONBestEffortOnFailure(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
exitCode := Run([]string{
|
|
"process",
|
|
fixturePath("malformed_transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--report-json",
|
|
reportPath,
|
|
}, &stdout, &stderr)
|
|
if exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
|
|
}
|
|
|
|
report := readProcessReport(t, reportPath)
|
|
if report.Status != "failed" {
|
|
t.Fatalf("expected failed status, got %q", report.Status)
|
|
}
|
|
if report.ErrorPhase != "transcript_schema" {
|
|
t.Fatalf("expected transcript_schema error phase, got %q", report.ErrorPhase)
|
|
}
|
|
if !strings.Contains(report.ErrorMessage, "not valid JSON") {
|
|
t.Fatalf("expected parse error message, got %q", report.ErrorMessage)
|
|
}
|
|
}
|
|
|
|
func TestRunProcessReportJSONDoesNotLeakAPIKeys(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
primaryKey := "top-secret-primary"
|
|
validationKey := "top-secret-validation"
|
|
t.Setenv("AUDITA_LLM_API_KEY", primaryKey)
|
|
t.Setenv("AUDITA_VALIDATION_LLM_API_KEY", validationKey)
|
|
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
exitCode := Run([]string{
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--report-json",
|
|
reportPath,
|
|
}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
|
|
reportBytes := readFile(t, reportPath)
|
|
if strings.Contains(string(reportBytes), primaryKey) || strings.Contains(string(reportBytes), validationKey) {
|
|
t.Fatalf("report leaked API key material: %s", string(reportBytes))
|
|
}
|
|
}
|
|
|
|
func TestRunProcessWritesNormalizationDiagnosticsArtifacts(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
workDir := t.TempDir()
|
|
transcriptPath := writeFile(t, "transcript.json", `[
|
|
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"Hello"},
|
|
{"id":2,"speaker":"Alice","start":1.5,"end":2.5,"text":"world"}
|
|
]`)
|
|
|
|
exitCode := Run([]string{
|
|
"process",
|
|
transcriptPath,
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--work-dir",
|
|
workDir,
|
|
"--work-dir-retention",
|
|
"always",
|
|
}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
|
|
runPath := onlyRunDir(t, workDir)
|
|
for _, artifact := range []string{
|
|
"source-transcript.json",
|
|
"source-transcript-parsed.json",
|
|
"normalized-transcript.json",
|
|
"normalization-summary.json",
|
|
"report.json",
|
|
} {
|
|
if _, err := os.Stat(filepath.Join(runPath, artifact)); err != nil {
|
|
t.Fatalf("expected artifact %s: %v", artifact, err)
|
|
}
|
|
}
|
|
|
|
summaryBytes := readFile(t, filepath.Join(runPath, "normalization-summary.json"))
|
|
var summary normalization.NormalizationSummary
|
|
if err := json.Unmarshal(summaryBytes, &summary); err != nil {
|
|
t.Fatalf("failed to parse normalization summary: %v", err)
|
|
}
|
|
if summary.OutputSegmentCount != 1 {
|
|
t.Fatalf("expected merged output in normalization summary, got %d", summary.OutputSegmentCount)
|
|
}
|
|
}
|
|
|
|
func TestRunProcessFailedRunRetainedWithNeverRetention(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
workDir := t.TempDir()
|
|
exitCode := Run([]string{
|
|
"process",
|
|
schemaFixturePath("transcript_empty_speaker.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--work-dir",
|
|
workDir,
|
|
"--work-dir-retention",
|
|
"never",
|
|
}, &stdout, &stderr)
|
|
if exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
|
|
}
|
|
|
|
runPath := onlyRunDir(t, workDir)
|
|
if _, err := os.Stat(filepath.Join(runPath, "error.log")); err != nil {
|
|
t.Fatalf("expected error.log in retained failed run: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunProcessReportJSONIncludesChunkingSummary(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptPath := writeFile(t, "transcript.json", `[
|
|
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"Hello world this is a test"},
|
|
{"id":2,"speaker":"Bob","start":2.0,"end":3.0,"text":"Another segment here with more text"},
|
|
{"id":3,"speaker":"Alice","start":4.0,"end":5.0,"text":"Final segment text"}
|
|
]`)
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
outputPath := filepath.Join(t.TempDir(), "normalized.json")
|
|
|
|
exitCode := Run([]string{
|
|
"process",
|
|
transcriptPath,
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--output",
|
|
outputPath,
|
|
"--report-json",
|
|
reportPath,
|
|
}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout with --output, got %q", stdout.String())
|
|
}
|
|
|
|
report := readProcessReport(t, reportPath)
|
|
if report.Chunking == nil {
|
|
t.Fatalf("expected chunking summary in report")
|
|
}
|
|
if report.Chunking.ChunkCount == 0 {
|
|
t.Errorf("expected non-zero chunk count")
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestRunProcessChunkingSummaryArtifactWritten(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
workDir := t.TempDir()
|
|
transcriptPath := writeFile(t, "transcript.json", `[
|
|
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"Hello"},
|
|
{"id":2,"speaker":"Bob","start":2.0,"end":3.0,"text":"World"}
|
|
]`)
|
|
|
|
exitCode := Run([]string{
|
|
"process",
|
|
transcriptPath,
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--work-dir",
|
|
workDir,
|
|
"--work-dir-retention",
|
|
"always",
|
|
}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
|
|
runPath := onlyRunDir(t, workDir)
|
|
|
|
// Verify chunking-summary.json exists
|
|
chunkingSummaryPath := filepath.Join(runPath, "chunking-summary.json")
|
|
if _, err := os.Stat(chunkingSummaryPath); err != nil {
|
|
t.Fatalf("expected chunking-summary.json artifact: %v", err)
|
|
}
|
|
|
|
// Verify it contains valid JSON with expected fields
|
|
content := readFile(t, chunkingSummaryPath)
|
|
var summary struct {
|
|
ChunkCount int `json:"chunk_count"`
|
|
MinEstimatedTokens int `json:"min_estimated_chunk_tokens"`
|
|
MaxEstimatedTokens int `json:"max_estimated_chunk_tokens"`
|
|
TotalEstimatedTokens int `json:"total_estimated_transcript_tokens"`
|
|
Chunks []struct {
|
|
Index int `json:"index"`
|
|
StartSegmentID int `json:"start_segment_id"`
|
|
EndSegmentID int `json:"end_segment_id"`
|
|
EstimatedTokens int `json:"estimated_tokens"`
|
|
SegmentCount int `json:"segment_count"`
|
|
} `json:"chunks"`
|
|
}
|
|
if err := json.Unmarshal(content, &summary); err != nil {
|
|
t.Fatalf("failed to parse chunking summary: %v", err)
|
|
}
|
|
if summary.ChunkCount == 0 {
|
|
t.Errorf("expected non-zero chunk count in summary")
|
|
}
|
|
if len(summary.Chunks) != summary.ChunkCount {
|
|
t.Errorf("expected %d chunks, got %d", summary.ChunkCount, len(summary.Chunks))
|
|
}
|
|
}
|
|
|
|
func TestRunProcessTargetSectionsThroughCLI(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptPath := writeFile(t, "transcript.json", `[
|
|
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"Hello world"},
|
|
{"id":2,"speaker":"Bob","start":2.0,"end":3.0,"text":"Another test"},
|
|
{"id":3,"speaker":"Charlie","start":4.0,"end":5.0,"text":"Third segment"},
|
|
{"id":4,"speaker":"Alice","start":6.0,"end":7.0,"text":"Final segment"}
|
|
]`)
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
|
|
exitCode := Run([]string{
|
|
"process",
|
|
transcriptPath,
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--target-sections",
|
|
"2",
|
|
"--report-json",
|
|
reportPath,
|
|
}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
|
|
report := readProcessReport(t, reportPath)
|
|
if report.Chunking == nil {
|
|
t.Fatalf("expected chunking summary in report")
|
|
}
|
|
if report.Chunking.ChunkCount != 2 {
|
|
t.Errorf("expected 2 chunks with --target-sections 2, got %d", report.Chunking.ChunkCount)
|
|
}
|
|
if report.Chunking.TargetSections == nil || *report.Chunking.TargetSections != 2 {
|
|
t.Errorf("expected target_sections=2 in report")
|
|
}
|
|
}
|
|
|
|
func TestRunProcessImpossibleTargetSectionsFailsCleanly(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
// Create transcript with 2 segments
|
|
transcriptPath := writeFile(t, "transcript.json", `[
|
|
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"Hello"},
|
|
{"id":2,"speaker":"Bob","start":2.0,"end":3.0,"text":"World"}
|
|
]`)
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
workDir := t.TempDir()
|
|
|
|
exitCode := Run([]string{
|
|
"process",
|
|
transcriptPath,
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--target-sections",
|
|
"5",
|
|
"--report-json",
|
|
reportPath,
|
|
"--work-dir",
|
|
workDir,
|
|
"--work-dir-retention",
|
|
"always",
|
|
}, &stdout, &stderr)
|
|
if exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code for impossible target-sections")
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "target_sections") {
|
|
t.Errorf("expected target_sections error in stderr, got %q", stderr.String())
|
|
}
|
|
|
|
// Verify report was written with failure info
|
|
report := readProcessReport(t, reportPath)
|
|
if report.Status != "failed" {
|
|
t.Errorf("expected failed status, got %q", report.Status)
|
|
}
|
|
if report.ErrorPhase != "chunking" {
|
|
t.Errorf("expected error phase 'chunking', got %q", report.ErrorPhase)
|
|
}
|
|
|
|
// Verify run dir was retained with error.log
|
|
runPath := onlyRunDir(t, workDir)
|
|
if _, err := os.Stat(filepath.Join(runPath, "error.log")); err != nil {
|
|
t.Errorf("expected error.log in retained failed run: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunProcessOutputTranscriptStillNormalizedOnly(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptPath := writeFile(t, "transcript.json", `[
|
|
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"Hello"},
|
|
{"id":2,"speaker":"Alice","start":1.5,"end":2.5,"text":"world"}
|
|
]`)
|
|
outputPath := filepath.Join(t.TempDir(), "normalized.json")
|
|
|
|
exitCode := Run([]string{
|
|
"process",
|
|
transcriptPath,
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--output",
|
|
outputPath,
|
|
}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout with --output, got %q", stdout.String())
|
|
}
|
|
|
|
// Verify output is the normalized transcript (same as before)
|
|
output := readFile(t, outputPath)
|
|
transcript, err := schema.ParseTranscriptJSON(output)
|
|
if err != nil {
|
|
t.Fatalf("expected valid transcript JSON: %v", err)
|
|
}
|
|
if len(transcript.Segments) != 1 {
|
|
t.Errorf("expected 1 merged segment, got %d", len(transcript.Segments))
|
|
}
|
|
// Text should be normalized (merged with space)
|
|
if transcript.Segments[0].Text != "Hello world" {
|
|
t.Errorf("expected merged text 'Hello world', got %q", transcript.Segments[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestRunProcessReportJSONNotPrintedToStdout(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptPath := writeFile(t, "transcript.json", `[
|
|
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"Hello"}
|
|
]`)
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
|
|
exitCode := Run([]string{
|
|
"process",
|
|
transcriptPath,
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--report-json",
|
|
reportPath,
|
|
}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
|
|
// Verify stdout contains only transcript JSON, not report JSON
|
|
stdoutStr := stdout.String()
|
|
if strings.Contains(stdoutStr, "chunk_count") {
|
|
t.Errorf("stdout should not contain report JSON fields, got: %s", stdoutStr)
|
|
}
|
|
if strings.Contains(stdoutStr, "normalization_merges") {
|
|
t.Errorf("stdout should not contain report JSON fields, got: %s", stdoutStr)
|
|
}
|
|
|
|
// Verify transcript is valid JSON
|
|
if _, err := schema.ParseTranscriptJSON(stdout.Bytes()); err != nil {
|
|
t.Errorf("stdout should contain valid transcript JSON: %v", err)
|
|
}
|
|
}
|
|
|
|
func fixturePath(name string) string {
|
|
return filepath.Join("testdata", name)
|
|
}
|
|
|
|
func schemaFixturePath(name string) string {
|
|
return filepath.Join("..", "core", "schema", "testdata", name)
|
|
}
|
|
|
|
func writeFile(t *testing.T, name string, content string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), name)
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("failed to write test file %q: %v", path, err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func readFile(t *testing.T, path string) []byte {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("failed to read file %q: %v", path, err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func readProcessReport(t *testing.T, path string) reporting.ProcessReport {
|
|
t.Helper()
|
|
raw := readFile(t, path)
|
|
var report reporting.ProcessReport
|
|
if err := json.Unmarshal(raw, &report); err != nil {
|
|
t.Fatalf("failed to parse process report JSON: %v (raw=%s)", err, string(raw))
|
|
}
|
|
return report
|
|
}
|
|
|
|
func onlyRunDir(t *testing.T, workDir string) string {
|
|
t.Helper()
|
|
entries, err := os.ReadDir(workDir)
|
|
if err != nil {
|
|
t.Fatalf("failed to read work dir %q: %v", workDir, err)
|
|
}
|
|
if len(entries) != 1 {
|
|
t.Fatalf("expected exactly one run dir in %q, got %d", workDir, len(entries))
|
|
}
|
|
return filepath.Join(workDir, entries[0].Name())
|
|
}
|