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 [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(), "transcript file 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 TestRunProcessCLIOverridesEnvironment(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer t.Setenv("AUDITA_MODEL", "env-model") t.Setenv("AUDITA_VALIDATION_LLM_CONCURRENCY", "2") transcriptPath := fixturePath("tiny_transcript.json") glossaryPath := fixturePath("tiny_glossary.yaml") var captured processInvocation originalRunner := processRunner processRunner = func(inv processInvocation, output io.Writer) error { _ = output captured = inv return nil } t.Cleanup(func() { processRunner = originalRunner }) exitCode := Run([]string{ "process", transcriptPath, "--glossary", glossaryPath, "--model", "cli-model", "--validation-llm-concurrency", "5", }, &stdout, &stderr) if exitCode != 0 { t.Fatalf("expected exit code 0, got %d", exitCode) } if captured.Config.PrimaryLLM.Model != "cli-model" { t.Fatalf("expected CLI model override, got %q", captured.Config.PrimaryLLM.Model) } if captured.Config.ValidationLLM.Concurrency == nil || *captured.Config.ValidationLLM.Concurrency != 5 { t.Fatalf("expected CLI validation concurrency override, got %#v", captured.Config.ValidationLLM.Concurrency) } if captured.GlossaryPath != glossaryPath { t.Fatalf("unexpected glossary path: %q", captured.GlossaryPath) } } 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)) } return report }