package config import ( "os" "path/filepath" "strings" "testing" ) func TestOmittingNormalizeSpeakersDoesNotRequireSpeakers(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") _, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: "validate-raw", PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("expected speakers file to be optional when normalize-speakers is omitted, got %v", err) } } func TestDuplicateInputFilesFailValidation(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") _, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input, input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err == nil { t.Fatal("expected duplicate input error") } if !strings.Contains(err.Error(), "duplicate --input-file") { t.Fatalf("unexpected error: %v", err) } } func TestOutputSchemaDefaultsToIntermediate(t *testing.T) { t.Setenv(OutputSchemaEnv, "") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.OutputSchema != DefaultOutputSchema { t.Fatalf("output schema = %q, want %q", cfg.OutputSchema, DefaultOutputSchema) } } func TestOutputSchemaAcceptsIntermediate(t *testing.T) { t.Setenv(OutputSchemaEnv, "") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, OutputSchema: OutputSchemaIntermediate, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.OutputSchema != OutputSchemaIntermediate { t.Fatalf("output schema = %q, want %q", cfg.OutputSchema, OutputSchemaIntermediate) } } func TestOutputSchemaAcceptsMinimal(t *testing.T) { t.Setenv(OutputSchemaEnv, "") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, OutputSchema: OutputSchemaMinimal, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.OutputSchema != OutputSchemaMinimal { t.Fatalf("output schema = %q, want %q", cfg.OutputSchema, OutputSchemaMinimal) } } func TestOutputSchemaAcceptsFull(t *testing.T) { t.Setenv(OutputSchemaEnv, "") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, OutputSchema: OutputSchemaFull, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.OutputSchema != OutputSchemaFull { t.Fatalf("output schema = %q, want %q", cfg.OutputSchema, OutputSchemaFull) } } func TestOutputSchemaUsesEnvWhenFlagOmitted(t *testing.T) { t.Setenv(OutputSchemaEnv, OutputSchemaFull) dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.OutputSchema != OutputSchemaFull { t.Fatalf("output schema = %q, want %q", cfg.OutputSchema, OutputSchemaFull) } } func TestOutputSchemaFlagOverridesEnv(t *testing.T) { t.Setenv(OutputSchemaEnv, OutputSchemaFull) dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, OutputSchema: OutputSchemaMinimal, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.OutputSchema != OutputSchemaMinimal { t.Fatalf("output schema = %q, want %q", cfg.OutputSchema, OutputSchemaMinimal) } } func TestOutputSchemaRejectsInvalidEnvValue(t *testing.T) { t.Setenv(OutputSchemaEnv, "compact") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") _, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err == nil { t.Fatal("expected output schema error") } if !strings.Contains(err.Error(), "--output-schema must be one of") { t.Fatalf("unexpected error: %v", err) } } func TestOutputSchemaRejectsUnknownValue(t *testing.T) { t.Setenv(OutputSchemaEnv, "") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") _, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, OutputSchema: "compact", PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err == nil { t.Fatal("expected output schema error") } if !strings.Contains(err.Error(), "--output-schema must be one of") { t.Fatalf("unexpected error: %v", err) } } func TestOutputSchemaRejectsLegacyValues(t *testing.T) { tests := []string{"default", "minimal", "seriatim"} for _, legacy := range tests { t.Run(legacy, func(t *testing.T) { t.Setenv(OutputSchemaEnv, "") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") _, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, OutputSchema: legacy, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err == nil { t.Fatal("expected output schema error") } if !strings.Contains(err.Error(), "--output-schema must be one of") { t.Fatalf("unexpected error: %v", err) } }) } } func TestOverlapWordRunGapDefaultsTo1(t *testing.T) { t.Setenv(OverlapWordRunGapEnv, "") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.OverlapWordRunGap != DefaultOverlapWordRunGap { t.Fatalf("gap = %f, want %f", cfg.OverlapWordRunGap, DefaultOverlapWordRunGap) } } func TestOverlapWordRunGapUsesValidEnvOverride(t *testing.T) { t.Setenv(OverlapWordRunGapEnv, "1.25") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.OverlapWordRunGap != 1.25 { t.Fatalf("gap = %f, want 1.25", cfg.OverlapWordRunGap) } } func TestOverlapWordRunGapRejectsInvalidEnvOverride(t *testing.T) { tests := []struct { name string value string want string }{ {name: "non-numeric", value: "fast", want: "must be a positive number"}, {name: "zero", value: "0", want: "must be positive"}, {name: "negative", value: "-0.1", want: "must be positive"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Setenv(OverlapWordRunGapEnv, test.value) dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") _, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), test.want) { t.Fatalf("expected error to contain %q, got %v", test.want, err) } }) } } func TestWordRunReorderWindowDefaultsTo1(t *testing.T) { t.Setenv(WordRunReorderWindowEnv, "") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.WordRunReorderWindow != DefaultWordRunReorderWindow { t.Fatalf("window = %f, want %f", cfg.WordRunReorderWindow, DefaultWordRunReorderWindow) } } func TestWordRunReorderWindowUsesValidEnvOverride(t *testing.T) { t.Setenv(WordRunReorderWindowEnv, "0.2") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.WordRunReorderWindow != 0.2 { t.Fatalf("window = %f, want 0.2", cfg.WordRunReorderWindow) } } func TestWordRunReorderWindowRejectsInvalidEnvOverride(t *testing.T) { tests := []struct { name string value string want string }{ {name: "non-numeric", value: "fast", want: "must be a positive number"}, {name: "zero", value: "0", want: "must be positive"}, {name: "negative", value: "-0.1", want: "must be positive"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Setenv(WordRunReorderWindowEnv, test.value) dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") _, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), test.want) { t.Fatalf("expected error to contain %q, got %v", test.want, err) } }) } } func TestBackchannelMaxDurationDefaultsTo2(t *testing.T) { t.Setenv(BackchannelMaxDurationEnv, "") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.BackchannelMaxDuration != DefaultBackchannelMaxDuration { t.Fatalf("backchannel max duration = %f, want %f", cfg.BackchannelMaxDuration, DefaultBackchannelMaxDuration) } } func TestBackchannelMaxDurationUsesValidEnvOverride(t *testing.T) { t.Setenv(BackchannelMaxDurationEnv, "1.5") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.BackchannelMaxDuration != 1.5 { t.Fatalf("backchannel max duration = %f, want 1.5", cfg.BackchannelMaxDuration) } } func TestBackchannelMaxDurationRejectsInvalidEnvOverride(t *testing.T) { assertPositiveFloatEnvValidation(t, BackchannelMaxDurationEnv) } func TestFillerMaxDurationDefaultsTo125(t *testing.T) { t.Setenv(FillerMaxDurationEnv, "") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.FillerMaxDuration != DefaultFillerMaxDuration { t.Fatalf("filler max duration = %f, want %f", cfg.FillerMaxDuration, DefaultFillerMaxDuration) } } func TestFillerMaxDurationUsesValidEnvOverride(t *testing.T) { t.Setenv(FillerMaxDurationEnv, "1.75") dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.FillerMaxDuration != 1.75 { t.Fatalf("filler max duration = %f, want 1.75", cfg.FillerMaxDuration) } } func TestFillerMaxDurationRejectsInvalidEnvOverride(t *testing.T) { assertPositiveFloatEnvValidation(t, FillerMaxDurationEnv) } func TestCoalesceGapDefaultsTo3(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") cfg, err := NewMergeConfig(MergeOptions{ InputFiles: []string{input}, OutputFile: output, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, }) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.CoalesceGap != DefaultCoalesceGap { t.Fatalf("coalesce gap = %f, want %f", cfg.CoalesceGap, DefaultCoalesceGap) } } func TestCoalesceGapUsesValidOverride(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") opts := validMergeOptions(input, output) opts.CoalesceGap = "1.5" cfg, err := NewMergeConfig(opts) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.CoalesceGap != 1.5 { t.Fatalf("coalesce gap = %f, want 1.5", cfg.CoalesceGap) } } func TestCoalesceGapAllowsZero(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") opts := validMergeOptions(input, output) opts.CoalesceGap = "0" cfg, err := NewMergeConfig(opts) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.CoalesceGap != 0 { t.Fatalf("coalesce gap = %f, want 0", cfg.CoalesceGap) } } func TestCoalesceGapRejectsInvalidOverride(t *testing.T) { tests := []struct { name string value string want string }{ {name: "non-numeric", value: "fast", want: "must be a non-negative number"}, {name: "negative", value: "-0.1", want: "must be non-negative"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") opts := validMergeOptions(input, output) opts.CoalesceGap = test.value _, err := NewMergeConfig(opts) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), test.want) { t.Fatalf("expected error to contain %q, got %v", test.want, err) } }) } } func TestNewTrimConfigRequiresInputAndOutput(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "trimmed.json") _, err := NewTrimConfig(TrimOptions{ OutputFile: output, Keep: "1", }) if err == nil || !strings.Contains(err.Error(), "--input-file is required") { t.Fatalf("expected input-file required error, got %v", err) } _, err = NewTrimConfig(TrimOptions{ InputFile: input, Keep: "1", }) if err == nil || !strings.Contains(err.Error(), "--output-file is required") { t.Fatalf("expected output-file required error, got %v", err) } } func TestNewTrimConfigRequiresExactlyOneSelectorFlag(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "trimmed.json") opts := validTrimOptions(input, output) opts.Keep = "" _, err := NewTrimConfig(opts) if err == nil || !strings.Contains(err.Error(), "exactly one of --keep or --remove is required") { t.Fatalf("expected missing selector error, got %v", err) } opts = validTrimOptions(input, output) opts.Remove = "2" _, err = NewTrimConfig(opts) if err == nil || !strings.Contains(err.Error(), "mutually exclusive") { t.Fatalf("expected mutually exclusive selector error, got %v", err) } } func TestNewTrimConfigAcceptsOutputSchemaOverride(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "trimmed.json") reportPath := filepath.Join(dir, "report.json") opts := validTrimOptions(input, output) opts.Keep = "" opts.Remove = "3-5" opts.ReportFile = reportPath opts.OutputSchema = OutputSchemaMinimal opts.AllowEmpty = true cfg, err := NewTrimConfig(opts) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.Mode != "remove" { t.Fatalf("mode = %q, want remove", cfg.Mode) } if cfg.Selector != "3-5" { t.Fatalf("selector = %q, want 3-5", cfg.Selector) } if cfg.OutputSchema != OutputSchemaMinimal { t.Fatalf("output schema = %q, want %q", cfg.OutputSchema, OutputSchemaMinimal) } if !cfg.AllowEmpty { t.Fatal("allow empty should be true") } if cfg.ReportFile != reportPath { t.Fatalf("report file = %q, want %q", cfg.ReportFile, reportPath) } } func TestNewTrimConfigTreatsWhitespaceReportFileAsOmitted(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "trimmed.json") opts := validTrimOptions(input, output) opts.ReportFile = " \t " cfg, err := NewTrimConfig(opts) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.ReportFile != "" { t.Fatalf("report file = %q, want empty", cfg.ReportFile) } } func TestNewTrimConfigRejectsInvalidOutputSchemaOverride(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "trimmed.json") opts := validTrimOptions(input, output) opts.OutputSchema = "compact" _, err := NewTrimConfig(opts) if err == nil { t.Fatal("expected output schema validation error") } if !strings.Contains(err.Error(), "--output-schema must be one of") { t.Fatalf("unexpected error: %v", err) } } func TestNewNormalizeConfigRequiresInputFile(t *testing.T) { dir := t.TempDir() output := filepath.Join(dir, "normalized.json") _, err := NewNormalizeConfig(NormalizeOptions{ OutputFile: output, OutputModules: DefaultOutputModules, }) if err == nil { t.Fatal("expected input-file required error") } if !strings.Contains(err.Error(), "--input-file is required") { t.Fatalf("unexpected error: %v", err) } } func TestNewNormalizeConfigRequiresOutputFile(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") opts := validNormalizeOptions(input, "") _, err := NewNormalizeConfig(opts) if err == nil { t.Fatal("expected output-file required error") } if !strings.Contains(err.Error(), "--output-file is required") { t.Fatalf("unexpected error: %v", err) } } func TestNewNormalizeConfigResolvesOutputSchemaDefaultAndEnv(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "normalized.json") t.Setenv(OutputSchemaEnv, "") opts := validNormalizeOptions(input, output) cfg, err := NewNormalizeConfig(opts) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.OutputSchema != DefaultOutputSchema { t.Fatalf("output schema = %q, want %q", cfg.OutputSchema, DefaultOutputSchema) } t.Setenv(OutputSchemaEnv, OutputSchemaMinimal) cfg, err = NewNormalizeConfig(opts) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.OutputSchema != OutputSchemaMinimal { t.Fatalf("output schema = %q, want %q", cfg.OutputSchema, OutputSchemaMinimal) } } func TestNewNormalizeConfigRejectsInvalidOutputSchema(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "normalized.json") opts := validNormalizeOptions(input, output) opts.OutputSchema = "compact" _, err := NewNormalizeConfig(opts) if err == nil { t.Fatal("expected output schema error") } if !strings.Contains(err.Error(), "--output-schema must be one of") { t.Fatalf("unexpected error: %v", err) } } func TestNewNormalizeConfigRejectsUnknownOutputModule(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "normalized.json") opts := validNormalizeOptions(input, output) opts.OutputModules = "json,yaml" _, err := NewNormalizeConfig(opts) if err == nil { t.Fatal("expected output module error") } if !strings.Contains(err.Error(), "unknown output module") { t.Fatalf("unexpected error: %v", err) } } func TestNewNormalizeConfigTreatsWhitespaceReportFileAsOmitted(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "normalized.json") opts := validNormalizeOptions(input, output) opts.ReportFile = "\n\t " cfg, err := NewNormalizeConfig(opts) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.ReportFile != "" { t.Fatalf("report file = %q, want empty", cfg.ReportFile) } } func TestNewRenderConfigRequiresInputOutputAndFormat(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "rendered.md") _, err := NewRenderConfig(RenderOptions{ OutputFile: output, Format: RenderFormatMarkdown, }) if err == nil || !strings.Contains(err.Error(), "--input-file is required") { t.Fatalf("expected input-file required error, got %v", err) } _, err = NewRenderConfig(RenderOptions{ InputFile: input, Format: RenderFormatMarkdown, }) if err == nil || !strings.Contains(err.Error(), "--output-file is required") { t.Fatalf("expected output-file required error, got %v", err) } _, err = NewRenderConfig(RenderOptions{ InputFile: input, OutputFile: output, }) if err == nil || !strings.Contains(err.Error(), "--format is required") { t.Fatalf("expected format required error, got %v", err) } } func TestNewRenderConfigRejectsUnknownFormat(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "rendered.md") opts := validRenderOptions(input, output) opts.Format = "txt" _, err := NewRenderConfig(opts) if err == nil { t.Fatal("expected format validation error") } if !strings.Contains(err.Error(), "--format must be") { t.Fatalf("unexpected error: %v", err) } } func TestNewRenderConfigAppliesDefaultsAndFlags(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "rendered.md") cfg, err := NewRenderConfig(validRenderOptions(input, output)) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.Title != DefaultRenderTitle { t.Fatalf("title = %q, want %q", cfg.Title, DefaultRenderTitle) } if !cfg.IncludeTimestamps { t.Fatal("include timestamps should default true") } if cfg.IncludeSegmentIDs { t.Fatal("include segment IDs should default false") } if cfg.IncludeMetadata { t.Fatal("include metadata should default false") } opts := validRenderOptions(input, output) opts.Title = "Meeting Notes" opts.IncludeTimestamps = false opts.IncludeSegmentIDs = true opts.IncludeMetadata = true cfg, err = NewRenderConfig(opts) if err != nil { t.Fatalf("config failed: %v", err) } if cfg.Title != "Meeting Notes" { t.Fatalf("title = %q, want Meeting Notes", cfg.Title) } if cfg.IncludeTimestamps { t.Fatal("include timestamps should be false") } if !cfg.IncludeSegmentIDs { t.Fatal("include segment IDs should be true") } if !cfg.IncludeMetadata { t.Fatal("include metadata should be true") } } func TestNewRenderConfigRejectsMissingAndDirectoryInputFile(t *testing.T) { dir := t.TempDir() output := filepath.Join(dir, "rendered.md") missingInput := filepath.Join(dir, "missing.json") _, err := NewRenderConfig(validRenderOptions(missingInput, output)) if err == nil { t.Fatal("expected missing input-file error") } if !strings.Contains(err.Error(), "--input-file") { t.Fatalf("unexpected error: %v", err) } inputDir := filepath.Join(dir, "input-dir") if err := os.MkdirAll(inputDir, 0o700); err != nil { t.Fatalf("mkdir input dir: %v", err) } _, err = NewRenderConfig(validRenderOptions(inputDir, output)) if err == nil { t.Fatal("expected directory input-file error") } if !strings.Contains(err.Error(), "is a directory, not a file") { t.Fatalf("unexpected error: %v", err) } } func TestNewRenderConfigRejectsMissingOutputParent(t *testing.T) { dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "missing-parent", "rendered.md") _, err := NewRenderConfig(validRenderOptions(input, output)) if err == nil { t.Fatal("expected output parent directory error") } if !strings.Contains(err.Error(), "--output-file parent directory") { t.Fatalf("unexpected error: %v", err) } } func assertPositiveFloatEnvValidation(t *testing.T, envName string) { t.Helper() tests := []struct { name string value string want string }{ {name: "non-numeric", value: "fast", want: "must be a positive number"}, {name: "zero", value: "0", want: "must be positive"}, {name: "negative", value: "-0.1", want: "must be positive"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Setenv(envName, test.value) dir := t.TempDir() input := writeTempFile(t, dir, "input.json") output := filepath.Join(dir, "merged.json") _, err := NewMergeConfig(validMergeOptions(input, output)) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), test.want) { t.Fatalf("expected error to contain %q, got %v", test.want, err) } }) } } func validMergeOptions(inputFile string, outputFile string) MergeOptions { return MergeOptions{ InputFiles: []string{inputFile}, OutputFile: outputFile, InputReader: DefaultInputReader, OutputModules: DefaultOutputModules, PreprocessingModules: DefaultPreprocessingModules, PostprocessingModules: DefaultPostprocessingModules, } } func validTrimOptions(inputFile string, outputFile string) TrimOptions { return TrimOptions{ InputFile: inputFile, OutputFile: outputFile, Keep: "1", } } func validNormalizeOptions(inputFile string, outputFile string) NormalizeOptions { return NormalizeOptions{ InputFile: inputFile, OutputFile: outputFile, OutputModules: DefaultOutputModules, } } func validRenderOptions(inputFile string, outputFile string) RenderOptions { return RenderOptions{ InputFile: inputFile, OutputFile: outputFile, Format: RenderFormatMarkdown, Title: DefaultRenderTitle, IncludeTimestamps: true, IncludeSegmentIDs: false, IncludeMetadata: false, } } func writeTempFile(t *testing.T, dir string, name string) string { t.Helper() path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte("{}\n"), 0o600); err != nil { t.Fatalf("write temp file: %v", err) } return path }