291 lines
8.0 KiB
Go
291 lines
8.0 KiB
Go
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
|
|
}
|