Wire process command flags

This commit is contained in:
2026-05-10 23:24:48 +00:00
parent 9427c4e6cc
commit 8f3c2ec5fd
7 changed files with 355 additions and 125 deletions

View File

@@ -12,6 +12,19 @@ import (
const processNotImplementedMessage = "process command is not implemented yet"
type processInvocation struct {
TranscriptPath string
GlossaryPath string
OutputPath string
ReportJSONPath string
Config config.Config
}
var processRunner = func(inv processInvocation) error {
_ = inv
return errors.New(processNotImplementedMessage)
}
// Run executes the Audita CLI with the provided arguments and streams.
func Run(args []string, stdout, stderr io.Writer) int {
if len(args) == 0 {
@@ -34,92 +47,29 @@ func Run(args []string, stdout, stderr io.Writer) int {
}
func runProcess(args []string, stdout, stderr io.Writer) int {
if isHelpCommand(args) || hasHelpFlag(args) {
writeProcessUsage(stdout)
return 0
}
cfg, err := config.LoadFromEnv()
if err != nil {
fmt.Fprintf(stderr, "audita process: invalid environment configuration: %v\n", err)
return 2
}
fs := flag.NewFlagSet("process", flag.ContinueOnError)
fs.SetOutput(stderr)
fs, pFlags := newProcessFlagSet(cfg, stderr)
glossaryPath := fs.String("glossary", "", "Path to glossary YAML file")
outputPath := fs.String("output", "", "Path to corrected transcript JSON output file")
reportJSONPath := fs.String("report-json", "", "Path to machine-readable report JSON output file")
modules := fs.String("modules", strings.Join(cfg.Modules, ","), "Comma-separated module sequence override")
llmAPIKey := fs.String("llm-api-key", cfg.PrimaryLLM.APIKey, "Primary LLM API key")
validationLLMAPIKey := fs.String("validation-llm-api-key", cfg.ValidationLLM.APIKey, "Validation LLM API key")
model := fs.String("model", cfg.PrimaryLLM.Model, "Primary LLM model name")
validationModel := fs.String("validation-model", cfg.ValidationLLM.Model, "Validation LLM model name")
baseURL := fs.String("base-url", cfg.PrimaryLLM.BaseURL, "Primary OpenAI-compatible base URL")
validationBaseURL := fs.String("validation-base-url", cfg.ValidationLLM.BaseURL, "Validation OpenAI-compatible base URL")
llmTimeoutSeconds := fs.Int("llm-timeout-seconds", cfg.PrimaryLLM.TimeoutSeconds, "Primary LLM timeout in seconds")
validationTimeoutSecondsDefault := cfg.PrimaryLLM.TimeoutSeconds
if cfg.ValidationLLM.TimeoutSeconds != nil {
validationTimeoutSecondsDefault = *cfg.ValidationLLM.TimeoutSeconds
if isHelpCommand(args) || hasHelpFlag(args) {
writeProcessUsage(stdout, fs)
return 0
}
validationLLMTimeoutSeconds := fs.Int("validation-llm-timeout-seconds", validationTimeoutSecondsDefault, "Validation LLM timeout in seconds")
maxRetries := fs.Int("max-retries", cfg.PrimaryLLM.MaxRetries, "Maximum structured-output retries")
validationMaxRetriesDefault := cfg.PrimaryLLM.MaxRetries
if cfg.ValidationLLM.MaxRetries != nil {
validationMaxRetriesDefault = *cfg.ValidationLLM.MaxRetries
}
validationMaxRetries := fs.Int("validation-max-retries", validationMaxRetriesDefault, "Validation structured-output retries")
validationMaxPromptTokens := fs.Int("validation-max-prompt-tokens", cfg.ValidationMaxPromptTokens, "Validation max prompt tokens")
maxSectionTokens := fs.Int("max-section-tokens", cfg.MaxSectionTokens, "Max section tokens")
minSectionTokens := fs.Int("min-section-tokens", cfg.MinSectionTokens, "Min section tokens")
targetSectionsDefault := 0
if cfg.TargetSections != nil {
targetSectionsDefault = *cfg.TargetSections
}
targetSections := fs.Int("target-sections", targetSectionsDefault, "Target number of transcript sections")
glossaryConfidenceThreshold := fs.Float64("glossary-confidence-threshold", cfg.Thresholds.Glossary, "Glossary confidence threshold")
grammarConfidenceThreshold := fs.Float64("grammar-confidence-threshold", cfg.Thresholds.Grammar, "Grammar confidence threshold")
homophonesConfidenceThreshold := fs.Float64("homophones-confidence-threshold", cfg.Thresholds.Homophones, "Homophones confidence threshold")
spokenWordConfidenceThreshold := fs.Float64("spoken-word-confidence-threshold", cfg.Thresholds.SpokenWord, "Spoken-word confidence threshold")
normalizeMaxSegmentGap := fs.Float64("normalize-max-segment-gap", cfg.Normalization.MaxSegmentGap, "Maximum same-speaker merge gap")
normalizeEllipsisGap := fs.Float64("normalize-ellipsis-gap", cfg.Normalization.EllipsisGap, "Gap threshold for ellipsis insertion")
normalizeMaxSegmentDuration := fs.Float64("normalize-max-segment-duration", cfg.Normalization.MaxSegmentDuration, "Maximum merged segment duration")
normalizeMaxSegmentTokens := fs.Int("normalize-max-segment-tokens", cfg.Normalization.MaxSegmentTokens, "Maximum merged segment token estimate")
workDir := fs.String("work-dir", cfg.WorkDir, "Per-run work directory")
workDirRetention := fs.String("work-dir-retention", string(cfg.WorkDirRetention), "Work-dir retention policy: auto|always|never")
_ = glossaryPath
_ = outputPath
_ = reportJSONPath
_ = llmAPIKey
_ = validationLLMAPIKey
_ = model
_ = validationModel
_ = baseURL
_ = validationBaseURL
_ = llmTimeoutSeconds
_ = validationLLMTimeoutSeconds
_ = maxRetries
_ = validationMaxRetries
_ = validationMaxPromptTokens
_ = maxSectionTokens
_ = minSectionTokens
_ = targetSections
_ = glossaryConfidenceThreshold
_ = grammarConfidenceThreshold
_ = homophonesConfidenceThreshold
_ = spokenWordConfidenceThreshold
_ = normalizeMaxSegmentGap
_ = normalizeEllipsisGap
_ = normalizeMaxSegmentDuration
_ = normalizeMaxSegmentTokens
_ = workDir
_ = workDirRetention
parseArgs := args
transcriptFromFront := ""
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
transcriptFromFront = args[0]
parseArgs = args[1:]
}
if err := fs.Parse(args); err != nil {
if err := fs.Parse(parseArgs); err != nil {
if errors.Is(err, flag.ErrHelp) {
writeProcessUsage(stdout)
writeProcessUsage(stdout, fs)
return 0
}
return 2
@@ -129,55 +79,57 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
fs.Visit(func(f *flag.Flag) {
switch f.Name {
case "modules":
overrides.ModulesCSV = modules
overrides.ModulesCSV = pFlags.modules
case "llm-api-key":
overrides.PrimaryLLMAPIKey = llmAPIKey
overrides.PrimaryLLMAPIKey = pFlags.llmAPIKey
case "validation-llm-api-key":
overrides.ValidationLLMAPIKey = validationLLMAPIKey
overrides.ValidationLLMAPIKey = pFlags.validationLLMAPIKey
case "model":
overrides.PrimaryModel = model
overrides.PrimaryModel = pFlags.model
case "validation-model":
overrides.ValidationModel = validationModel
overrides.ValidationModel = pFlags.validationModel
case "base-url":
overrides.PrimaryBaseURL = baseURL
overrides.PrimaryBaseURL = pFlags.baseURL
case "validation-base-url":
overrides.ValidationBaseURL = validationBaseURL
overrides.ValidationBaseURL = pFlags.validationBaseURL
case "llm-timeout-seconds":
overrides.PrimaryLLMTimeoutSeconds = llmTimeoutSeconds
overrides.PrimaryLLMTimeoutSeconds = pFlags.llmTimeoutSeconds
case "validation-llm-timeout-seconds":
overrides.ValidationLLMTimeoutSeconds = validationLLMTimeoutSeconds
overrides.ValidationLLMTimeoutSeconds = pFlags.validationLLMTimeoutSeconds
case "max-retries":
overrides.MaxRetries = maxRetries
overrides.MaxRetries = pFlags.maxRetries
case "validation-max-retries":
overrides.ValidationMaxRetries = validationMaxRetries
overrides.ValidationMaxRetries = pFlags.validationMaxRetries
case "validation-llm-concurrency":
overrides.ValidationLLMConcurrency = pFlags.validationLLMConcurrency
case "validation-max-prompt-tokens":
overrides.ValidationMaxPromptTokens = validationMaxPromptTokens
overrides.ValidationMaxPromptTokens = pFlags.validationMaxPromptTokens
case "max-section-tokens":
overrides.MaxSectionTokens = maxSectionTokens
overrides.MaxSectionTokens = pFlags.maxSectionTokens
case "min-section-tokens":
overrides.MinSectionTokens = minSectionTokens
overrides.MinSectionTokens = pFlags.minSectionTokens
case "target-sections":
overrides.TargetSections = targetSections
overrides.TargetSections = pFlags.targetSections
case "glossary-confidence-threshold":
overrides.GlossaryConfidenceThreshold = glossaryConfidenceThreshold
overrides.GlossaryConfidenceThreshold = pFlags.glossaryConfidenceThreshold
case "grammar-confidence-threshold":
overrides.GrammarConfidenceThreshold = grammarConfidenceThreshold
overrides.GrammarConfidenceThreshold = pFlags.grammarConfidenceThreshold
case "homophones-confidence-threshold":
overrides.HomophonesConfidenceThreshold = homophonesConfidenceThreshold
overrides.HomophonesConfidenceThreshold = pFlags.homophonesConfidenceThreshold
case "spoken-word-confidence-threshold":
overrides.SpokenWordConfidenceThreshold = spokenWordConfidenceThreshold
overrides.SpokenWordConfidenceThreshold = pFlags.spokenWordConfidenceThreshold
case "normalize-max-segment-gap":
overrides.NormalizeMaxSegmentGap = normalizeMaxSegmentGap
overrides.NormalizeMaxSegmentGap = pFlags.normalizeMaxSegmentGap
case "normalize-ellipsis-gap":
overrides.NormalizeEllipsisGap = normalizeEllipsisGap
overrides.NormalizeEllipsisGap = pFlags.normalizeEllipsisGap
case "normalize-max-segment-duration":
overrides.NormalizeMaxSegmentDuration = normalizeMaxSegmentDuration
overrides.NormalizeMaxSegmentDuration = pFlags.normalizeMaxSegmentDuration
case "normalize-max-segment-tokens":
overrides.NormalizeMaxSegmentTokens = normalizeMaxSegmentTokens
overrides.NormalizeMaxSegmentTokens = pFlags.normalizeMaxSegmentTokens
case "work-dir":
overrides.WorkDir = workDir
overrides.WorkDir = pFlags.workDir
case "work-dir-retention":
overrides.WorkDirRetention = workDirRetention
overrides.WorkDirRetention = pFlags.workDirRetention
}
})
@@ -187,15 +139,126 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
}
remaining := fs.Args()
if len(remaining) != 1 {
positional := make([]string, 0, len(remaining)+1)
if transcriptFromFront != "" {
positional = append(positional, transcriptFromFront)
}
positional = append(positional, remaining...)
if len(positional) != 1 {
fmt.Fprintln(stderr, "audita process: expected exactly 1 transcript JSON path argument")
fmt.Fprintln(stderr)
writeProcessUsage(stderr)
return 2
}
if strings.TrimSpace(*pFlags.glossaryPath) == "" {
fmt.Fprintln(stderr, "audita process: --glossary is required")
return 2
}
fmt.Fprintf(stderr, "audita process: %s\n", processNotImplementedMessage)
return 1
inv := processInvocation{
TranscriptPath: positional[0],
GlossaryPath: *pFlags.glossaryPath,
OutputPath: *pFlags.outputPath,
ReportJSONPath: *pFlags.reportJSONPath,
Config: cfg,
}
if err := processRunner(inv); err != nil {
fmt.Fprintf(stderr, "audita process: %v\n", err)
return 1
}
return 0
}
type processFlags struct {
glossaryPath *string
outputPath *string
reportJSONPath *string
modules *string
llmAPIKey *string
validationLLMAPIKey *string
model *string
validationModel *string
baseURL *string
validationBaseURL *string
llmTimeoutSeconds *int
validationLLMTimeoutSeconds *int
validationMaxPromptTokens *int
targetSections *int
maxRetries *int
validationMaxRetries *int
validationLLMConcurrency *int
maxSectionTokens *int
minSectionTokens *int
glossaryConfidenceThreshold *float64
grammarConfidenceThreshold *float64
homophonesConfidenceThreshold *float64
spokenWordConfidenceThreshold *float64
normalizeMaxSegmentGap *float64
normalizeEllipsisGap *float64
normalizeMaxSegmentDuration *float64
normalizeMaxSegmentTokens *int
workDir *string
workDirRetention *string
}
func newProcessFlagSet(cfg config.Config, stderr io.Writer) (*flag.FlagSet, processFlags) {
fs := flag.NewFlagSet("process", flag.ContinueOnError)
fs.SetOutput(stderr)
validationTimeoutSecondsDefault := cfg.PrimaryLLM.TimeoutSeconds
if cfg.ValidationLLM.TimeoutSeconds != nil {
validationTimeoutSecondsDefault = *cfg.ValidationLLM.TimeoutSeconds
}
validationMaxRetriesDefault := cfg.PrimaryLLM.MaxRetries
if cfg.ValidationLLM.MaxRetries != nil {
validationMaxRetriesDefault = *cfg.ValidationLLM.MaxRetries
}
validationLLMConcurrencyDefault := cfg.PrimaryLLM.Concurrency
if cfg.ValidationLLM.Concurrency != nil {
validationLLMConcurrencyDefault = *cfg.ValidationLLM.Concurrency
}
targetSectionsDefault := 0
if cfg.TargetSections != nil {
targetSectionsDefault = *cfg.TargetSections
}
pFlags := processFlags{
glossaryPath: fs.String("glossary", "", "Path to glossary YAML file"),
outputPath: fs.String("output", "", "Path to corrected transcript JSON output file"),
reportJSONPath: fs.String("report-json", "", "Path to machine-readable report JSON output file"),
modules: fs.String("modules", strings.Join(cfg.Modules, ","), "Comma-separated module sequence override"),
llmAPIKey: fs.String("llm-api-key", cfg.PrimaryLLM.APIKey, "Primary LLM API key"),
validationLLMAPIKey: fs.String("validation-llm-api-key", cfg.ValidationLLM.APIKey, "Validation LLM API key"),
model: fs.String("model", cfg.PrimaryLLM.Model, "Primary LLM model name"),
validationModel: fs.String("validation-model", cfg.ValidationLLM.Model, "Validation LLM model name"),
baseURL: fs.String("base-url", cfg.PrimaryLLM.BaseURL, "Primary OpenAI-compatible base URL"),
validationBaseURL: fs.String("validation-base-url", cfg.ValidationLLM.BaseURL, "Validation OpenAI-compatible base URL"),
llmTimeoutSeconds: fs.Int("llm-timeout-seconds", cfg.PrimaryLLM.TimeoutSeconds, "Primary LLM timeout in seconds"),
validationLLMTimeoutSeconds: fs.Int("validation-llm-timeout-seconds", validationTimeoutSecondsDefault, "Validation LLM timeout in seconds"),
validationMaxPromptTokens: fs.Int("validation-max-prompt-tokens", cfg.ValidationMaxPromptTokens, "Validation max prompt tokens"),
targetSections: fs.Int("target-sections", targetSectionsDefault, "Target number of transcript sections"),
maxRetries: fs.Int("max-retries", cfg.PrimaryLLM.MaxRetries, "Maximum structured-output retries"),
validationMaxRetries: fs.Int("validation-max-retries", validationMaxRetriesDefault, "Validation structured-output retries"),
validationLLMConcurrency: fs.Int("validation-llm-concurrency", validationLLMConcurrencyDefault, "Validation LLM concurrency"),
maxSectionTokens: fs.Int("max-section-tokens", cfg.MaxSectionTokens, "Maximum section tokens"),
minSectionTokens: fs.Int("min-section-tokens", cfg.MinSectionTokens, "Minimum section tokens"),
glossaryConfidenceThreshold: fs.Float64("glossary-confidence-threshold", cfg.Thresholds.Glossary, "Glossary confidence threshold"),
grammarConfidenceThreshold: fs.Float64("grammar-confidence-threshold", cfg.Thresholds.Grammar, "Grammar confidence threshold"),
homophonesConfidenceThreshold: fs.Float64("homophones-confidence-threshold", cfg.Thresholds.Homophones, "Homophones confidence threshold"),
spokenWordConfidenceThreshold: fs.Float64("spoken-word-confidence-threshold", cfg.Thresholds.SpokenWord, "Spoken-word confidence threshold"),
normalizeMaxSegmentGap: fs.Float64("normalize-max-segment-gap", cfg.Normalization.MaxSegmentGap, "Maximum same-speaker merge gap"),
normalizeEllipsisGap: fs.Float64("normalize-ellipsis-gap", cfg.Normalization.EllipsisGap, "Gap threshold for ellipsis insertion"),
normalizeMaxSegmentDuration: fs.Float64("normalize-max-segment-duration", cfg.Normalization.MaxSegmentDuration, "Maximum merged segment duration"),
normalizeMaxSegmentTokens: fs.Int("normalize-max-segment-tokens", cfg.Normalization.MaxSegmentTokens, "Maximum merged segment token estimate"),
workDir: fs.String("work-dir", cfg.WorkDir, "Per-run work directory"),
workDirRetention: fs.String("work-dir-retention", string(cfg.WorkDirRetention), "Work-dir retention policy: auto|always|never"),
}
return fs, pFlags
}
func isHelpCommand(args []string) bool {
@@ -239,22 +302,16 @@ func writeRootUsage(w io.Writer) {
fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json")
}
func writeProcessUsage(w io.Writer) {
func writeProcessUsage(w io.Writer, fs *flag.FlagSet) {
fmt.Fprintln(w, "Process a transcript JSON file.")
fmt.Fprintln(w)
fmt.Fprintln(w, "Usage:")
fmt.Fprintln(w, " audita process <transcript.json> [flags]")
fmt.Fprintln(w)
fmt.Fprintln(w, "Flags:")
fmt.Fprintln(w, " --glossary <path> Path to glossary YAML file")
fmt.Fprintln(w, " --output <path> Path to corrected transcript JSON output file")
fmt.Fprintln(w, " --report-json <path> Path to machine-readable report JSON output file")
fmt.Fprintln(w, " --modules <list> Comma-separated module sequence override")
fmt.Fprintln(w, " --llm-api-key <key> Primary LLM API key")
fmt.Fprintln(w, " --model <name> Primary LLM model")
fmt.Fprintln(w, " --base-url <url> Primary OpenAI-compatible API base URL")
fmt.Fprintln(w, " --work-dir <path> Per-run work directory")
fmt.Fprintln(w, " --work-dir-retention <mode> auto|always|never")
fs.VisitAll(func(f *flag.Flag) {
fmt.Fprintf(w, " --%s\n", f.Name)
})
fmt.Fprintln(w)
fmt.Fprintln(w, "Example:")
fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json")

View File

@@ -2,6 +2,7 @@ package cli
import (
"bytes"
"errors"
"strings"
"testing"
)
@@ -25,7 +26,7 @@ func TestRunRootHelp(t *testing.T) {
}
}
func TestRunProcessHelp(t *testing.T) {
func TestRunProcessHelpListsExpectedFlags(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
@@ -33,12 +34,43 @@ func TestRunProcessHelp(t *testing.T) {
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "audita process <transcript.json> [flags]") {
t.Fatalf("expected process usage in stdout, got %q", stdout.String())
}
if !strings.Contains(stdout.String(), "--glossary") {
t.Fatalf("expected glossary flag in process help, got %q", stdout.String())
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())
}
@@ -60,13 +92,61 @@ func TestRunUnknownCommand(t *testing.T) {
}
}
func TestRunProcessNotImplemented(t *testing.T) {
func TestRunProcessMissingTranscriptPath(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{"process", "--glossary", "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", "transcript.json"}, &stdout, &stderr)
if exitCode == 0 {
t.Fatalf("expected nonzero exit code for not-implemented process")
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 TestRunProcessInvalidCLIConfig(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{"process", "transcript.json", "--glossary", "glossary.yaml", "--work-dir-retention", "invalid"}, &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(), "invalid CLI configuration") {
t.Fatalf("expected invalid config error, got %q", stderr.String())
}
}
func TestRunProcessNotImplemented(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{"process", "transcript.json", "--glossary", "glossary.yaml"}, &stdout, &stderr)
if exitCode != 1 {
t.Fatalf("expected exit code 1 for not-implemented process, got %d", exitCode)
}
if stdout.Len() != 0 {
t.Fatalf("expected empty stdout, got %q", stdout.String())
@@ -75,3 +155,45 @@ func TestRunProcessNotImplemented(t *testing.T) {
t.Fatalf("expected not-implemented message in stderr, got %q", stderr.String())
}
}
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")
var captured processInvocation
originalRunner := processRunner
processRunner = func(inv processInvocation) error {
captured = inv
return errors.New(processNotImplementedMessage)
}
t.Cleanup(func() {
processRunner = originalRunner
})
exitCode := Run([]string{
"process",
"transcript.json",
"--glossary",
"glossary.yaml",
"--model",
"cli-model",
"--validation-llm-concurrency",
"5",
}, &stdout, &stderr)
if exitCode != 1 {
t.Fatalf("expected exit code 1 for not implemented, 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 != "glossary.yaml" {
t.Fatalf("unexpected glossary path: %q", captured.GlossaryPath)
}
}