from dataclasses import asdict, fields import pytest from voiceforge.cli import main, parser, settings_for from voiceforge.config import PROFILES, RANGES, Settings, resolve, toml from voiceforge.pipeline import filters @pytest.mark.parametrize("profile", PROFILES) def test_profiles_and_toml_round_trip(profile, tmp_path): settings = resolve(profile=profile) assert asdict(settings) == asdict(Settings()) | PROFILES[profile] | {"profile": profile} path = tmp_path / "settings.toml" path.write_text(toml(settings)) assert resolve(path) == settings def test_precedence_defaults_profile_file_flags_then_set(tmp_path): path = tmp_path / "settings.toml" path.write_text('profile = "narrator"\ncompressor_ratio = 2.2\nwarmth_db = 3\n') args = parser().parse_args([ "config", "--config", str(path), "--profile", "radio", "--warmth-db", "4", "--set", "warmth_db=5", "--set", "warmth_db=6", ]) settings = settings_for(args) assert settings.profile == "radio" assert settings.compressor_attack_ms == 8 # Selected profile beats file profile. assert settings.compressor_ratio == 2.2 # File values beat profile defaults. assert settings.warmth_db == 6 # Last --set beats named flags. assert settings.sample_rate == 48000 @pytest.mark.parametrize("field", [f.name for f in fields(Settings) if type(getattr(Settings(), f.name)) is bool]) def test_boolean_flags_preserve_unspecified_and_accept_both_forms(field): assert getattr(parser().parse_args(["config"]), field) is None for enabled in (True, False): flag = "--" + ("" if enabled else "no-") + field.replace("_", "-") extra = ["--limiter"] if field == "normalize" and enabled else [] args = parser().parse_args(["config", "--profile", "cleanup-only", flag, *extra]) assert getattr(settings_for(args), field) is enabled def test_false_cli_toggle_overrides_true_file_value(tmp_path): path = tmp_path / "settings.toml" path.write_text("eq = true\n") args = parser().parse_args(["config", "--config", str(path), "--no-eq"]) assert settings_for(args).eq is False def test_no_denoise_and_unquoted_set_strings(): assert settings_for(parser().parse_args(["config", "--no-denoise"])).denoiser == "none" args = parser().parse_args(["config", "--no-denoise", "--set", "denoiser=fft"]) assert settings_for(args).denoiser == "fft" @pytest.mark.parametrize("key", RANGES) def test_numeric_ranges_are_inclusive_and_reject_outside(key): low, high = RANGES[key] for value in (low, high): assert getattr(resolve(overrides={key: value}), key) == value for value in (low - 0.01, high + 0.01): with pytest.raises(ValueError, match=key): resolve(overrides={key: value}) @pytest.mark.parametrize("overrides,match", [ ({"typo": True}, "Unknown setting"), ({"highpass": 1}, "highpass must be bool"), ({"sample_rate": 48000.0}, "sample_rate must be int"), ({"target_lufs": True}, "finite number"), ({"target_lufs": float("nan")}, "finite number"), ({"target_lufs": float("inf")}, "finite number"), ({"denoiser": "unknown"}, "denoiser must be one of"), ({"device": "gpu"}, "device must be one of"), ({"channel": "stereo"}, "channel must be one of"), ({"hum_hz": 55}, "hum_hz must be one of"), ({"sample_rate": 96000}, "sample_rate must be one of"), ({"mp3_bitrate": 64}, "mp3_bitrate must be one of"), ({"limiter": False}, "Normalization includes true-peak limiting"), ]) def test_invalid_configuration(overrides, match): with pytest.raises(ValueError, match=match): resolve(overrides=overrides) def test_unknown_profile_and_malformed_toml(tmp_path): with pytest.raises(ValueError, match="Unknown profile"): resolve(profile="missing") path = tmp_path / "bad.toml" path.write_text("normalize = [") with pytest.raises(ValueError): resolve(path) def test_invalid_cli_configuration_fails_before_processing(monkeypatch, capsys): def unexpected(*args, **kwargs): pytest.fail("Invalid configuration must not reach processing or tool setup") monkeypatch.setattr("voiceforge.cli.process", unexpected) monkeypatch.setattr("voiceforge.cli.ensure_ffmpeg", unexpected) monkeypatch.setattr("sys.argv", ["voiceforge", "process", "missing.wav", "--target-lufs", "0"]) with pytest.raises(SystemExit) as error: main() assert error.value.code == 1 assert "target_lufs must be between" in capsys.readouterr().err def test_set_requires_assignment(): with pytest.raises(ValueError, match="KEY=VALUE"): settings_for(parser().parse_args(["config", "--set", "normalize"])) @pytest.mark.parametrize("profile", ["radio", "not-a-profile"]) def test_set_profile_uses_normal_profile_resolution(profile): args = parser().parse_args(["config", "--set", f"profile={profile}"]) if profile not in PROFILES: with pytest.raises(ValueError, match="Unknown profile"): settings_for(args) else: assert settings_for(args) == resolve(profile=profile) @pytest.mark.parametrize("toggle,prefix,count", [ ("expansion", "agate=", 1), ("eq", "equalizer=", 3), ("compression", "acompressor=", 1), ("deess", "deesser=", 1), ("lowpass", "lowpass=", 1), ]) def test_shaping_filter_toggles(bypass, toggle, prefix, count): assert filters(bypass) == ["anull"] setattr(bypass, toggle, True) chain = filters(bypass) assert len(chain) == count assert all(item.startswith(prefix) for item in chain)