package config import ( "reflect" "strings" "testing" ) func TestDefaultConfigValues(t *testing.T) { cfg := Default() if got, want := strings.Join(cfg.Modules, ","), DefaultModulesCSV; got != want { t.Fatalf("modules mismatch: got %q want %q", got, want) } if cfg.PrimaryLLM.Model != DefaultPrimaryModel { t.Fatalf("unexpected default primary model: %q", cfg.PrimaryLLM.Model) } if cfg.PrimaryLLM.BaseURL != DefaultPrimaryBaseURL { t.Fatalf("unexpected default primary base url: %q", cfg.PrimaryLLM.BaseURL) } if cfg.PrimaryLLM.TimeoutSeconds != DefaultPrimaryLLMTimeoutSeconds { t.Fatalf("unexpected default timeout seconds: %d", cfg.PrimaryLLM.TimeoutSeconds) } if cfg.PrimaryLLM.MaxRetries != DefaultMaxRetries { t.Fatalf("unexpected default max retries: %d", cfg.PrimaryLLM.MaxRetries) } if cfg.TotalLLMConcurrency != DefaultLLMConcurrency { t.Fatalf("unexpected default total llm concurrency: %d", cfg.TotalLLMConcurrency) } if cfg.ProposalLLMConcurrency != DefaultLLMConcurrency { t.Fatalf("unexpected default proposal llm concurrency: %d", cfg.ProposalLLMConcurrency) } if cfg.ValidationLLMConcurrency != nil { t.Fatalf("expected validation llm concurrency to be unset by default") } if cfg.PrimaryLLM.Concurrency != cfg.TotalLLMConcurrency { t.Fatalf("expected primary llm concurrency alias to mirror total, got primary=%d total=%d", cfg.PrimaryLLM.Concurrency, cfg.TotalLLMConcurrency) } if cfg.ValidationLLM.TimeoutSeconds != nil { t.Fatalf("expected validation timeout to be unset by default") } if cfg.ValidationLLM.MaxRetries != nil { t.Fatalf("expected validation max retries to be unset by default") } if cfg.ValidationLLM.Concurrency != nil { t.Fatalf("expected legacy validation llm concurrency alias to be unset by default") } if cfg.TargetSections != nil { t.Fatalf("expected target sections to be unset by default") } if cfg.WorkDir != DefaultWorkDir { t.Fatalf("unexpected default work dir: %q", cfg.WorkDir) } if cfg.TranscriptDescription != "" { t.Fatalf("expected default transcript description to be empty, got %q", cfg.TranscriptDescription) } if cfg.WorkDirRetention != DefaultWorkDirRetention { t.Fatalf("unexpected default work dir retention: %q", cfg.WorkDirRetention) } if err := cfg.Validate(); err != nil { t.Fatalf("default config should validate: %v", err) } } func TestLoadFromEnvOverridesAndFallback(t *testing.T) { env := map[string]string{ "AUDITA_MODEL": "openai/gpt-4.1-mini", "AUDITA_BASE_URL": "https://api.openai.com/v1", "AUDITA_LLM_TIMEOUT_SECONDS": "120", "AUDITA_MAX_RETRIES": "7", "AUDITA_TOTAL_LLM_CONCURRENCY": "6", "AUDITA_PROPOSAL_LLM_CONCURRENCY": "4", "AUDITA_VALIDATION_LLM_CONCURRENCY": "2", "AUDITA_VALIDATION_MAX_PROMPT_TOKENS": "4096", "AUDITA_MAX_SECTION_TOKENS": "9000", "AUDITA_MIN_SECTION_TOKENS": "3000", "AUDITA_TARGET_SECTIONS": "5", "AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD": "0.9", "AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD": "0.7", "AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD": "0.6", "AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD": "0.5", "AUDITA_NORMALIZE_MAX_SEGMENT_GAP": "2.5", "AUDITA_NORMALIZE_ELLIPSIS_GAP": "2.0", "AUDITA_NORMALIZE_MAX_SEGMENT_DURATION": "30.0", "AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS": "1024", "AUDITA_WORK_DIR": "/var/tmp/audita", "AUDITA_WORK_DIR_RETENTION": "always", "OPENROUTER_API_KEY": "fallback-key", } cfg, err := loadFromLookup(mapLookup(env)) if err != nil { t.Fatalf("loadFromLookup returned error: %v", err) } if cfg.PrimaryLLM.APIKey != "fallback-key" { t.Fatalf("expected OPENROUTER_API_KEY fallback, got %q", cfg.PrimaryLLM.APIKey) } if cfg.PrimaryLLM.Model != env["AUDITA_MODEL"] { t.Fatalf("unexpected model: %q", cfg.PrimaryLLM.Model) } if cfg.PrimaryLLM.BaseURL != env["AUDITA_BASE_URL"] { t.Fatalf("unexpected base url: %q", cfg.PrimaryLLM.BaseURL) } if cfg.TargetSections == nil || *cfg.TargetSections != 5 { t.Fatalf("unexpected target sections: %#v", cfg.TargetSections) } if cfg.TotalLLMConcurrency != 6 { t.Fatalf("unexpected total llm concurrency: %d", cfg.TotalLLMConcurrency) } if cfg.ProposalLLMConcurrency != 4 { t.Fatalf("unexpected proposal llm concurrency: %d", cfg.ProposalLLMConcurrency) } if cfg.ValidationLLMConcurrency == nil || *cfg.ValidationLLMConcurrency != 2 { t.Fatalf("unexpected validation llm concurrency: %#v", cfg.ValidationLLMConcurrency) } if cfg.PrimaryLLM.Concurrency != 6 { t.Fatalf("expected primary alias concurrency 6, got %d", cfg.PrimaryLLM.Concurrency) } if cfg.ValidationLLM.Concurrency == nil || *cfg.ValidationLLM.Concurrency != 2 { t.Fatalf("expected validation alias concurrency 2, got %#v", cfg.ValidationLLM.Concurrency) } if cfg.WorkDirRetention != WorkDirRetentionAlways { t.Fatalf("unexpected work dir retention: %q", cfg.WorkDirRetention) } } func TestLoadFromEnvLegacyLLMConcurrencyAliasForTotalAndProposal(t *testing.T) { env := map[string]string{ "AUDITA_LLM_CONCURRENCY": "5", } cfg, err := loadFromLookup(mapLookup(env)) if err != nil { t.Fatalf("loadFromLookup returned error: %v", err) } if cfg.TotalLLMConcurrency != 5 { t.Fatalf("expected total concurrency from legacy alias, got %d", cfg.TotalLLMConcurrency) } if cfg.ProposalLLMConcurrency != 5 { t.Fatalf("expected proposal concurrency to inherit legacy total, got %d", cfg.ProposalLLMConcurrency) } } func TestLoadFromEnvCanonicalTotalWinsLegacyAlias(t *testing.T) { env := map[string]string{ "AUDITA_TOTAL_LLM_CONCURRENCY": "4", "AUDITA_LLM_CONCURRENCY": "9", } cfg, err := loadFromLookup(mapLookup(env)) if err != nil { t.Fatalf("loadFromLookup returned error: %v", err) } if cfg.TotalLLMConcurrency != 4 { t.Fatalf("expected canonical total to win over legacy alias, got %d", cfg.TotalLLMConcurrency) } if cfg.ProposalLLMConcurrency != 4 { t.Fatalf("expected proposal to inherit canonical total when unset, got %d", cfg.ProposalLLMConcurrency) } } func TestLoadFromEnvUsesAuditaLLMAPIKeyOverFallback(t *testing.T) { env := map[string]string{ "AUDITA_LLM_API_KEY": "primary-key", "OPENROUTER_API_KEY": "fallback-key", } cfg, err := loadFromLookup(mapLookup(env)) if err != nil { t.Fatalf("loadFromLookup returned error: %v", err) } if cfg.PrimaryLLM.APIKey != "primary-key" { t.Fatalf("expected AUDITA_LLM_API_KEY to win, got %q", cfg.PrimaryLLM.APIKey) } } func TestApplyCLIOverridesPrecedence(t *testing.T) { cfg := Default() cfg.PrimaryLLM.Model = "env-model" cfg.WorkDir = "/env/work" model := "cli-model" workDir := "/cli/work" modules := "grammar" totalLLMConcurrency := 5 proposalLLMConcurrency := 3 overrides := CLIOverrides{ PrimaryModel: &model, WorkDir: &workDir, ModulesCSV: &modules, TotalLLMConcurrency: &totalLLMConcurrency, ProposalLLMConcurrency: &proposalLLMConcurrency, } if err := cfg.ApplyCLIOverrides(overrides); err != nil { t.Fatalf("ApplyCLIOverrides failed: %v", err) } if cfg.PrimaryLLM.Model != "cli-model" { t.Fatalf("expected CLI model override, got %q", cfg.PrimaryLLM.Model) } if cfg.WorkDir != "/cli/work" { t.Fatalf("expected CLI work dir override, got %q", cfg.WorkDir) } if !reflect.DeepEqual(cfg.Modules, []string{"grammar"}) { t.Fatalf("unexpected modules: %#v", cfg.Modules) } if cfg.TotalLLMConcurrency != 5 { t.Fatalf("expected CLI total concurrency override, got %d", cfg.TotalLLMConcurrency) } if cfg.ProposalLLMConcurrency != 3 { t.Fatalf("expected CLI proposal concurrency override, got %d", cfg.ProposalLLMConcurrency) } } func TestApplyCLIOverridesTrimsTranscriptDescription(t *testing.T) { cfg := Default() description := " background context about speakers " if err := cfg.ApplyCLIOverrides(CLIOverrides{TranscriptDescription: &description}); err != nil { t.Fatalf("ApplyCLIOverrides failed: %v", err) } if cfg.TranscriptDescription != "background context about speakers" { t.Fatalf("unexpected transcript description trim result: %q", cfg.TranscriptDescription) } } func TestValidationRejectsOverlyLongTranscriptDescription(t *testing.T) { cfg := Default() cfg.TranscriptDescription = strings.Repeat("a", DefaultTranscriptDescriptionMaxChars+1) err := cfg.Validate() if err == nil { t.Fatalf("expected transcript description length validation error") } if !strings.Contains(err.Error(), "transcript description must be 500 characters or fewer") { t.Fatalf("unexpected validation error: %v", err) } } func TestApplyCLIOverridesLegacyLLMConcurrencyAlias(t *testing.T) { cfg := Default() aliasConcurrency := 6 if err := cfg.ApplyCLIOverrides(CLIOverrides{PrimaryLLMConcurrency: &aliasConcurrency}); err != nil { t.Fatalf("ApplyCLIOverrides failed: %v", err) } if cfg.TotalLLMConcurrency != 6 { t.Fatalf("expected legacy --llm-concurrency alias to set total, got %d", cfg.TotalLLMConcurrency) } if cfg.ProposalLLMConcurrency != 6 { t.Fatalf("expected proposal to inherit aliased total when unset, got %d", cfg.ProposalLLMConcurrency) } } func TestApplyCLIOverridesCanonicalTotalWinsLegacyAlias(t *testing.T) { cfg := Default() canonicalTotal := 4 legacyAlias := 9 if err := cfg.ApplyCLIOverrides(CLIOverrides{TotalLLMConcurrency: &canonicalTotal, PrimaryLLMConcurrency: &legacyAlias}); err != nil { t.Fatalf("ApplyCLIOverrides failed: %v", err) } if cfg.TotalLLMConcurrency != 4 { t.Fatalf("expected canonical total concurrency to win, got %d", cfg.TotalLLMConcurrency) } if cfg.ProposalLLMConcurrency != 4 { t.Fatalf("expected proposal to inherit canonical total when proposal is unset, got %d", cfg.ProposalLLMConcurrency) } } func TestValidationFailures(t *testing.T) { cfg := Default() cfg.PrimaryLLM.TimeoutSeconds = -1 cfg.TotalLLMConcurrency = 0 cfg.ProposalLLMConcurrency = 0 validationConcurrency := 5 cfg.ValidationLLMConcurrency = &validationConcurrency cfg.ValidationMaxPromptTokens = 0 cfg.MaxSectionTokens = 100 cfg.MinSectionTokens = 200 cfg.Thresholds.Grammar = 1.5 cfg.WorkDirRetention = WorkDirRetention("sometimes") err := cfg.Validate() if err == nil { t.Fatalf("expected validation error") } message := err.Error() for _, expected := range []string{ "primary llm timeout seconds", "total llm concurrency", "proposal llm concurrency", "validation llm concurrency must be less than or equal to total llm concurrency", "validation max prompt tokens", "min section tokens", "grammar confidence threshold", "work dir retention", } { if !strings.Contains(message, expected) { t.Fatalf("expected error to contain %q, got %q", expected, message) } } } func TestEffectiveValidationLLMInheritance(t *testing.T) { cfg := Default() cfg.PrimaryLLM.APIKey = "primary-key" cfg.PrimaryLLM.Model = "primary-model" cfg.PrimaryLLM.BaseURL = "https://primary.example/v1" cfg.PrimaryLLM.TimeoutSeconds = 111 cfg.PrimaryLLM.MaxRetries = 2 cfg.TotalLLMConcurrency = 7 cfg.syncLegacyConcurrencyAliases() effective := cfg.EffectiveValidationLLMConfig() if effective.APIKey != "primary-key" || effective.Model != "primary-model" || effective.BaseURL != "https://primary.example/v1" || effective.TimeoutSeconds != 111 || effective.MaxRetries != 2 || effective.Concurrency != 7 { t.Fatalf("unexpected inherited config: %#v", effective) } validationTimeout := 222 validationRetries := 9 cfg.ValidationLLM.APIKey = "validation-key" cfg.ValidationLLM.Model = "validation-model" cfg.ValidationLLM.BaseURL = "https://validation.example/v1" cfg.ValidationLLM.TimeoutSeconds = &validationTimeout cfg.ValidationLLM.MaxRetries = &validationRetries validationConcurrency := 4 cfg.ValidationLLMConcurrency = &validationConcurrency cfg.syncLegacyConcurrencyAliases() effective = cfg.EffectiveValidationLLMConfig() if effective.APIKey != "validation-key" || effective.Model != "validation-model" || effective.BaseURL != "https://validation.example/v1" || effective.TimeoutSeconds != 222 || effective.MaxRetries != 9 || effective.Concurrency != 4 { t.Fatalf("unexpected overridden validation config: %#v", effective) } } func TestValidationLLMConcurrencyCannotExceedTotal(t *testing.T) { cfg := Default() cfg.TotalLLMConcurrency = 2 cfg.ProposalLLMConcurrency = 2 validationConcurrency := 3 cfg.ValidationLLMConcurrency = &validationConcurrency if err := cfg.Validate(); err == nil { t.Fatal("expected validation error when validation llm concurrency exceeds total") } validationConcurrency = 2 cfg.ValidationLLMConcurrency = &validationConcurrency if err := cfg.Validate(); err != nil { t.Fatalf("expected equal concurrency to validate, got %v", err) } } func TestProposalLLMConcurrencyCannotExceedTotal(t *testing.T) { cfg := Default() cfg.TotalLLMConcurrency = 2 cfg.ProposalLLMConcurrency = 3 if err := cfg.Validate(); err == nil { t.Fatal("expected validation error when proposal llm concurrency exceeds total") } cfg.ProposalLLMConcurrency = 2 if err := cfg.Validate(); err != nil { t.Fatalf("expected equal concurrency to validate, got %v", err) } } func TestCLITotalLLMConcurrencyOverrideDrivesEffectiveValidationConcurrencyWhenValidationUnset(t *testing.T) { cfg := Default() totalLLMConcurrency := 6 if err := cfg.ApplyCLIOverrides(CLIOverrides{TotalLLMConcurrency: &totalLLMConcurrency}); err != nil { t.Fatalf("ApplyCLIOverrides failed: %v", err) } if cfg.ValidationLLMConcurrency != nil { t.Fatalf("expected validation concurrency to remain unset, got %#v", cfg.ValidationLLMConcurrency) } if cfg.EffectiveValidationLLMConcurrency() != 6 { t.Fatalf("expected inherited validation concurrency 6, got %d", cfg.EffectiveValidationLLMConcurrency()) } } func TestRedactedConfig(t *testing.T) { cfg := Default() cfg.PrimaryLLM.APIKey = "secret-primary" cfg.ValidationLLM.APIKey = "secret-validation" redacted := cfg.Redacted() if redacted.PrimaryLLM.APIKey != redactedSecret { t.Fatalf("expected primary api key to be redacted, got %q", redacted.PrimaryLLM.APIKey) } if redacted.ValidationLLM.APIKey != redactedSecret { t.Fatalf("expected validation api key to be redacted, got %q", redacted.ValidationLLM.APIKey) } if cfg.PrimaryLLM.APIKey != "secret-primary" { t.Fatalf("redaction should not mutate original config") } } func mapLookup(values map[string]string) func(string) (string, bool) { return func(key string) (string, bool) { value, ok := values[key] return value, ok } }