284 lines
8.1 KiB
Go
284 lines
8.1 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(), "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 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))
|
|
}
|
|
}
|