856 lines
28 KiB
Go
856 lines
28 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
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 !strings.Contains(stdout.String(), "process") {
|
|
t.Fatalf("expected process command in root help, 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, got %q", expectedFlag, stdout.String())
|
|
}
|
|
}
|
|
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected empty stderr, got %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunUnknownCommand(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode := Run([]string{"unknown"}, &stdout, &stderr)
|
|
if exitCode != 2 {
|
|
t.Fatalf("expected exit code 2, got %d", exitCode)
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout, got %q", stdout.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "unknown command") {
|
|
t.Fatalf("expected unknown command error in 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 TestRunProcessUnreadableTranscript(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
missingTranscript := filepath.Join(t.TempDir(), "missing.json")
|
|
exitCode := Run([]string{"process", missingTranscript, "--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(), "failed to read transcript file") {
|
|
t.Fatalf("expected unreadable transcript error, got %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunProcessMalformedTranscriptJSON(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode := Run([]string{"process", fixturePath("malformed_transcript.json"), "--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(), "is not valid JSON") {
|
|
t.Fatalf("expected malformed transcript error, got %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunProcessOutputToFile(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(), "corrected.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 when --output is used, got %q", stdout.String())
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected empty stderr on success, got %q", stderr.String())
|
|
}
|
|
|
|
inputBytes := readFile(t, transcriptPath)
|
|
outputBytes := readFile(t, outputPath)
|
|
assertJSONSemanticallyEqual(t, inputBytes, outputBytes)
|
|
}
|
|
|
|
func TestRunProcessOutputToStdout(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptPath := fixturePath("tiny_transcript.json")
|
|
glossaryPath := fixturePath("tiny_glossary.yaml")
|
|
|
|
exitCode := Run([]string{"process", transcriptPath, "--glossary", glossaryPath}, &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())
|
|
}
|
|
|
|
inputBytes := readFile(t, transcriptPath)
|
|
assertJSONSemanticallyEqual(t, inputBytes, stdout.Bytes())
|
|
}
|
|
|
|
func TestRunProcessReportJSONSuccess(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(), "corrected.json")
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
|
|
exitCode := Run([]string{
|
|
"process",
|
|
transcriptPath,
|
|
"--glossary",
|
|
glossaryPath,
|
|
"--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 when --output is used, got %q", stdout.String())
|
|
}
|
|
|
|
report := readProcessReport(t, reportPath)
|
|
if report.Status != "success" {
|
|
t.Fatalf("expected success report status, got %q", report.Status)
|
|
}
|
|
if report.Operation != "process" {
|
|
t.Fatalf("expected operation process, got %q", report.Operation)
|
|
}
|
|
if report.TranscriptPath != transcriptPath {
|
|
t.Fatalf("unexpected transcript path in report: %q", report.TranscriptPath)
|
|
}
|
|
if report.GlossaryPath != glossaryPath {
|
|
t.Fatalf("unexpected glossary path in report: %q", report.GlossaryPath)
|
|
}
|
|
if report.OutputPath != outputPath {
|
|
t.Fatalf("unexpected output path in report: %q", report.OutputPath)
|
|
}
|
|
if len(report.Modules) == 0 {
|
|
t.Fatalf("expected configured module sequence in report")
|
|
}
|
|
if report.StartedAt == "" || report.CompletedAt == "" {
|
|
t.Fatalf("expected started_at and completed_at timestamps in report")
|
|
}
|
|
if report.ErrorMessage != "" {
|
|
t.Fatalf("did not expect error message in success report, got %q", report.ErrorMessage)
|
|
}
|
|
}
|
|
|
|
func TestRunProcessReportJSONNoStdoutPollution(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptPath := fixturePath("tiny_transcript.json")
|
|
glossaryPath := fixturePath("tiny_glossary.yaml")
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
|
|
exitCode := Run([]string{
|
|
"process",
|
|
transcriptPath,
|
|
"--glossary",
|
|
glossaryPath,
|
|
"--report-json",
|
|
reportPath,
|
|
}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
assertJSONSemanticallyEqual(t, readFile(t, transcriptPath), stdout.Bytes())
|
|
|
|
reportBytes := readFile(t, reportPath)
|
|
if bytes.Contains(stdout.Bytes(), reportBytes) {
|
|
t.Fatalf("stdout was polluted with report JSON")
|
|
}
|
|
}
|
|
|
|
func TestRunProcessReportJSONRedactsSecrets(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
secretPrimary := "top-secret-primary-key"
|
|
secretValidation := "top-secret-validation-key"
|
|
t.Setenv("AUDITA_LLM_API_KEY", secretPrimary)
|
|
t.Setenv("AUDITA_VALIDATION_LLM_API_KEY", secretValidation)
|
|
|
|
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), secretPrimary) || strings.Contains(string(reportBytes), secretValidation) {
|
|
t.Fatalf("report JSON leaked API key material: %q", string(reportBytes))
|
|
}
|
|
}
|
|
|
|
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 for malformed transcript")
|
|
}
|
|
|
|
report := readProcessReport(t, reportPath)
|
|
if report.Status != "failed" {
|
|
t.Fatalf("expected failed report status, got %q", report.Status)
|
|
}
|
|
if report.ErrorMessage == "" {
|
|
t.Fatalf("expected error message in failed report")
|
|
}
|
|
if !strings.Contains(report.ErrorMessage, "not valid JSON") {
|
|
t.Fatalf("expected malformed JSON failure in report error, got %q", report.ErrorMessage)
|
|
}
|
|
}
|
|
|
|
func TestRunProcessInvalidTranscriptSchema(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode := Run([]string{"process", fixturePath("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(), "invalid transcript:") {
|
|
t.Fatalf("expected invalid transcript error, got %q", stderr.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "speaker") {
|
|
t.Fatalf("expected speaker validation error, got %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunProcessValidBareArrayTranscript(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode := Run([]string{"process", "../core/schema/testdata/transcript_bare_array.json", "--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())
|
|
}
|
|
|
|
// Should produce valid JSON output
|
|
if !json.Valid(stdout.Bytes()) {
|
|
t.Fatalf("expected valid JSON output, got %q", stdout.String())
|
|
}
|
|
|
|
// Parse the output to verify it's a proper transcript
|
|
var output any
|
|
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
|
|
t.Fatalf("failed to unmarshal output JSON: %v", err)
|
|
}
|
|
|
|
// Should be an array at the top level (canonical object form would be {"segments": [...]})
|
|
outputArray, ok := output.([]any)
|
|
if !ok {
|
|
t.Fatalf("expected output to be JSON array, got %T", output)
|
|
}
|
|
if len(outputArray) != 2 {
|
|
t.Fatalf("expected 2 segments in output, got %d", len(outputArray))
|
|
}
|
|
}
|
|
|
|
func TestRunProcessInvalidGlossarySchema(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode := Run([]string{"process", fixturePath("tiny_transcript.json"), "--glossary", "../core/schema/testdata/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(), "invalid glossary:") {
|
|
t.Fatalf("expected invalid glossary error, got %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func fixturePath(name string) string {
|
|
return filepath.Join("testdata", name)
|
|
}
|
|
|
|
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 assertJSONSemanticallyEqual(t *testing.T, expected []byte, actual []byte) {
|
|
t.Helper()
|
|
if !json.Valid(actual) {
|
|
t.Fatalf("actual output is not valid JSON: %q", string(actual))
|
|
}
|
|
|
|
var expectedValue any
|
|
var actualValue any
|
|
if err := json.Unmarshal(expected, &expectedValue); err != nil {
|
|
t.Fatalf("failed to unmarshal expected JSON: %v", err)
|
|
}
|
|
if err := json.Unmarshal(actual, &actualValue); err != nil {
|
|
t.Fatalf("failed to unmarshal actual JSON: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(expectedValue, actualValue) {
|
|
t.Fatalf("JSON content mismatch: expected %q got %q", string(expected), string(actual))
|
|
}
|
|
}
|
|
|
|
type minimalProcessReport struct {
|
|
Status string `json:"status"`
|
|
Operation string `json:"operation"`
|
|
TranscriptPath string `json:"transcript_path"`
|
|
GlossaryPath string `json:"glossary_path"`
|
|
OutputPath string `json:"output_path"`
|
|
Modules []string `json:"modules"`
|
|
StartedAt string `json:"started_at"`
|
|
CompletedAt string `json:"completed_at"`
|
|
ErrorMessage string `json:"error_message"`
|
|
}
|
|
|
|
func readProcessReport(t *testing.T, path string) minimalProcessReport {
|
|
t.Helper()
|
|
raw := readFile(t, path)
|
|
var report minimalProcessReport
|
|
if err := json.Unmarshal(raw, &report); err != nil {
|
|
t.Fatalf("failed to unmarshal process report JSON: %v (raw: %q)", err, string(raw))
|
|
}
|
|
func TestRunProcessStdoutBehaviorWithoutOutput(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode := Run([]string{"process", "../core/schema/testdata/transcript_bare_array.json", "--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())
|
|
}
|
|
|
|
// Should have transcript JSON in stdout
|
|
if stdout.Len() == 0 {
|
|
t.Fatalf("expected transcript JSON in stdout, got empty stdout")
|
|
}
|
|
if !json.Valid(stdout.Bytes()) {
|
|
t.Fatalf("expected valid JSON in stdout, got %q", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestRunProcessEmptyStdoutWithOutput(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptPath := "../core/schema/testdata/transcript_bare_array.json"
|
|
glossaryPath := fixturePath("tiny_glossary.yaml")
|
|
outputPath := filepath.Join(t.TempDir(), "corrected.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 when --output is used, got %q", stdout.String())
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected empty stderr on success, got %q", stderr.String())
|
|
}
|
|
|
|
// Verify output file was created and contains valid JSON
|
|
outputBytes := readFile(t, outputPath)
|
|
if !json.Valid(outputBytes) {
|
|
t.Fatalf("expected valid JSON in output file, got %q", string(outputBytes))
|
|
}
|
|
}
|
|
|
|
func TestRunProcessNormalizationMergeInOutput(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
// Create a test transcript with segments that should be merged
|
|
transcriptJSON := `[
|
|
{"id": 1, "speaker": "Alice", "start": 0.0, "end": 1.0, "text": "Hello"},
|
|
{"id": 2, "speaker": "Alice", "start": 1.5, "end": 2.5, "text": "world"}
|
|
]`
|
|
|
|
transcriptPath := filepath.Join(t.TempDir(), "transcript.json")
|
|
glossaryPath := fixturePath("tiny_glossary.yaml")
|
|
outputPath := filepath.Join(t.TempDir(), "corrected.json")
|
|
|
|
if err := os.WriteFile(transcriptPath, []byte(transcriptJSON), 0644); err != nil {
|
|
t.Fatalf("failed to create test transcript: %v", err)
|
|
}
|
|
|
|
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 when --output is used, got %q", stdout.String())
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected empty stderr on success, got %q", stderr.String())
|
|
}
|
|
|
|
// Read and parse the output
|
|
outputBytes := readFile(t, outputPath)
|
|
var outputTranscript schema.Transcript
|
|
if err := schema.ParseTranscriptJSON(outputBytes); err != nil {
|
|
t.Fatalf("failed to parse output transcript: %v (output: %s)", err, string(outputBytes))
|
|
}
|
|
|
|
// Should have merged the two segments into one
|
|
if len(outputTranscript.Segments) != 1 {
|
|
t.Fatalf("expected 1 merged segment, got %d segments", len(outputTranscript.Segments))
|
|
}
|
|
|
|
// Should have sequential ID starting at 1
|
|
if outputTranscript.Segments[0].ID != 1 {
|
|
t.Fatalf("expected segment ID 1, got %d", outputTranscript.Segments[0].ID)
|
|
}
|
|
|
|
// Should contain merged text
|
|
if !strings.Contains(outputTranscript.Segments[0].Text, "Hello") || !strings.Contains(outputTranscript.Segments[0].Text, "world") {
|
|
t.Fatalf("expected merged text to contain both parts, got %q", outputTranscript.Segments[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestRunProcessNormalizedOutputToStdout(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptJSON := `[
|
|
{"id": 1, "speaker": "Alice", "start": 0.0, "end": 1.0, "text": "Hello"},
|
|
{"id": 2, "speaker": "Alice", "start": 1.5, "end": 2.5, "text": "world"}
|
|
]`
|
|
|
|
transcriptPath := filepath.Join(t.TempDir(), "transcript.json")
|
|
glossaryPath := fixturePath("tiny_glossary.yaml")
|
|
|
|
if err := os.WriteFile(transcriptPath, []byte(transcriptJSON), 0644); err != nil {
|
|
t.Fatalf("failed to create test transcript: %v", err)
|
|
}
|
|
|
|
exitCode := Run([]string{"process", transcriptPath, "--glossary", glossaryPath}, &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())
|
|
}
|
|
|
|
// Should have normalized output in stdout
|
|
if stdout.Len() == 0 {
|
|
t.Fatalf("expected normalized transcript in stdout, got empty stdout")
|
|
}
|
|
|
|
// Should be valid JSON
|
|
if !json.Valid(stdout.Bytes()) {
|
|
t.Fatalf("expected valid JSON in stdout, got %q", stdout.String())
|
|
}
|
|
|
|
// Parse and verify it's normalized (merged)
|
|
var outputTranscript schema.Transcript
|
|
if err := schema.ParseTranscriptJSON(stdout.Bytes()); err != nil {
|
|
t.Fatalf("failed to parse stdout transcript: %v", err)
|
|
}
|
|
|
|
if len(outputTranscript.Segments) != 1 {
|
|
t.Fatalf("expected 1 merged segment in stdout, got %d", len(outputTranscript.Segments))
|
|
}
|
|
}
|
|
|
|
func TestRunProcessSequentialIDsInOutput(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptJSON := `[
|
|
{"id": 5, "speaker": "Alice", "start": 0.0, "end": 1.0, "text": "Hello"},
|
|
{"id": 10, "speaker": "Bob", "start": 2.0, "end": 3.0, "text": "Hi"}
|
|
]`
|
|
|
|
transcriptPath := filepath.Join(t.TempDir(), "transcript.json")
|
|
glossaryPath := fixturePath("tiny_glossary.yaml")
|
|
|
|
if err := os.WriteFile(transcriptPath, []byte(transcriptJSON), 0644); err != nil {
|
|
t.Fatalf("failed to create test transcript: %v", err)
|
|
}
|
|
|
|
exitCode := Run([]string{"process", transcriptPath, "--glossary", glossaryPath}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
|
|
// Parse output and verify sequential IDs
|
|
var outputTranscript schema.Transcript
|
|
if err := schema.ParseTranscriptJSON(stdout.Bytes()); err != nil {
|
|
t.Fatalf("failed to parse output transcript: %v", err)
|
|
}
|
|
|
|
// Should have sequential IDs starting at 1
|
|
for i, segment := range outputTranscript.Segments {
|
|
expectedID := i + 1
|
|
if segment.ID != expectedID {
|
|
t.Fatalf("expected segment %d to have ID %d, got %d", i, expectedID, segment.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunProcessNoStdoutWithOutput(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptJSON := `[
|
|
{"id": 1, "speaker": "Alice", "start": 0.0, "end": 1.0, "text": "Hello"}
|
|
]`
|
|
|
|
transcriptPath := filepath.Join(t.TempDir(), "transcript.json")
|
|
glossaryPath := fixturePath("tiny_glossary.yaml")
|
|
outputPath := filepath.Join(t.TempDir(), "corrected.json")
|
|
|
|
if err := os.WriteFile(transcriptPath, []byte(transcriptJSON), 0644); err != nil {
|
|
t.Fatalf("failed to create test transcript: %v", err)
|
|
}
|
|
|
|
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 when --output is used, got %q", stdout.String())
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected empty stderr on success, got %q", stderr.String())
|
|
}
|
|
|
|
// Verify output file exists and is valid
|
|
if _, err := os.Stat(outputPath); err != nil {
|
|
t.Fatalf("expected output file to be created: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunProcessReportIncludesNormalizationSummary(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
// Create a test transcript with segments that will be merged
|
|
transcriptJSON := `[
|
|
{"id": 1, "speaker": "Alice", "start": 0.0, "end": 1.0, "text": "Hello"},
|
|
{"id": 2, "speaker": "Alice", "start": 1.5, "end": 2.5, "text": "world"}
|
|
]`
|
|
|
|
transcriptPath := filepath.Join(t.TempDir(), "transcript.json")
|
|
glossaryPath := fixturePath("tiny_glossary.yaml")
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
|
|
if err := os.WriteFile(transcriptPath, []byte(transcriptJSON), 0644); err != nil {
|
|
t.Fatalf("failed to create test transcript: %v", err)
|
|
}
|
|
|
|
exitCode := Run([]string{"process", transcriptPath, "--glossary", glossaryPath, "--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 when --output is not used but --report-json is provided, got %q", stdout.String())
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected empty stderr on success, got %q", stderr.String())
|
|
}
|
|
|
|
// Read and parse the report
|
|
reportBytes := readFile(t, reportPath)
|
|
var report reporting.ProcessReport
|
|
if err := json.Unmarshal(reportBytes, &report); err != nil {
|
|
t.Fatalf("failed to parse report JSON: %v (raw: %s)", err, string(reportBytes))
|
|
}
|
|
|
|
// Verify report includes normalization summary
|
|
if report.Status != "success" {
|
|
t.Fatalf("expected success status, got %q", report.Status)
|
|
}
|
|
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)
|
|
}
|
|
if report.NormalizationIDReassignments == nil || *report.NormalizationIDReassignments != 0 {
|
|
t.Fatalf("expected 0 ID reassignments (already sequential), got %v", report.NormalizationIDReassignments)
|
|
}
|
|
}
|
|
|
|
func TestRunProcessReportFailedTranscriptSchema(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
exitCode := Run([]string{
|
|
"process",
|
|
fixturePath("transcript_empty_speaker.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--report-json",
|
|
reportPath,
|
|
}, &stdout, &stderr)
|
|
if exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code for invalid transcript")
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
|
|
}
|
|
|
|
// Read and parse the report
|
|
reportBytes := readFile(t, reportPath)
|
|
var report reporting.ProcessReport
|
|
if err := json.Unmarshal(reportBytes, &report); err != nil {
|
|
t.Fatalf("failed to parse report JSON: %v (raw: %s)", err, string(reportBytes))
|
|
}
|
|
|
|
// Verify failed report structure
|
|
if report.Status != "failed" {
|
|
t.Fatalf("expected failed status, got %q", report.Status)
|
|
}
|
|
if report.ErrorPhase != "transcript_schema" {
|
|
t.Fatalf("expected error phase 'transcript_schema', got %q", report.ErrorPhase)
|
|
}
|
|
if report.ErrorMessage == "" {
|
|
t.Fatalf("expected error message in failed report")
|
|
}
|
|
if !strings.Contains(report.ErrorMessage, "speaker") {
|
|
t.Fatalf("expected speaker validation error, got %q", report.ErrorMessage)
|
|
}
|
|
}
|
|
|
|
func TestRunProcessReportFailedGlossarySchema(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
exitCode := Run([]string{
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
"../core/schema/testdata/glossary_missing_fields.yaml",
|
|
"--report-json",
|
|
reportPath,
|
|
}, &stdout, &stderr)
|
|
if exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code for invalid glossary")
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
|
|
}
|
|
|
|
// Read and parse the report
|
|
reportBytes := readFile(t, reportPath)
|
|
var report reporting.ProcessReport
|
|
if err := json.Unmarshal(reportBytes, &report); err != nil {
|
|
t.Fatalf("failed to parse report JSON: %v (raw: %s)", err, string(reportBytes))
|
|
}
|
|
|
|
// Verify failed report structure
|
|
if report.Status != "failed" {
|
|
t.Fatalf("expected failed status, got %q", report.Status)
|
|
}
|
|
if report.ErrorPhase != "glossary_schema" {
|
|
t.Fatalf("expected error phase 'glossary_schema', got %q", report.ErrorPhase)
|
|
}
|
|
if report.ErrorMessage == "" {
|
|
t.Fatalf("expected error message in failed report")
|
|
}
|
|
}
|
|
|
|
func TestRunProcessReportJSONNotInStdout(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
transcriptJSON := `[
|
|
{"id": 1, "speaker": "Alice", "start": 0.0, "end": 1.0, "text": "Hello"}
|
|
]`
|
|
|
|
transcriptPath := filepath.Join(t.TempDir(), "transcript.json")
|
|
glossaryPath := fixturePath("tiny_glossary.yaml")
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
|
|
if err := os.WriteFile(transcriptPath, []byte(transcriptJSON), 0644); err != nil {
|
|
t.Fatalf("failed to create test transcript: %v", err)
|
|
}
|
|
|
|
exitCode := Run([]string{"process", transcriptPath, "--glossary", glossaryPath, "--report-json", reportPath}, &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
|
}
|
|
|
|
// Read the report to get its content
|
|
reportBytes := readFile(t, reportPath)
|
|
|
|
// Verify report JSON is not in stdout
|
|
if bytes.Contains(stdout.Bytes(), reportBytes) {
|
|
t.Fatalf("stdout was polluted with report JSON")
|
|
}
|
|
|
|
// Verify stdout only contains transcript JSON
|
|
if !json.Valid(stdout.Bytes()) {
|
|
t.Fatalf("expected valid JSON in stdout, got %q", stdout.String())
|
|
}
|
|
|
|
// Parse stdout to verify it's a transcript, not a report
|
|
var stdoutContent map[string]interface{}
|
|
if err := json.Unmarshal(stdout.Bytes(), &stdoutContent); err != nil {
|
|
t.Fatalf("failed to parse stdout as JSON: %v", err)
|
|
}
|
|
|
|
// Transcript should be an array at top level
|
|
if _, isArray := stdoutContent["segments"]; !isArray && stdoutContent["segments"] != nil {
|
|
t.Fatalf("expected transcript in stdout, got report structure")
|
|
}
|
|
}
|