Add versioned Audita config support
This commit is contained in:
280
internal/core/config/file_config_test.go
Normal file
280
internal/core/config/file_config_test.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseFileConfigYAMLValid(t *testing.T) {
|
||||
raw := `
|
||||
version: 1
|
||||
pipeline:
|
||||
modules: [glossary, homophones, grammar]
|
||||
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.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:
|
||||
schema: v1
|
||||
`
|
||||
_, err := ParseFileConfigYAML([]byte(raw))
|
||||
if err == nil {
|
||||
t.Fatalf("expected unknown field error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "field output 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]
|
||||
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.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
|
||||
}
|
||||
Reference in New Issue
Block a user