package config import ( "os" "strings" "testing" ) func TestParseFileConfigYAMLValid(t *testing.T) { raw := ` version: 1 pipeline: modules: [glossary, homophones, grammar] output: schema: audita-v1 llm: proposal: base_url: https://example.test/v1 model: provider/model-a api_key_env: AUDITA_PROPOSAL_KEY timeout: 2m max_retries: 4 validation: base_url: https://example.test/validation model: provider/model-b api_key_env: AUDITA_VALIDATION_KEY timeout: 45 max_retries: 3 concurrency: total_llm: 8 proposal_llm: 4 validation_llm: 2 chunking: target_sections: 6 max_section_tokens: 9000 min_section_tokens: 3000 normalization: max_segment_gap: 1.5s ellipsis_gap: 2 max_segment_duration: 45s max_segment_tokens: 1500 thresholds: glossary: 0.9 homophones: 0.7 spoken_word: 0.8 grammar: 0.75 context: description: " crowd scene with many proper nouns " diagnostics: work_dir: /tmp/audita-config retention: always ` cfg, err := ParseFileConfigYAML([]byte(raw)) if err != nil { t.Fatalf("ParseFileConfigYAML error: %v", err) } if cfg.Version != 1 { t.Fatalf("expected version 1, got %d", cfg.Version) } if cfg.Pipeline == nil || len(cfg.Pipeline.Modules) != 3 { t.Fatalf("unexpected pipeline modules: %#v", cfg.Pipeline) } if cfg.Output == nil || cfg.Output.Schema == nil || *cfg.Output.Schema != "audita-v1" { t.Fatalf("expected output schema audita-v1, got %#v", cfg.Output) } if cfg.LLM == nil || cfg.LLM.Proposal == nil || cfg.LLM.Validation == nil { t.Fatalf("expected llm proposal+validation blocks") } if cfg.LLM.Proposal.Timeout == nil || cfg.LLM.Proposal.Timeout.Seconds() != 120 { t.Fatalf("expected proposal timeout 120s, got %#v", cfg.LLM.Proposal.Timeout) } if cfg.LLM.Validation.Timeout == nil || cfg.LLM.Validation.Timeout.Seconds() != 45 { t.Fatalf("expected validation timeout 45s, got %#v", cfg.LLM.Validation.Timeout) } if cfg.Normalization == nil || cfg.Normalization.MaxSegmentGap == nil || cfg.Normalization.MaxSegmentGap.Seconds() != 1.5 { t.Fatalf("expected parsed duration for normalization max_segment_gap") } } func TestParseFileConfigYAMLRejectsUnknownField(t *testing.T) { raw := ` version: 1 pipeline: modules: [grammar] output: unknown: v1 ` _, err := ParseFileConfigYAML([]byte(raw)) if err == nil { t.Fatalf("expected unknown field error") } if !strings.Contains(err.Error(), "field unknown not found") { t.Fatalf("unexpected error: %v", err) } } func TestParseFileConfigYAMLRejectsMissingVersion(t *testing.T) { raw := `pipeline: {modules: [grammar]}` _, err := ParseFileConfigYAML([]byte(raw)) if err == nil { t.Fatalf("expected missing version error") } if !strings.Contains(err.Error(), "config version is required") { t.Fatalf("unexpected error: %v", err) } } func TestParseFileConfigYAMLRejectsUnsupportedVersion(t *testing.T) { raw := `version: 2` _, err := ParseFileConfigYAML([]byte(raw)) if err == nil { t.Fatalf("expected unsupported version error") } if !strings.Contains(err.Error(), "unsupported config version 2") { t.Fatalf("unexpected error: %v", err) } } func TestApplyFileConfigParsesAndMergesFields(t *testing.T) { raw := ` version: 1 pipeline: modules: [spoken_word, grammar] output: schema: audita-v1 llm: proposal: model: provider/new-proposal api_key_env: PROPOSAL_KEY_NAME timeout: 90s max_retries: 5 validation: model: provider/new-validation api_key_env: VALIDATION_KEY_NAME timeout: 150 max_retries: 6 concurrency: total_llm: 7 proposal_llm: 3 validation_llm: 2 chunking: target_sections: 9 thresholds: glossary: 0.91 homophones: 0.61 spoken_word: 0.71 grammar: 0.81 diagnostics: retention: never ` fileCfg, err := ParseFileConfigYAML([]byte(raw)) if err != nil { t.Fatalf("ParseFileConfigYAML error: %v", err) } cfg := Default() lookup := func(name string) (string, bool) { switch name { case "PROPOSAL_KEY_NAME": return "proposal-secret", true case "VALIDATION_KEY_NAME": return "validation-secret", true default: return "", false } } if err := cfg.applyFileConfigWithLookup(fileCfg, lookup); err != nil { t.Fatalf("applyFileConfigWithLookup error: %v", err) } if strings.Join(cfg.Modules, ",") != "spoken_word,grammar" { t.Fatalf("unexpected modules: %#v", cfg.Modules) } if cfg.OutputSchema != "audita-v1" { t.Fatalf("unexpected output schema: %q", cfg.OutputSchema) } if cfg.PrimaryLLM.Model != "provider/new-proposal" { t.Fatalf("unexpected proposal model: %q", cfg.PrimaryLLM.Model) } if cfg.PrimaryLLM.APIKey != "proposal-secret" { t.Fatalf("expected proposal key from api_key_env lookup, got %q", cfg.PrimaryLLM.APIKey) } if cfg.PrimaryLLM.TimeoutSeconds != 90 { t.Fatalf("unexpected proposal timeout: %d", cfg.PrimaryLLM.TimeoutSeconds) } if cfg.ValidationLLM.Model != "provider/new-validation" { t.Fatalf("unexpected validation model: %q", cfg.ValidationLLM.Model) } if cfg.ValidationLLM.APIKey != "validation-secret" { t.Fatalf("expected validation key from api_key_env lookup, got %q", cfg.ValidationLLM.APIKey) } if cfg.ValidationLLM.TimeoutSeconds == nil || *cfg.ValidationLLM.TimeoutSeconds != 150 { t.Fatalf("unexpected validation timeout: %#v", cfg.ValidationLLM.TimeoutSeconds) } if cfg.TotalLLMConcurrency != 7 || cfg.ProposalLLMConcurrency != 3 { t.Fatalf("unexpected llm concurrency values: total=%d proposal=%d", cfg.TotalLLMConcurrency, cfg.ProposalLLMConcurrency) } if cfg.ValidationLLMConcurrency == nil || *cfg.ValidationLLMConcurrency != 2 { t.Fatalf("unexpected validation llm concurrency: %#v", cfg.ValidationLLMConcurrency) } if cfg.TargetSections == nil || *cfg.TargetSections != 9 { t.Fatalf("unexpected target sections: %#v", cfg.TargetSections) } if cfg.WorkDirRetention != WorkDirRetentionNever { t.Fatalf("unexpected retention: %q", cfg.WorkDirRetention) } if cfg.PrimaryLLM.Concurrency != 7 { t.Fatalf("expected legacy alias to sync, got %d", cfg.PrimaryLLM.Concurrency) } if cfg.ValidationLLM.Concurrency == nil || *cfg.ValidationLLM.Concurrency != 2 { t.Fatalf("expected validation alias to sync, got %#v", cfg.ValidationLLM.Concurrency) } } func TestApplyFileConfigContextDescriptionTrim(t *testing.T) { raw := ` version: 1 context: description: " scene context " ` fileCfg, err := ParseFileConfigYAML([]byte(raw)) if err != nil { t.Fatalf("ParseFileConfigYAML error: %v", err) } cfg := Default() if err := cfg.applyFileConfigWithLookup(fileCfg, mapLookup(map[string]string{})); err != nil { t.Fatalf("applyFileConfigWithLookup error: %v", err) } if cfg.TranscriptDescription != "scene context" { t.Fatalf("unexpected transcript description: %q", cfg.TranscriptDescription) } } func TestApplyFileConfigRejectsInvalidAPIKeyEnvName(t *testing.T) { raw := ` version: 1 llm: proposal: api_key_env: "not a var name" ` fileCfg, err := ParseFileConfigYAML([]byte(raw)) if err != nil { t.Fatalf("ParseFileConfigYAML error: %v", err) } cfg := Default() err = cfg.applyFileConfigWithLookup(fileCfg, mapLookup(map[string]string{})) if err == nil { t.Fatalf("expected api_key_env validation error") } if !strings.Contains(err.Error(), "environment variable name") { t.Fatalf("unexpected error: %v", err) } } func TestParseFileConfigDurationParsingErrors(t *testing.T) { raw := ` version: 1 llm: proposal: timeout: "1.5s" ` _, err := ParseFileConfigYAML([]byte(raw)) if err == nil { t.Fatalf("expected duration parse error") } if !strings.Contains(err.Error(), "whole seconds") { t.Fatalf("unexpected error: %v", err) } } func TestLoadFileConfigReadsFromPath(t *testing.T) { p := writeTempFileConfig(t, "version: 1\n") cfg, err := LoadFileConfig(p) if err != nil { t.Fatalf("LoadFileConfig error: %v", err) } if cfg.Version != 1 { t.Fatalf("expected version 1, got %d", cfg.Version) } } func writeTempFileConfig(t *testing.T, contents string) string { t.Helper() dir := t.TempDir() path := dir + "/config.yaml" if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { t.Fatalf("write config file: %v", err) } return path }